#include <stdexcept>
#include <sstream>
#include <stdarg.h>

#include "UserInterface.h"
#include "StkStream.h"
#include "Features.h"

namespace TNet
{
  //***************************************************************************
  //***************************************************************************
  int 
  npercents(const char *str)
  {
    int ret = 0;
    while (*str) if (*str++ == '%') ret++;
    return ret;
  }
  

  //***************************************************************************
  //***************************************************************************
  void 
  UserInterface::
  ReadConfig(const char *file_name)
  {
    std::string   line_buf;
    std::string::iterator chptr;
    std::string   key;
    std::string   value;
    std::ostringstream ss;
    int           line_no = 0;
    IStkStream    i_stream;


    i_stream.open(file_name, std::ios::binary);
    if (!i_stream.good()) {
      throw std::runtime_error(std::string("Cannot open input config file ")
          + file_name);
    }
    i_stream >> std::ws;

    while (!i_stream.eof()) {
      size_t i_pos;

      // read line
      std::getline(i_stream, line_buf);
      i_stream >> std::ws;

      if (i_stream.fail()) {
        throw std::runtime_error(std::string("Error reading (") 
            + file_name + ":" + (ss << line_no,ss).str() + ")");
      }

      // increase line counter
      line_no++;

      // cut comments
      if (std::string::npos != (i_pos = line_buf.find('#'))) {
        line_buf.erase(i_pos);
      }

      // cut leading and trailing spaces
      Trim(line_buf);

      // if empty line, then skip it
      if (0 == line_buf.length()) {
        continue;
      }
  
      // line = line_buf.c_str();
      // chptr = parptr;

      chptr = line_buf.begin();

      for (;;) {
        // Replace speces by '_', which is removed in InsertConfigParam
        while (isalnum(*chptr) || *chptr == '_' || *chptr == '-') {
          chptr++;
        }

        while (std::isspace(*chptr)) {
          *chptr = '_';
          chptr++;
        }

        if (*chptr != ':') {
          break;
        }

        chptr++;

        while (std::isspace(*chptr)) {
          *chptr = '_';
          chptr++;
        }
      }
      
      if (*chptr != '=') {
        throw std::runtime_error(std::string("Character '=' expected (") 
            + file_name + ":" + (ss.str(""),ss<<line_no,ss).str() + ")");
      }

      key.assign(line_buf.begin(), chptr);

      chptr++;

      value.assign(chptr, line_buf.end());

      ParseHTKString(value, value);
      InsertConfigParam(key.c_str(), value.c_str(), 'C');
    }
  
    i_stream.close();
  }
  

  //***************************************************************************
  //***************************************************************************
  void 
  UserInterface::
  InsertConfigParam(const char *pParamName, const char *value, int optionChar) 
  {
    std::string key(pParamName);
    std::string::iterator i_key = key.begin();

    while (i_key != key.end()) {
      if (*i_key == '-' || *i_key == '_') {
        i_key = key.erase(i_key);
      }
      else {
        *i_key = toupper(*i_key);
        i_key ++;
      }
    }
  
    mMap[key].mValue  = value;
    mMap[key].mRead   = false;
    mMap[key].mOption = optionChar;
  }
  
