/*
 * Copyright 2004-2019 Cray Inc.
 * Other additional copyright holders may be indicated within.
 * 
 * The entirety of this work is licensed under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License.
 * 
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "chplrt.h"

#include "arg.h"
#include "chplcgfns.h"
#include "chplexit.h"
#include "chplio.h"
#include "chpl-mem.h"
#include "chpl-linefile-support.h"
#include "config.h"
#include "error.h"

#include <stdio.h>
#include <string.h>


#define HASHSIZE 101

typedef struct _configVarType { /* table entry */
  char* varName;
  const char* moduleName;
  char* defaultValue;
  char* setValue;

  struct _configVarType* nextInBucket;
  struct _configVarType* nextInstalled;
} configVarType;


/* hash table */
static configVarType* configVarTable[HASHSIZE]; 
static configVarType* firstInTable = NULL;
static configVarType* lastInTable = NULL;

static configVarType _ambiguousConfigVar;
static configVarType* ambiguousConfigVar = &_ambiguousConfigVar;

static configVarType* lookupConfigVar(const char* moduleName, 
                                      const char* varName);


static void parseModVarName(char* modVarName, const char** moduleName, 
                            char** varName) {
  char* dot = strrchr(modVarName, '.');
  if (dot) {
    *dot = '\0';
    *moduleName = modVarName;
    *varName = dot + 1;
  } else {
    *moduleName = "";
    *varName = modVarName;
  }
}


/* This function parses a config var of type string, and sets its value in 
   the hash table.  
*/
static int aParsedString(FILE* argFile, char* setConfigBuffer, 
                         int32_t lineno, int32_t filename) {
  char* equalsSign = strchr(setConfigBuffer, '=');
  int stringLength = strlen(setConfigBuffer);
  char firstChar;
  char* value;
  char lastChar;
  const char* moduleName;
  char* varName;

  if (!equalsSign || !(equalsSign + 1)) {
    return 0;
  }

  firstChar = equalsSign[1];
  if ((firstChar != '"') && (firstChar != '\'')) {
    return 0;
  }

  value = equalsSign + 2;
  *equalsSign = '\0';
  lastChar = setConfigBuffer[stringLength - 1];

  parseModVarName(setConfigBuffer, &moduleName, &varName);
  
  if ((firstChar != lastChar) || (strlen(value) == 0)) {
    int nextChar = fgetc(argFile);
    do {
      switch (nextChar) {
      case EOF:
        {
          char* message;
          setConfigBuffer[stringLength] = '\0';
          message = chpl_glom_strings(2, "Found end of file while reading string: ",
                                      equalsSign + 1);
          chpl_error(message, lineno, filename);
          break;
        }
      case '\n':
        {
          char* message;
          setConfigBuffer[stringLength] = '\0';
          message = chpl_glom_strings(2, "Found newline while reading string: ", 
                                      equalsSign + 1);
          chpl_error(message, lineno, filename);
          break;
        }
      default:
        {
          if (stringLength >= _default_string_length - 1) {
            char dsl[1024];
            char* message;
            sprintf(dsl, "%d", _default_string_length);
            message = chpl_glom_strings(2, "String exceeds the maximum string length of ",
                                        dsl);
            chpl_error(message, lineno, filename);
          }
          setConfigBuffer[stringLength] = nextChar;
          stringLength++;
          nextChar = fgetc(argFile);
        }
      }
    } while (nextChar != firstChar);
  } else {
    stringLength--;
  }
  setConfigBuffer[stringLength] = '\0';
  initSetValue(varName, value, moduleName, lineno, filename);
  return 1;
}


void initConfigVarTable(void) {
  int i;
  for (i = 0; i < HASHSIZE; i++) {
    configVarTable[i] = NULL;
  }
}


/* hashing function */
static unsigned hash(const char* varName) {
  unsigned hashValue;
  for (hashValue = 0; *varName != '\0'; varName++) {
    hashValue = *varName + 31 * hashValue;
  }
  return hashValue % HASHSIZE;
}


void printConfigVarTable(void) {
  configVarType* configVar = NULL; 
  int longestName = 0;
  const char* moduleName = NULL;
  int foundUserConfigs = 0;
  int foundMultipleModules = 0;
  int thisName;

  for (configVar = firstInTable;
       configVar != NULL;
       configVar = configVar->nextInstalled) {

      if (foundUserConfigs == 0) {
        foundUserConfigs = 1;
        moduleName = configVar->moduleName;
      } else {
        if (strcmp(moduleName, configVar->moduleName) != 0) {
          foundMultipleModules = 1;
        }
      }
      thisName = strlen(configVar->varName);
      if (longestName < thisName)  {
        longestName = thisName;
      }
  }

  moduleName = NULL;
  if (foundUserConfigs) {
    fprintf(stdout, "CONFIG VARS:\n");
    fprintf(stdout, "============\n");

    for (configVar = firstInTable; 
         configVar != NULL; 
         configVar = configVar->nextInstalled) {

        if (foundMultipleModules) {
          if (moduleName == NULL) {
            moduleName = configVar->moduleName;
            fprintf(stdout, "%s config vars:\n", configVar->moduleName);
          }
          if (strcmp(configVar->moduleName, moduleName) != 0) {
            fprintf(stdout, "\n");
            fprintf(stdout, "%s config vars:\n", configVar->moduleName);
            moduleName = configVar->moduleName;
          }
        }
        fprintf(stdout, "  %*s: ", longestName, configVar->varName);
        fprintf(stdout, "%s", configVar->defaultValue);
        if (configVar->setValue) {
          fprintf(stdout, " (configured to %s)", configVar->setValue);
        }
        fprintf(stdout, "\n");
      }
  }

}


