/*
 * This file is part of the Owl Positioning System (OwlPS) project.
 * It is subject to the copyright notice and license terms in the
 * COPYRIGHT.t2t file found in the top-level directory of this
 * distribution and at
 *   https://code.lm7.fr/mcy/owlps/src/master/COPYRIGHT.t2t
 * No part of the OwlPS Project, including this file, may be copied,
 * modified, propagated, or distributed except according to the terms
 * contained in the COPYRIGHT.t2t file; the COPYRIGHT.t2t file must be
 * distributed along with this file, either separately or by replacing
 * this notice by the COPYRIGHT.t2t file's contents.
 */


#include "userinterface.hh"
#include "configuration.hh"

#include <owlps-config.h>

#include <owlps.h>

#include <iostream>
#include <fstream>
#include <boost/program_options.hpp>

using namespace std ;
namespace po = boost::program_options ;



/* *** Default value definitions *** */

/* General options */
#define DEFAULT_CONFIG_FILE OWL_CONFIG_PREFIX "/owlps-positioner.conf"

/* Positioning options */
#define DEFAULT_SS_SIMILARITY "mean"
#define DEFAULT_MESHING_GRAIN 0.5
#define DEFAULT_Z_MESHING_GRAIN 1
#define DEFAULT_SMALLEST_SS -99

/* Output options */
#define DEFAULT_TCPEVAAL_HOST "127.0.0.1"
#define DEFAULT_TCPEVAAL_PORT 4444



/* *** Constructors *** */


/**
 * `argc` and `argv` are (normally) the ones passed to the main()
 * function.
 * @param argc Number of arguments passed to the program.
 * @param argv Values of the arguments.
 */
UserInterface::UserInterface(const int argc,
                             const char *const *const argv):
  cli_argument_count(argc), cli_argument_values(argv)
{
  assert(cli_argument_values) ;

  try
    {
      cli_options = new po::options_description("General options") ;
      file_options = new po::options_description("Parameters") ;
    }
  catch (bad_alloc &e)
    {
      throw ;
    }

  fill_options() ;
  parse_options() ;
}


UserInterface::~UserInterface()
{
  delete cli_options ;
  delete file_options ;
}



/* *** Operations *** */


void UserInterface::fill_options()
{
  fill_cli_options() ;
  fill_file_options() ;

  // File options are also accepted on the command line
  cli_options->add(*file_options) ;
}


void UserInterface::fill_cli_options()
{
  cli_options->add_options()
  ("help,h",
   "Print help.")
  ("version,V",
   "Print version information.")
  ("config-file,f",
   po::value<string>()->default_value(DEFAULT_CONFIG_FILE),
   "Alternative configuration file.")
  ("verbose,v",
   "Verbose mode.")
  ; // End of options
}


void UserInterface::fill_file_options()
{
  fill_data_input_options() ;
  fill_input_options() ;
  fill_log_options() ;
  fill_positioning_options() ;
  fill_output_options() ;
  fill_misc_options() ;
}


void UserInterface::fill_data_input_options()
{
  po::options_description options("Data input options") ;

  options.add_options()
  ("data-input.cp-medium",
   po::value< vector<string> >()->composing(),
   "Medium from which capture points are read. You can specify this"
   " option more than once. Allowed: CSV.")
  ("data-input.cp-csv-file",
   po::value<string>(),
   "CSV file to use for capture points input (when"
   " data-input.cp-medium = CSV).")
  ("data-input.mobile-medium",
   po::value< vector<string> >()->composing(),
   "Medium from which mobiles are read. You can specify this"
   " option more than once. Allowed: CSV.")
  ("data-input.mobile-csv-file",
   po::value<string>(),
   "CSV file to use for mobiles input (when"
   " data-input.mobile-medium = CSV).")
  ("data-input.topology-medium",
   po::value< vector<string> >()->composing(),
   "Medium from which topology (buildings, areas and waypoints) is"
   " read. You can specify this option more than once. Allowed: CSV.")
  ("data-input.areas-csv-file",
   po::value<string>(),
   "CSV file to use for topology input (when"
   " data-input.topology-medium = CSV).")
  ("data-input.waypoints-csv-file",
   po::value<string>(),
   "CSV file to use for waypoints input (when"
   " data-input.topology-medium = CSV).")
  ("data-input.reference-points-medium",
   po::value< vector<string> >()->composing(),
   "Medium from which reference points are read. You can specify this"
   " option more than once. Allowed: CSV.")
  ("data-input.reference-points-csv-file",
   po::value<string>(),
   "CSV file to use for reference points input (when"
   " data-input.reference-points-medium = CSV).")
  ;

  file_options->add(options) ;
}


