//
// fgcom_init.cxx -- FGCOM configuration parsing and initialization
// FGCOM: Copyright (C) H. Wirtz <wirtz@dfn.de>
//
// Adaption of fg_init.cxx from FlightGear
// FlightGear: Copyright (C) 1997  Curtis L. Olson  - http://www.flightgear.org/~curt
//
// Huge part rewritten by Tobias Ramforth to fit needs of FGCOM.
// <tobias@ramforth.com>
//
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#if defined( _MSC_VER) || defined(__MINGW32__)
#  include <direct.h>		// for getcwd()
#  define getcwd _getcwd
#endif

#ifdef _MSC_VER
#include "fgcom_getopt.h"
#else
#include <pwd.h>
#endif

#include <iostream>
#include <fstream>
#include <iomanip>
#include <map>

#include "fgcom_init.hxx"
#include "fgcom.hxx"
#include "utils.hxx"

#include <simgear/debug/logstream.hxx>

using namespace std;
using std::string;
using std::cerr;
using std::endl;

#define ARRAY_SIZE(x) (sizeof(x) / sizeof(*(x)))

// Manipulators
istream& skip_eol( istream& in ) {
    char c = '\0';
    // skip to end of line.
    while ( in.get(c) ) {
        if ( (c == '\n') || (c == '\r') ) {
            break;
        }
    }
    return in;
}

istream& skip_ws( istream& in ) {
    char c;
    while ( in.get(c) ) {
        if ( ! isspace( c ) ) {
            // put back the non-space character
            in.putback(c);
            break;
        }
    }
    return in;
}

istream& skip_comment( istream& in )
{
    while ( in ) {
        // skip whitespace
        in >> skip_ws;
        char c;
        if ( in.get( c ) && c != '#' ) {
            // not a comment
            in.putback(c);
            break;
        }
        in >> skip_eol;
    }
    return in;
}


/* program name */
extern char *prog;
extern const char * default_root;

static std::string config;

static OptionEntry *fgcomOptionArray = 0;

static void _doOptions (int argc, char **argv);
static int _parseOption (const std::string & arg, const std::string & next_arg);
static void _fgcomParseArgs (int argc, char **argv);
static void _fgcomParseOptions (const std::string & path);

// Read in configuration (file and command line)
bool fgcomInitOptions (const OptionEntry * fgcomOptions, int argc, char **argv)
{
    if (!fgcomOptions) {
        SG_LOG( SG_GENERAL, SG_ALERT, "Error! Uninitialized fgcomOptionArray!" );
        return false;
    }

    // set option array
    int n_options;
    for (n_options = 0; fgcomOptions[n_options].long_option != NULL; n_options++) {}

    fgcomOptionArray = (OptionEntry *) realloc (fgcomOptionArray, sizeof (OptionEntry) * (n_options + 1));
    memcpy (fgcomOptionArray, fgcomOptions, sizeof (OptionEntry) * (n_options + 1));

    // parse options
    _doOptions (argc, argv);

    return true;
}