static configVarType* lookupConfigVar(const char* moduleName, 
                                      const char* varName) {
  configVarType* configVar = NULL;
  configVarType* foundConfigVar = NULL; 
  unsigned hashValue;
  int numTimesFound = 0;
  hashValue = hash(varName);

  /* This loops walks through the list of configuration variables 
     hashed to this location in the table. */
  for (configVar = configVarTable[hashValue]; 
       configVar != NULL; 
       configVar = configVar->nextInBucket) {

    if (strcmp(configVar->varName, varName) == 0) {
      if (strcmp(moduleName, "") == 0) {
        numTimesFound++;
        if (numTimesFound == 1) {
          foundConfigVar = configVar;
        } else {
          foundConfigVar = ambiguousConfigVar;
        }
      } else {
        if (strcmp(configVar->moduleName, moduleName) == 0) {
          foundConfigVar = configVar;
        }
      }
    }
  }
  return foundConfigVar;
}


void initSetValue(const char* varName, const char* value, 
                  const char* moduleName, 
                  int32_t lineno, int32_t filename) {
  configVarType* configVar;
  if  (*varName == '\0') {
    const char* message = "No variable name given";
    chpl_error(message, lineno, filename);
  }
  configVar = lookupConfigVar(moduleName, varName);
  if (configVar == NULL) {
    const char* configName = ((moduleName == NULL || !strcmp(moduleName, ""))
                              ? varName
                              : chpl_glom_strings(3,
                                                  moduleName, ".", varName));
    const char* message = chpl_glom_strings(3, "attempt to set config named '",
                                            configName,
                                            "', but there is no such config");
    chpl_error(message, lineno, filename);
  } else if (configVar == ambiguousConfigVar) {
    const char* message = chpl_glom_strings(5,
                                            "Multiple modules define a config "
                                            "named '", varName,
                                            "'.  Use '--help' for a list and "
                                            "disambiguate using '<moduleName>.",
                                            varName, "'.");
    chpl_error(message, lineno, filename);
  }
  if (strcmp(varName, "numLocales") == 0) {
    parseNumLocales(value, lineno, filename);
  }
  configVar->setValue = chpl_glom_strings(1, value);
}



const char* lookupSetValue(const char* varName, const char* moduleName) {
  configVarType* configVar;
  if (strcmp(moduleName, "") == 0) {
    const char* message = "Attempted to lookup value with the module name an "
      "empty string";
    chpl_internal_error(message);
  }

  configVar = lookupConfigVar(moduleName, varName);
  if (configVar) {
    return configVar->setValue;
  } else {
    return NULL;
  }
}


void installConfigVar(const char* varName, const char* value, 
                      const char* moduleName) {
  unsigned hashValue;
  configVarType* configVar = (configVarType*) 
    chpl_mem_allocMany(1, sizeof(configVarType), CHPL_RT_MD_CF_TABLE_DATA, 0, 0);

  hashValue = hash(varName);
  configVar->nextInBucket = configVarTable[hashValue]; 
  configVar->nextInstalled = NULL;
  configVarTable[hashValue] = configVar;
  if (firstInTable == NULL) {
    firstInTable = configVar;
  } else {
    lastInTable->nextInstalled = configVar;
  }
  lastInTable = configVar;
  configVar->varName = chpl_glom_strings(1, varName);
  configVar->moduleName = chpl_glom_strings(1, moduleName);
  configVar->defaultValue = chpl_glom_strings(1, value);
  configVar->setValue = NULL;
} 


static configVarType* breakIntoPiecesAndLookup(char* str, char** equalsSign, 
                                               const char** moduleName, 
                                               char** varName,
                                               int32_t lineno, 
                                               int32_t filename) {
  configVarType* configVar;

  *equalsSign = strchr(str, '=');
  if (*equalsSign) {
    **equalsSign = '\0';
  }
  parseModVarName(str, moduleName, varName);
  configVar = lookupConfigVar(*moduleName, *varName);
  if (configVar == ambiguousConfigVar) {
    const char* message = chpl_glom_strings(5, "Configuration variable '", 
                                            *varName, 
                                            "' is defined in more than one "
                                            "module.  Use '--help' for a list "
                                            "of configuration variables and "
                                            "'-s<module>.", 
                                            *varName, "' to disambiguate.");
    chpl_error(message, lineno, filename);
  }
  return configVar;
}