  //***************************************************************************
  //***************************************************************************
  int 
  UserInterface::
  ParseOptions(
    int             argc,
    char*           argv[],
    const char*     pOptionMapping,
    const char*     pToolName)
  {
    int          i;
    int          opt = '?';
    int          optind;
    bool         option_must_follow = false;
    char         param[1024];
    char*        value;
    const char*  optfmt;
    const char*  optarg;
    char*        chptr;
    char*        bptr;
    char         tstr[4] = " -?";
    unsigned long long option_mask = 0;
    std::ostringstream ss;
  
    #define MARK_OPTION(ch) {if (isalpha(ch)) option_mask |= 1ULL << ((ch) - 'A');}
    #define OPTION_MARK(ch) (isalpha(ch) && ((1ULL << ((ch) - 'A')) & option_mask))
    #define IS_OPTION(str) ((str)[0] == '-' && (isalpha((str)[1]) || (str)[1] == '-'))
 
    //search for the -A param
    for (optind = 1; optind < argc; optind++) {
      // we found "--", no -A
      if (!strcmp(argv[optind], "--")) {
        break;
      }

      //repeat till we find -A
      if (argv[optind][0] != '-' || argv[optind][1] != 'A') {
        continue;
      }

      // just "-A" form
      if (argv[optind][2] != '\0') {
        throw std::runtime_error(std::string("Unexpected argument '") 
            + (argv[optind] + 2) + "' after option '-A'");
      }

      for (i=0; i < argc; i++) {
        // display all params
        if(strchr(argv[i], ' ') || strchr(argv[i], '*')) 
          std::cout << '\'' << argv[i] << '\'' << " ";
        else std::cout << argv[i] << " ";
      }

      std::cout << std::endl;

      break;
    }

    for (optind = 1; optind < argc; optind++) {
      // find the '-C?' parameter (possible two configs)
      if (!strcmp(argv[optind], "--")) break;
      if (argv[optind][0] != '-' || argv[optind][1] != 'C') continue;
      if (argv[optind][2] != '\0') {
        ReadConfig(argv[optind] + 2);
      } else if (optind+1 < argc && !IS_OPTION(argv[optind+1])) {
        ReadConfig(argv[++optind]);
      } else {
        throw std::runtime_error("Config file name expected after option '-C'");
      }
    }

    for (optind = 1; optind < argc; optind++) {
      if (!strcmp(argv[optind], "--")) break;
      if (argv[optind][0] != '-' || argv[optind][1] != '-') continue;

      bptr = new char[strlen(pToolName) + strlen(argv[optind]+2) + 2];
      strcat(strcat(strcpy(bptr, pToolName), ":"), argv[optind]+2);
      value = strchr(bptr, '=');
      if (!value) {
        throw std::runtime_error(std::string("Character '=' expected after option '")
            + argv[optind] + "'");
      }

      *value++ = '\0';
      
      InsertConfigParam(bptr, value /*? value : "TRUE"*/, '-');
      delete [] bptr;
    }

    for (optind = 1; optind < argc && IS_OPTION(argv[optind]); optind++) {
      option_must_follow = false;
      tstr[2] = opt = argv[optind][1];
      optarg = argv[optind][2] != '\0' ? argv[optind] + 2 : NULL;
  
      if (opt == '-' && !optarg) {    // '--' terminates the option list
        return optind+1;
      }
      if (opt == 'C' || opt == '-') { // C, A and long options have been already processed
        if (!optarg) optind++;
        continue;
      }
      if (opt == 'A') continue;
  
      chptr = strstr((char*)pOptionMapping, tstr);
      if (chptr == NULL) {
        throw std::runtime_error(std::string("Invalid command line option '-") 
            + static_cast<char>(opt) + "'");
      }
  
      chptr += 3;
      while (std::isspace(*chptr)) {
        chptr++;
      }
  
      if (!chptr || chptr[0] == '-') {// Option without format string will be ignored
        optfmt = " ";
      } else {
        optfmt = chptr;
        while (*chptr && !std::isspace(*chptr)) {
          chptr++;
        }
        if (!*chptr) {
          throw std::runtime_error("Fatal: Unexpected end of optionMap string");
        }
      }
      for (i = 0; !std::isspace(*optfmt); optfmt++) {
        while (std::isspace(*chptr)) chptr++;
        value = chptr;
        while (*chptr && !std::isspace(*chptr)) chptr++;
        assert(static_cast<unsigned int>(chptr-value+1) < sizeof(param));
        strncat(strcat(strcpy(param, pToolName), ":"), value, chptr-value);
        param[chptr-value+strlen(pToolName)+1] = '\0';
        switch (*optfmt) {
          case 'n': 
            value = strchr(param, '=');
            if (value) *value = '\0';
            InsertConfigParam(param,
                              value ? value + 1: "TRUE", opt);
            break;

          case 'l':
          case 'o':
          case 'r': 
            i++;
            if (!optarg && (optind+1==argc || IS_OPTION(argv[optind+1]))) {
              if (*optfmt == 'r' || *optfmt == 'l') {
                throw std::runtime_error(std::string("Argument ") 
                    + (ss<<i,ss).str() + " of option '-" 
                    + static_cast<char>(opt) + "' expected");
              }
              optfmt = "  "; // Stop reading option arguments
              break;
            }
            if (!optarg) optarg = argv[++optind];
            if (*optfmt == 'o') {
              option_must_follow = (bool) 1;
            }
            bptr = NULL;

            // For repeated use of option with 'l' (list) format, append
            // ',' and argument string to existing config parameter value.
            if (*optfmt == 'l' && OPTION_MARK(opt)) {
              bptr = strdup(GetStr(param, ""));
              if (bptr == NULL) throw std::runtime_error("Insufficient memory");
              bptr = (char*) realloc(bptr, strlen(bptr) + strlen(optarg) + 2);
              if (bptr == NULL) throw std::runtime_error("Insufficient memory");
              strcat(strcat(bptr, ","), optarg);
              optarg = bptr;
            }
            MARK_OPTION(opt);
            InsertConfigParam(param, optarg, opt);
            free(bptr);
            optarg = NULL;
            break;

          default : 
            throw std::runtime_error(std::string("Fatal: Invalid character '")
                + *optfmt + "' in optionMap after " + tstr);
        }
      }
      if (optarg) {
        throw std::runtime_error(std::string("Unexpected argument '")
            + optarg + "' after option '-" 
            + static_cast<char>(opt) + "'");
      }
    }
  
    for (i = optind; i < argc && !IS_OPTION(argv[i]); i++)
    {}
   
    if (i < argc) {
      throw std::runtime_error(std::string("No option expected after first non-option argument '")
          + argv[optind] + "'");
    }

    if (option_must_follow) {
      throw std::runtime_error(std::string("Option '-")
          + static_cast<char>(opt) 
          + "' with optional argument must not be the last option");
    }

    return optind;
  }
  