// Create usage information out of fgcomOptionArray
void
fgcomUsage ()
{
  size_t
    max_length = 0;

  if (!fgcomOptionArray)
    {
      SG_LOG( SG_GENERAL, SG_ALERT, "Error! Options need to be initialized by calling 'fgcomInitConfig'!" );
      return;
    }


  // find longest long_option
  const OptionEntry *
    currentEntry = &fgcomOptionArray[0];
  while (currentEntry->long_option != 0)
    {
      size_t
	current_length = strlen (currentEntry->long_option);
      if (current_length > max_length)
	{
	  max_length = current_length;
	}
      currentEntry++;
    }

  max_length *= 2;
  max_length += 10;		// for "-o, --option=, -option"

  // print head
  std::cout << "  OPTION" << std::string (max_length - 8,
					  ' ') << "\t\t" << "DESCRIPTION" <<
    std::endl;
  std::cout << std::endl;

  // iterate through option array
  currentEntry = &fgcomOptionArray[0];
  while (currentEntry->long_option != 0)
    {
      size_t
	current_length = strlen (currentEntry->long_option);

      current_length *= 2;
      current_length += 10;

      std::string option = std::string ("  -")
	+ std::string (&currentEntry->option);
      if (option.size() > 4)
          option = option.substr(0,4);
	option += std::string (", -")
	+ std::string (currentEntry->long_option)
	+ std::string (", --")
	+ std::string (currentEntry->long_option)
	+ std::string ("=") + std::string (max_length - current_length, ' ');

      std::cout << option << "\t\t" << currentEntry->description;

      if (currentEntry->has_param && (currentEntry->type != OPTION_NONE)
	  && (currentEntry->default_value != 0))
	{
	  std::cout << " (default: '";

	  if (currentEntry->type == OPTION_NONE)
	    {

	    }
	  else if (currentEntry->type == OPTION_BOOL)
	    {
	      std::cout << *(bool *) currentEntry->default_value;
	    }
	  else if (currentEntry->type == OPTION_STRING)
	    {
	      std::cout << (char *) currentEntry->default_value;
	    }
	  else if (currentEntry->type == OPTION_FLOAT)
	    {
	      std::cout << *(float *) currentEntry->default_value;
	    }
	  else if (currentEntry->type == OPTION_DOUBLE)
	    {
	      std::cout << *(double *) currentEntry->default_value;
	    }
	  else if (currentEntry->type == OPTION_FREQ)
	    {
	      std::cout << std::setw (7) << std::
		setprecision (3) << *(double *) currentEntry->default_value;
	    }
	  else if (currentEntry->type == OPTION_INT)
	    {
	      std::cout << *(int *) currentEntry->default_value;
	    }
	  else if (currentEntry->type == OPTION_CHAR)
	    {
	      std::cout << *(char *) currentEntry->default_value;
	    }

	  std::cout << "')";
	}

      std::cout << std::endl;

      currentEntry++;
    }

  std::cout << std::endl;

  std::cout << "  Available codecs:" << std::endl;
  std::cout << "  \t" <<
    "u - ulaw (default and best codec because the mixing is based onto ulaw)"
    << std::endl;
  std::cout << "  \t" << "a - alaw" << std::endl;
  std::cout << "  \t" << "g - gsm" << std::endl;
  std::cout << "  \t" << "s - speex" << std::endl;
  std::cout << "  \t" << "7 - G.723" << std::endl;

  std::cout << std::endl;
  std::cout << std::endl;

  std::cout << "  Mode 1: client for COM1 of flightgear:" << std::endl;
  std::cout << "  \t" << "$ " << prog << std::endl;
  std::cout << "  - connects " << prog << " to fgfs at localhost:" <<
    DEFAULT_FG_PORT << std::endl;
  std::cout << "  \t" << "$ " << prog << " -sother.host.tld -p23456" <<
    std::endl;
  std::cout << "  - connects " << prog << " to fgfs at other.host.tld:23456" << std::endl;

  std::cout << std::endl;

  std::cout << "  Mode 2: client for an ATC at <airport> on <frequency>:" <<
    std::endl;
  std::cout << "  \t" << "$ " << prog << " -aKSFO -f120.500" << std::endl;
  std::cout << "  - sets up " << prog <<
    " for an ATC radio at KSFO 120.500 MHz" << std::endl;

  std::cout << std::endl;
  std::cout << std::endl;

  std::cout << "Note that " << prog <<
    " starts with a guest account unless you use -U and -P!" << std::endl;

  std::cout << std::endl;
}

static char *
get_alternate_home(void)
{
	char *ah = 0;
#ifdef _MSC_VER
	char *app_data = getenv("LOCALAPPDATA");
	if (app_data) {
		ah  = _strdup(app_data);
	}
#else
      struct passwd *
	pwd = getpwent ();
      ah = strdup (pwd->pw_dir);
#endif
	return ah;
}