static void handleUnexpectedConfigVar(const char* moduleName, char* varName,
                                      int32_t lineno, int32_t filename) {
  const char* message;
  if (moduleName[0]) {
    message = chpl_glom_strings(5, "Module '", moduleName, 
                                "' has no configuration variable named '", 
                                varName, "'");
  } else if (varName[0]) {
    message = chpl_glom_strings(3, "Unrecognized configuration variable '",
                                varName, "'");
  } else {
    message = "No configuration variable name specified";
  }
  chpl_error(message, lineno, filename);
}


int handlePossibleConfigVar(int* argc, char* argv[], int argnum, 
                            int32_t lineno, int32_t filename) {
  int retval = 0;
  int arglen = strlen(argv[argnum]+2)+1;
  char* argCopy = chpl_mem_allocMany(arglen, sizeof(char),
                                     CHPL_RT_MD_CFG_ARG_COPY_DATA, argnum,
                                     CHPL_FILE_IDX_COMMAND_LINE);
  char* equalsSign;
  const char* moduleName;
  char* varName;
  configVarType* configVar;

  strcpy(argCopy, argv[argnum]+2);
  configVar = breakIntoPiecesAndLookup(argCopy, &equalsSign, &moduleName,
                                       &varName, lineno, filename);
  if (configVar == NULL) {
    if (argv[argnum][1] == '-') { // this is a -- argument
      retval = handleNonstandardArg(argc, argv, argnum, lineno, filename);
    } else {                      // this is a -s argument
      handleUnexpectedConfigVar(moduleName, varName, lineno, filename);
    }
  } else {
    char* value = equalsSign + 1;
    if (equalsSign && *value) {
      initSetValue(varName, value, moduleName, lineno, filename);
    } else if (!strcmp(configVar->defaultValue, "bool")) {
      initSetValue(varName, "true", moduleName, lineno, filename);
    } else {
      if (argnum + 1 >= *argc) {
        char* message = chpl_glom_strings(3, "Configuration variable '", varName, 
                                          "' is missing its initialization value");
        chpl_error(message, lineno, filename);
      } else {
        initSetValue(varName, argv[argnum+1], moduleName, lineno, filename);
        retval = 1;
      }
    }
  }

  chpl_mem_free(argCopy, argnum, CHPL_FILE_IDX_COMMAND_LINE);
  return retval;
}

// TODO: Change all the 0 linenos below into real line numbers
void parseConfigFile(const char* configFilename, 
                     int32_t lineno, int32_t filename) {
  FILE* argFile = fopen(configFilename, "r");
  if (!argFile) {
    char* message = chpl_glom_strings(2, "Unable to open ", configFilename);
    chpl_error(message, lineno, filename);
  }
  chpl_saveFilename(configFilename); // CHPL_FILE_IDX_SAVED_FILENAME will now
                                     // give us configFilename
  while (!feof(argFile)) {
    int numScans = 0;
    char setConfigBuffer[_default_string_length];
    numScans = fscanf(argFile, _default_format_read_string, setConfigBuffer);
    if (numScans == 1) {
     if (*setConfigBuffer == '#') {
        if (!feof(argFile)) {
          do {
            char ch = fgetc(argFile);
            if (ch == '\n')
              break;
          } while (!feof(argFile));
        }
      } else if (!aParsedString(argFile, setConfigBuffer, 0,
                                CHPL_FILE_IDX_SAVED_FILENAME)) {
        char* equalsSign;
        const char* moduleName;
        char* varName;
        configVarType *configVar =
            breakIntoPiecesAndLookup(setConfigBuffer, &equalsSign, &moduleName,
                                     &varName, 0, CHPL_FILE_IDX_SAVED_FILENAME);
        if (configVar == NULL) {
          handleUnexpectedConfigVar(moduleName, varName, 0,
                                    CHPL_FILE_IDX_SAVED_FILENAME);
        } else {
          char* value = equalsSign + 1;
          if (equalsSign && *value) {
            initSetValue(varName, value, moduleName, 0,
                         CHPL_FILE_IDX_SAVED_FILENAME);
          } else {
            char configValBuffer[_default_string_length];
            numScans =
                fscanf(argFile, _default_format_read_string, configValBuffer);
            if (numScans != 1) {
              char *message =
                  chpl_glom_strings(3, "Configuration variable '", varName,
                                    "' is missing its initialization value");
              chpl_error(message, 0, CHPL_FILE_IDX_SAVED_FILENAME);
            } else {
              initSetValue(varName, configValBuffer, moduleName, 0,
                           CHPL_FILE_IDX_SAVED_FILENAME);
            }
          }
        }
      }
    }
  }
  fclose(argFile);
}


chpl_bool chpl_config_has_value(c_string v, c_string m) { 
  return lookupSetValue(v, m) != NULL;
}


// Returning a c_string here is thread-safe because config consts
// are constant for the duration of the program.
c_string chpl_config_get_value(c_string v, c_string m) { 
  return lookupSetValue(v, m);
}