void UserInterface::fill_input_options()
{
  po::options_description options("Input options") ;

  options.add_options()
  ("input.medium,I",
   po::value<string>(),
   "Medium from which requests are read. Allowed: CSV, UDP.")
  ("input.csv-file",
   po::value<string>(),
   "CSV file to use for input (when input.medium = CSV).")
  ("input.udp-port",
   po::value<int>()->default_value(OWL_DEFAULT_AGGREGATION_PORT),
   "Port on which the UDP socket listens (when input.medium = UDP).")
  ;

  file_options->add(options) ;
}


void UserInterface::fill_log_options()
{
  po::options_description options("Log options") ;

  options.add_options()
  ("log.medium,L",
   po::value< vector<string> >()->composing(),
   "Medium to which the requests will be logged. You can specify"
   " this option more than once. Allowed: none, CSV. The `none`"
   " value completely disables logging.")
  ("log.csv-file",
   po::value<string>(),
   "CSV file to use for logging (when log.medium = CSV).")
  ;

  file_options->add(options) ;
}


void UserInterface::fill_positioning_options()
{
  po::options_description options("Positioning options") ;

  options.add_options()
  ("positioning.algorithm,a",
   po::value< vector<string> >()->composing(),
   "Algorithms used to compute positions. You can specify"
   " this option more than once (but at least once). Allowed: Real,"
   " FBCM, FRBHMBasic, InterlinkNetworks, NSS.")
  ("positioning.accept-new-mobiles",
   po::value<bool>()->default_value(false),
   "When receiving requests, add unknown mobiles (mobiles which are not"
   " declared in the mobiles' configuration file) to the mobiles' list.")
  ("positioning.accept-new-cps",
   po::value<bool>()->default_value(false),
   "When receiving requests, add unknown CPs (CPs which are not"
   " declared in the CPs' configuration file) to the CPs' list"
   " (default is false, for security purposes).")
  ("positioning.update-cp-coordinates-online",
   po::value<bool>()->default_value(false),
   "Allow CP's coordinates to be updated when a calibration request"
   " with new coordinates is received from the CP (default is false,"
   " for security purposes).")
  ("positioning.area-start",
   po::value<string>(),
   "Coordinates of the first point of the deployment area; this is"
   " used to delimit the area in which the MinMax trilateration"
   " method tests points and in which the reference points are"
   " generated, if the corresponding options are activated"
   " (string format: \"X;Y;Z\").")
  ("positioning.area-stop",
   po::value<string>(),
   "Coordinates of the last point of the deployment area"
   " (string format: \"X;Y;Z\").")
  ("positioning.smallest-ss",
   po::value<int>()->default_value(DEFAULT_SMALLEST_SS),
   "Smallest possible value for a received signal strength, in dBm.")
  ("positioning.ss-similarity",
   po::value<string>()->default_value(DEFAULT_SS_SIMILARITY),
   "Algorithm to calculate the similarity, in the signal strength space,"
   " between two measurements. Allowed: mean, interval, interval2.")
  ("positioning.generate-reference-points",
   po::value<string>()->default_value("false"),
   "Generate reference points from the (auto)calibration requests"
   " received. Can be 'false', 'mesh' to generate reference points"
   " regularly according to the meshing grain specified and the"
   " positioning area defined, 'line' to generate reference points"
   " regularly according to a straight path (cf. options"
   " generated-line-path and generated-line-step), 'list' to generate"
   " reference points from a list (cf. option generated-points-list),"
   " or a combination of the three previous options (separated by any"
   " character) to generate points in several ways.")
  ("positioning.generate-multi-packet-reference-points",
   po::value<bool>()->default_value(true),
   "Generate several packets per reference point by trying to match the"
   " packets in the real requests. If false, the SS values of the"
   " requests are averaged and only one packet is generated.")
  ("positioning.generated-meshing-grain-x",
   po::value<float>()->default_value(DEFAULT_MESHING_GRAIN),
   "When generate-reference-points includes 'mesh', this distance (in"
   " metres) will separate each point to the next in X.")
  ("positioning.generated-meshing-grain-y",
   po::value<float>()->default_value(DEFAULT_MESHING_GRAIN),
   "When generate-reference-points includes 'mesh', this distance (in"
   " metres) will separate each point to the next in Y.")
  ("positioning.generated-meshing-grain-z",
   po::value<float>()->default_value(DEFAULT_Z_MESHING_GRAIN),
   "When generate-reference-points includes 'mesh', this parameter"
   " represents the number of the floor. Currently, each increment of Y"
   " is a new floor (full 3-D coordinates are not supported yet).")
  ("positioning.generated-line-path",
   po::value<string>(),
   "Path of reference points to generate, if generate-reference-points"
   " includes 'line'; the reference points are generated in straight"
   " lines between the given points, and one of the points must be the"
   " coordinates of a CP (string format: \"(X1;Y1;Z1);(X2;Y2;Z2);...\""
   " or \"X1;Y1;Z1;X2;Y2;Z2;...\").")
  ("positioning.generated-line-step",
   po::value<float>()->default_value(DEFAULT_MESHING_GRAIN),
   "When generate-reference-points includes 'line', two generated points"
   " will be separated approximately by this distance (in metres).")
  ("positioning.generated-points-list",
   po::value<string>(),
   "List of reference points to generate, if generate-reference-points"
   " includes 'list' (string format: \"(X1;Y1;Z1);(X2;Y2;Z2);...\" or"
   " \"X1;Y1;Z1;X2;Y2;Z2;...\").")
  ("positioning.accept-new-calibration-requests",
   po::value<bool>()->default_value(false),
   "Add the calibration requests received during the run-time to"
   " the calibration requests' list; this is required for the"
   " self-calibration. If unactivated, the calibration requests"
   " are handled as positioning requests (default is unactivated,"
   " for security purposes).")
  ("positioning.calibration-requests-timeout",
   po::value<unsigned int>()->default_value(0),
   "Maximum age of a calibration request before to delete it"
   " (0 = unlimited). See also the \"replay\" option.")
  ("positioning.unique-calibration-requests",
   po::value<bool>()->default_value(true),
   "Always delete existing former calibration requests when receiving"
   " a new calibration request for an existing reference point.")
  ("positioning.position-calibration-requests",
   po::value<bool>()->default_value(false),
   "When accept-new-calibration-requests is activated, allow the"
   " calibration requests to be positioned as normal requests."
   " The default is to add them to the calibration requests' list"
   " without position them.")
  ("positioning.nss.average-reference-points",
   po::value<bool>()->default_value(false),
   "With the NSS algorithm, for a given positioning request, average"
   " all the calibration requests associated with a reference point"
   " before to compute the SS similarity."
   " The default is false, i.e the positioning request is compared"
   " directly to each calibration request.")
  ("positioning.nss.ignore-cp-reference-points",
   po::value<bool>()->default_value(false),
   "With the NSS algorithm, try to avoid selecting the reference"
   " points which are coordinates of a CP.")
  ("positioning.filter.max-speed",
   po::value<float>()->default_value(0),
   "Maximal speed at which the mobiles can move, in km/h (0 = unlimited"
   " speed).")
  ("positioning.filter.cp-reset-distance",
   po::value<float>()->default_value(0),
   "Use max-speed-cp instead of max-speed when the computed position is"
   " at this distance (in m) from a CP or closer (0 = never use"
   " max-speed-cp).")
  ("positioning.filter.max-speed-cp",
   po::value<float>()->default_value(0),
   "Maximal (virtual) speed at which the mobiles can move when they are"
   " within cp-reset-distance, in km/h (0 = unlimited speed, i.e."
   " completely disables filtering when the mobile is close to a CP).")
  ;

  file_options->add(options) ;
}