// Attempt to locate and parse the various non-XML config files in order
// from least precidence to greatest precidence
static void
_doOptions (int argc, char **argv)
{
  char *
    homedir = getenv ("HOME");

  if (homedir == NULL)
    {
	  homedir = get_alternate_home();
    }

  // Check for ~/.fgfsrc
  if (homedir != NULL)
    {
      config = string (homedir);

#ifdef _WIN32
      config.append ("\\");
#else
      config.append ("/");
#endif

      config.append (".fgcomrc");
      _fgcomParseOptions (config);
    }

  // Parse remaining command line options
  // These will override anything specified in a config file
  _fgcomParseArgs (argc, argv);
}

// lookup maps
static std::map<string, string> fgcomOptionMap;
static std::map<string, size_t> fgcomLongOptionMap;

// Parse a single option
static int
_parseOption (const std::string & arg, const std::string & next_arg)
{
  if (fgcomLongOptionMap.size () == 0)
    {
      size_t i = 0;
      const OptionEntry * entry = &fgcomOptionArray[0];
      while (entry->long_option != 0)
	{
	  fgcomLongOptionMap.insert (std::pair < std::string,
				     size_t >
				     (std::string (entry->long_option), i));
	  fgcomOptionMap.insert (std::pair < std::string,
				 std::string >
				 (std::string (1, entry->option),
				  std::string (entry->long_option)));
	  i += 1;
	  entry += 1;
	}
    }

  // General Options
  if ((arg == "--help") || (arg == "-h") || (arg == "-?"))
    {
      // help/usage request
      return FGCOM_OPTIONS_HELP;
    }
  else if ((arg == "--verbose") || (arg == "-v"))
    {
      // verbose help/usage request
      return FGCOM_OPTIONS_VERBOSE_HELP;
    }
  else
    {
      std::map < string, size_t >::iterator it;
      std::string arg_name, arg_value;

      if (arg.find ("--") == 0)
	{
	  size_t
	    pos = arg.find ('=');
	  if (pos == string::npos)
	    {
	      // did not find a value
	      arg_name = arg.substr (2);

	      // now there are two possibilities:
	      //              1: this is an option without a value
	      //              2: the value can be found in next_arg
	      if (next_arg.empty ())
		{
		  // ok, value cannot be in next_arg
                  SG_LOG( SG_GENERAL, SG_DEBUG, "option '" << arg_name << "'" );
		}
	      else
		{
		  if (next_arg.at (0) == '-')
		    {
		      // there is no value, new option starts in next_arg
                      SG_LOG( SG_GENERAL, SG_DEBUG, "option '" << arg_name << "'" );
		    }
		  else
		    {
		      // the value is in next_arg
		      arg_value = std::string (next_arg);
                      SG_LOG( SG_GENERAL, SG_DEBUG, "option '" << arg_name << "' with argument '" << arg_value << "'" );
		    }
		}
	    }
	  else
	    {
	      // found a value
	      arg_name = arg.substr (2, pos - 2);
	      arg_value = arg.substr (pos + 1);
              SG_LOG( SG_GENERAL, SG_DEBUG, "option '" << arg_name << "' with argument '" << arg_value << "'" );
	    }

	  it = fgcomLongOptionMap.find (arg_name);
	}
      else
	{
	  std::map < string, string >::iterator it_b;
	  arg_name = arg.substr (1, 1);
	  arg_value = arg.substr (2);

	  if (arg_name.empty ())
	    {
	      SG_LOG (SG_GENERAL, SG_ALERT, "Unknown option '" << arg << "'");
	      return FGCOM_OPTIONS_ERROR;
	    }

          SG_LOG( SG_GENERAL, SG_DEBUG, "option '" << arg_name << "' with argument '" << arg_value << "'" );

	  it_b = fgcomOptionMap.find (arg_name);

	  if (it_b != fgcomOptionMap.end ())
	    {
	      it = fgcomLongOptionMap.find (it_b->second);
	    }
	  else
	    {
	      SG_LOG( SG_GENERAL, SG_ALERT, "Unknown option '" << arg << "'" );
	      return FGCOM_OPTIONS_ERROR;
	    }
	}

      if (it != fgcomLongOptionMap.end ())
	{
	  const OptionEntry *
	    entry = &fgcomOptionArray[it->second];
	  switch (entry->type)
	    {
	    case OPTION_BOOL:
	      *(bool *) entry->parameter = true;
	      break;
	    case OPTION_STRING:
	      if (entry->has_param && !arg_value.empty ())
		{
		  *(char **) entry->parameter = strdup (arg_value.c_str ());
		}
	      else if (entry->has_param)
		{
		  SG_LOG( SG_GENERAL, SG_ALERT, "Option '" << arg << "' needs a parameter" );
		  return FGCOM_OPTIONS_ERROR;
		}
	      else
		{
		  SG_LOG( SG_GENERAL, SG_ALERT, "Option '" << arg << "' does not have a parameter" );
		  return FGCOM_OPTIONS_ERROR;
		}
	      break;
	    case OPTION_FLOAT:
	      if (!arg_value.empty ())
		{
#ifdef _MSC_VER
		  float temp = atof(arg_value.c_str ());
#else // !_MSC_VER
		  char *
		    end;
		  float
		    temp = strtof (arg_value.c_str (), &end);

		  errno = 0;

		  if (*end != '\0')
		    {
		      SG_LOG( SG_GENERAL, SG_ALERT, "Cannot parse float value '" << arg_value << "' for option " << arg_name << "!" );
		      return FGCOM_OPTIONS_ERROR;
		    }
#endif // _MSC_VER y/n

		  *(float *) (entry->parameter) = temp;
		  if (*(float *) (entry->parameter) != temp
		      || errno == ERANGE)
		    {
		      SG_LOG( SG_GENERAL, SG_ALERT, "Float value '" << arg_value << "' for option " << arg_name << " out of range!" );
		      return FGCOM_OPTIONS_ERROR;
		    }
		}
	      else
		{
		  SG_LOG( SG_GENERAL, SG_ALERT, "Option '" << arg << "' needs a parameter" );
		  return FGCOM_OPTIONS_ERROR;
		}
	      break;
	    case OPTION_DOUBLE:
        case OPTION_FREQ:
	      if (!arg_value.empty ())
		{
		  char *
		    end;
		  double
		    temp = strtod (arg_value.c_str (), &end);

		  errno = 0;

		  if (*end != '\0')
		    {
		      SG_LOG( SG_GENERAL, SG_ALERT, "Cannot parse double value '" << arg_value << "' for option " << arg_name << "!" );
		      return FGCOM_OPTIONS_ERROR;
		    }

		  *(double *) (entry->parameter) = temp;
		  if (*(double *) (entry->parameter) != temp
		      || errno == ERANGE)
		    {
		      SG_LOG( SG_GENERAL, SG_ALERT, "Double value '" << arg_value << "' for option " << arg_name << " out of range!" );
		      return FGCOM_OPTIONS_ERROR;
		    }
		}
	      else
		{
		  SG_LOG( SG_GENERAL, SG_ALERT, "Option '" << arg << "' needs a parameter" );
		  return FGCOM_OPTIONS_ERROR;
		}
	      break;
	    case OPTION_INT:
	      if (!arg_value.empty ())
		{
		  char *
		    end;
		  long
		    temp = strtol (arg_value.c_str (), &end, 0);

		  errno = 0;

		  if (*end != '\0')
		    {
		      SG_LOG( SG_GENERAL, SG_ALERT, "Cannot parse integer value '" << arg_value << "' for option " << arg_name << "!" );
		      return FGCOM_OPTIONS_ERROR;
		    }

		  *(int *) (entry->parameter) = temp;
		  if (*(int *) (entry->parameter) != temp || errno == ERANGE)
		    {
		      SG_LOG( SG_GENERAL, SG_ALERT, "Integer value '" << arg_value << "' for option " << arg_name << " out of range!" );
		      return FGCOM_OPTIONS_ERROR;
		    }
		}
	      else
		{
		  SG_LOG( SG_GENERAL, SG_ALERT, "Option '" << arg << "' needs a parameter" );
		  return FGCOM_OPTIONS_ERROR;
		}
	      break;
	    case OPTION_CHAR:
	      if (entry->has_param && !arg_value.empty ())
		{
		  if (arg_value.length () > 1)
		    {
		      SG_LOG( SG_GENERAL, SG_ALERT, "Option '" << arg << "' needs a single char as parameter" );
		      return FGCOM_OPTIONS_ERROR;
		    }
		  else
		    {
		      *(char *) entry->parameter = arg_value.c_str ()[0];
		    }
		}
	      else if (entry->has_param)
		{
		  SG_LOG( SG_GENERAL, SG_ALERT, "Option '" << arg << "' needs a parameter" );
		  return FGCOM_OPTIONS_ERROR;
		}
	      else
		{
		  SG_LOG( SG_GENERAL, SG_ALERT, "Option '" << arg << "' does not have a parameter" );
		  return FGCOM_OPTIONS_ERROR;
		}
	      break;
	    case OPTION_NONE:
	      *(bool *) entry->parameter = true;
	      break;
	    }
	}
      else
	{
	  SG_LOG( SG_GENERAL, SG_ALERT, "Unknown option '" << arg << "'" );
	  return FGCOM_OPTIONS_ERROR;
	}
    }

  return FGCOM_OPTIONS_OK;
}