  //***************************************************************************
  //***************************************************************************
  int 
  UserInterface::
  GetFeatureParams(
    int *           derivOrder,
    int **          derivWinLens,
    int *           startFrmExt,
    int *           endFrmExt,
    char **         CMNPath,
    char **         CMNFile,
    const char **   CMNMask,
    char **         CVNPath,
    char **         CVNFile,
    const char **   CVNMask,
    const char **   CVGFile,
    const char *    pToolName,
    int             pseudoModeule)
  {
    const char *  str;
    int           targetKind;
    char *        chrptr;
    char          paramName[32];
    const char *  CMNDir;
    const char *  CVNDir;
    
    strcpy(paramName, pToolName);
    strcat(paramName, pseudoModeule == 1 ? "SPARM1:" :
                      pseudoModeule == 2 ? "SPARM2:" : "");
                      
    chrptr = paramName + strlen(paramName);
  
    strcpy(chrptr, "STARTFRMEXT");
    *startFrmExt = GetInt(paramName, 0);
    strcpy(chrptr, "ENDFRMEXT");
    *endFrmExt   = GetInt(paramName, 0);
  
    *CMNPath = *CVNPath = NULL;
    strcpy(chrptr, "CMEANDIR");
    CMNDir       = GetStr(paramName, NULL);
    strcpy(chrptr, "CMEANMASK");
    *CMNMask     = GetStr(paramName, NULL);

    if (*CMNMask != NULL) {
      *CMNPath = (char*) malloc((CMNDir ? strlen(CMNDir) : 0) + npercents(*CMNMask) + 2);
      if (*CMNPath == NULL) throw std::runtime_error("Insufficient memory");
      if (CMNDir != NULL) strcat(strcpy(*CMNPath, CMNDir), "/");
      *CMNFile = *CMNPath + strlen(*CMNPath);
    }
    strcpy(chrptr, "VARSCALEDIR");
    CVNDir      = GetStr(paramName, NULL);
    strcpy(chrptr, "VARSCALEMASK");
    *CVNMask     = GetStr(paramName, NULL);


    if (*CVNMask != NULL) {
      *CVNPath = (char*) malloc((CVNDir ? strlen(CVNDir) : 0) + npercents(*CVNMask) + 2);
      if (*CVNPath == NULL) throw std::runtime_error("Insufficient memory");
      if (CVNDir != NULL) strcat(strcpy(*CVNPath, CVNDir), "/");
      *CVNFile = *CVNPath + strlen(*CVNPath);
    }
    strcpy(chrptr, "VARSCALEFN");
    *CVGFile     = GetStr(paramName, NULL);
    strcpy(chrptr, "TARGETKIND");
    str = GetStr(paramName, "ANON");

    targetKind = FeatureRepository::ReadParmKind(str, false);

    if (targetKind == -1) {
      throw std::runtime_error(std::string("Invalid TARGETKIND = '")
          + str + "'");
    }
  
    strcpy(chrptr, "DERIVWINDOWS");
    if ((str = GetStr(paramName, NULL)) != NULL) {
      long lval;
      *derivOrder      = 0;
      *derivWinLens = NULL;
      
      if (NULL != str)
      {
        while ((str = strtok((char *) str, " \t_")) != NULL) 
        {
          lval = strtol(str, &chrptr, 0);
          if (!*str || *chrptr) {
            throw std::runtime_error("Integers separated by '_' expected for parameter DERIVWINDOWS");
          }
          *derivWinLens = (int *)realloc(*derivWinLens, ++*derivOrder*sizeof(int));
          if (*derivWinLens == NULL) throw std::runtime_error("Insufficient memory");
          (*derivWinLens)[*derivOrder-1] = lval;
          str = NULL;
        }
      }
      
      return targetKind;
    }
    *derivOrder = targetKind & PARAMKIND_T ? 3 :
                  targetKind & PARAMKIND_A ? 2 :
                  targetKind & PARAMKIND_D ? 1 : 0;
  
    if (*derivOrder || targetKind != PARAMKIND_ANON) {
    *derivWinLens = (int *) malloc(3 * sizeof(int));
      if (*derivWinLens == NULL) throw std::runtime_error("Insufficient memory");
  
      strcpy(chrptr, "DELTAWINDOW");
      (*derivWinLens)[0] = GetInt(paramName, 2);
      strcpy(chrptr, "ACCWINDOW");
      (*derivWinLens)[1] = GetInt(paramName, 2);
      strcpy(chrptr, "THIRDWINDOW");
      (*derivWinLens)[2] = GetInt(paramName, 2);
      return targetKind;
    }
    *derivWinLens = NULL;
    *derivOrder   = -1;
    return targetKind;
  }
  