void UserInterface::fill_output_options()
{
  po::options_description options("Output options") ;

  options.add_options()
  ("output.2d-error",
   po::value<bool>()->default_value(false),
   "Compute the distance error in two dimensions instead of three"
   " dimensions.")
  ("output.medium,O",
   po::value< vector<string> >()->composing(),
   "Medium to which the results will be written. You can specify"
   " this option more than once."
   " Allowed: Terminal, CSV, UDP, TCPEvAAL (EvAAL 2012 format)."
   " If this option is absent, the results are printed on the terminal.")
  ("output.csv-file",
   po::value<string>(),
   "CSV file to use for output (when output.medium = CSV).")
  ("output.udp-host",
   po::value<string>(),
   "Host to which the UDP data is sent (when output.medium = UDP).")
  ("output.udp-port",
   po::value<int>()->default_value(OWL_DEFAULT_RESULT_PORT),
   "Port on which the UDP data is sent (when output.medium = UDP).")
  ("output.tcpevaal-host",
   po::value<string>()->default_value(DEFAULT_TCPEVAAL_HOST),
   "IP address of the host to which the TCP data is sent (when"
   " output.medium = TCPEvAAL).")
  ("output.tcpevaal-port",
   po::value<int>()->default_value(DEFAULT_TCPEVAAL_PORT),
   "Port on which the TCP data is sent (when output.medium = TCPEvAAL).")
  ;

  file_options->add(options) ;
}