// Parse the command line options
static void
_fgcomParseArgs (int argc, char **argv)
{
  SG_LOG( SG_GENERAL, SG_DEBUG, "Processing commandline options" );

  for (int i = 1; i < argc; i++)
    {
      std::string arg = std::string (argv[i]);
      std::string next_arg;
      if (i < argc - 1)
	{
	  next_arg = std::string (argv[i + 1]);
	}

      if (arg.find ('-') == 0)
	{
	  if (arg == "--")
	    {
	      // do nothing
	    }
	  else
	    {
	      int
		result = _parseOption (arg, next_arg);

	      if (result == FGCOM_OPTIONS_OK)
		{
		  // that is great
		}
	      else if (result == FGCOM_OPTIONS_HELP)
		{
		  fgcomUsage ();
		  exit (0);
		}
	      else
		{
                  SG_LOG( SG_GENERAL, SG_ALERT, "Error parsing commandline options !" );
		  exit (1);
		}
	    }
	}
    }
  SG_LOG( SG_GENERAL, SG_ALERT, "Successfully parsed commandline options" );
}

// Parse config file options
static void
_fgcomParseOptions (const std::string & path)
{
    if (is_file_or_directory(path.c_str()) != 1) {
        SG_LOG( SG_GENERAL, SG_DEBUG, "Error: Unable to open " << path );
        return;
    }

    std::fstream in;
    std::ios_base::openmode mode = std::ios_base::in;
    in.open(path.c_str(),mode);
    if (!in.is_open ()) {
        SG_LOG( SG_GENERAL, SG_DEBUG, "Error: DEBUG: Unable to open " << path );
        return;
    }

    SG_LOG(SG_GENERAL, SG_DEBUG, "Processing config file: " << path );

    in >> skip_comment;
    while (!in.eof ()) {
        std::string line;
        getline (in, line, '\n');

        // catch extraneous (DOS) line ending character
        int i;
        for (i = line.length(); i > 0; i--) {
            if (line[i - 1] > 32) {
                break;
            }
        }

        line = line.substr(0, i);

        std::string next_arg;
        if (_parseOption (line, next_arg) == FGCOM_OPTIONS_ERROR) {
            SG_LOG( SG_GENERAL, SG_ALERT, "ERROR: Config file parse error: " << path << " '" << line << "'" );
            fgcomUsage ();
            exit(1);
        }

        in >> skip_comment;
    }
}

/* eof - fgcom_init.cpp */