  //***************************************************************************
  //***************************************************************************
  UserInterface::ValueRecord*
  UserInterface::
  GetParam(const char* pParamName)
  {
    MapType::iterator it;

    // this is done only for convenience. in the loop we will increase the 
    // pointer again
    pParamName--;

    // we iteratively try to find the param name in the map. if an attempt 
    // fails, we strip off all characters until the first ':' and we search 
    // again
    do {
      pParamName++;
      it = mMap.find(pParamName);
    } while ((it == mMap.end()) && (NULL != (pParamName = strchr(pParamName, ':'))));

    if (it == mMap.end()) {
      return NULL;
    }
    else {
      it->second.mRead = true;
      return &(it->second);
    }
  }
  

  //***************************************************************************
  //***************************************************************************
  const char * 
  UserInterface::
  GetStr(
    const char *    pParamName,
    const char *    default_value)
  {
    ValueRecord* p_val = GetParam(pParamName);

    if (NULL == p_val) {
      return default_value;
    }
    else {
      return p_val->mValue.c_str();
    }
  }
  

  //***************************************************************************
  //***************************************************************************
  long 
  UserInterface::
  GetInt(
    const char *pParamName,
    long default_value)
  {
    char *chrptr;
    ValueRecord* p_val = GetParam(pParamName);

    if (NULL == p_val) {
      return default_value;
    }
  
    const char *val = p_val->mValue.c_str();
    default_value = strtol(val, &chrptr, 0);
    if (!*val || *chrptr) {
      throw std::runtime_error(std::string("Integer number expected for ") 
          + pParamName + " but found '" + val + "'");
    }
    return default_value;
  }
  