void UserInterface::fill_misc_options()
{
  po::options_description options("Miscellaneous options") ;

  options.add_options()
  ("flush-output-files",
   po::value<bool>()->default_value(true),
   "Flush output text files after each line.")
  ("replay",
   po::value<bool>()->default_value(false),
   "\"Replay\" mode. With this option enabled, the current time is the"
   " transmission timestamp of the most recent request; you will want"
   " to enable this when reading inputs (requests) offline to replay"
   " scenarios, if time-related options are enabled (e.g."
   " \"positioning.calibration-requests-timeout\"). To be useful, this"
   " option requires the listeners' clocks to be synchronised.")
  ;

  file_options->add(options) ;
}


void UserInterface::parse_options()
{
  parse_command_line() ;
  print_information() ;
  if (! owl_run)
    return ;

  parse_file() ;

  po::notify(Configuration::getw_configuration()) ;
}


void UserInterface::parse_command_line() const
{
  po::store(po::parse_command_line(cli_argument_count,
                                   cli_argument_values,
                                   *cli_options),
            Configuration::getw_configuration()) ;
}


/**
 * If information is printed, owl_run is set to false, to indicate that
 * the program should stop.
 */
void UserInterface::print_information() const
{
  // Print version if requested
  if (Configuration::is_configured("version"))
    {
      cout
          << "This is OwlPS Positioner, part of the Owl"
          << " Positioning System project.\nVersion: " <<
#ifdef OWLPS_VERSION
          OWLPS_VERSION
#else // OWLPS_VERSION
          "unknown version"
#endif // OWLPS_VERSION
          << ".\n" ;
      owl_run = false ; // Tell main() to exit
    }

  // Print usage if requested
  if (Configuration::is_configured("help"))
    {
      cout << *cli_options ;
      owl_run = false ; // Tell main() to exit
    }
}


void UserInterface::parse_file() const
{
  string config_file_name = Configuration::string_value("config-file") ;
  ifstream config_file(config_file_name.c_str()) ;

  if (! config_file)
    {
      cerr
          << "Warning! Error opening input configuration file \""
          << config_file_name
          << "\"! Using command line and default values..."
          << endl ;
      return ;
    }

  po::store(po::parse_config_file(config_file, *file_options),
            Configuration::getw_configuration()) ;
}