  //***************************************************************************
  //***************************************************************************
  float 
  UserInterface::
  GetFlt(
    const char *      pParamName,
    float             default_value)
  {
    char *chrptr;
    ValueRecord* p_val = GetParam(pParamName);

    if (NULL == p_val) {
      return default_value;
    }
  
    const char *val = p_val->mValue.c_str();
    default_value = strtod(val, &chrptr);
    if (!*val || *chrptr) {
      throw std::runtime_error(std::string("Decimal number expected for ") 
          + pParamName + " but found '" + val + "'");
    }
    return default_value;
  }
  
  //***************************************************************************
  //***************************************************************************
  bool 
  UserInterface::
  GetBool(
    const char *    pParamName,
    bool            default_value)
  {
    ValueRecord* p_val = GetParam(pParamName);

    if (NULL == p_val) {
      return default_value;
    }
  
    const char* val = p_val->mValue.c_str();

    if (!strcasecmp(val, "TRUE") || !strcmp(val, "T")) return 1;
    if (strcasecmp(val, "FALSE") && strcmp(val, "F")) {
      throw std::runtime_error(std::string("TRUE or FALSE expected for ")
          + pParamName + " but found '" + val + "'");
    }
    return false;
  }
  
  //***************************************************************************
  //***************************************************************************
  // '...' are pairs: string and corresponding integer value , terminated by NULL
  int 
  UserInterface::
  GetEnum(
    const char *    pParamName,
    int             default_value, 
    ...)  
  {
    ValueRecord* p_val = GetParam(pParamName);

    if (NULL == p_val) {
      return default_value;
    }

    const char* val = p_val->mValue.c_str();
    char*       s;
    int i = 0, cnt = 0, l = 0;
    va_list ap;
  
    va_start(ap, default_value);
    while ((s = va_arg(ap, char *)) != NULL) {
      l += strlen(s) + 2;
      ++cnt;
      i = va_arg(ap, int);
      if (!strcmp(val, s)) break;
    }
    va_end(ap);

    if (s) {
      return i;
    }
  
    //To report error, create string listing all possible values
    s = (char*) malloc(l + 1);
    s[0] = '\0';
    va_start(ap, default_value);
    for (i = 0; i < cnt; i++) {
      strcat(s, va_arg(ap, char *));
      va_arg(ap, int);
      if (i < cnt - 2) strcat(s, ", ");
      else if (i == cnt - 2) strcat(s, " or ");
    }

    va_end(ap);

    throw std::runtime_error(std::string(s) + " expected for "
        + pParamName + " but found '" + val + "'");

    return 0;
  }
  

  //***************************************************************************
  //***************************************************************************
  void
  UserInterface::
  PrintConfig(std::ostream& rStream)
  {
    rStream << "Configuration Parameters[" << mMap.size() << "]\n";
    for (MapType::iterator it = mMap.begin(); it != mMap.end(); ++it) {
      rStream << (it->second.mRead ? "  " : "# ") 
        << std::setw(35) << std::left << it->first << " = "
        << std::setw(30) << std::left << it->second.mValue 
        << " # -" << it->second.mOption << std::endl;
    }
  }
  
  //***************************************************************************
  //***************************************************************************
  void 
  UserInterface::
  CheckCommandLineParamUse()
  {
    for (MapType::iterator it = mMap.begin(); it != mMap.end(); ++it) {
      if (!it->second.mRead && it->second.mOption != 'C') {
        Error("Unexpected command line parameter " + it->first);
      }
    }
  }

}