#include <fstream>
#include <sstream>
#include <iostream>

#include "config.h"

#include "../log/log.h"

#include "../util_env.h"

namespace dxvk {

  const static std::unordered_map<std::string, Config> g_appDefaults = {{
    /* Assassin's Creed Syndicate - amdags issues */
    { "ACS.exe", {{
      { "dxgi.customVendorId",              "10de" },
    }} },
    /* Dishonored 2                               */
    { "Dishonored2.exe", {{
      { "d3d11.allowMapFlagNoWait",         "True" }
    }} },
    /* Elite Dangerous                            */
    { "EliteDangerous64.exe", {{
      { "dxgi.customVendorId",              "10de" },
    }} },
    /* The Vanishing of Ethan Carter Redux        */
    { "EthanCarter-Win64-Shipping.exe", {{
      { "dxgi.customVendorId",              "10de" },
    }} },
    /* The Evil Within                            */
    { "EvilWithin.exe", {{
      { "d3d11.dcSingleUseMode",            "False" },
    }} },
    /* The Evil Within Demo                       */
    { "EvilWithinDemo.exe", {{
      { "d3d11.dcSingleUseMode",            "False" },
    }} },
    /* F1 2015                                    */
    { "F1_2015.exe", {{
      { "d3d11.fakeStreamOutSupport",       "True" },
    }} },
    /* Far Cry 5                                  */
    { "FarCry5.exe", {{
      { "d3d11.allowMapFlagNoWait",         "True" }
    }} },
    /* Final Fantasy XV                           */
    { "ffxv_s.exe", {{
      { "d3d11.fakeStreamOutSupport",       "True" },
    }} },
    /* Frostpunk                                  */
    { "Frostpunk.exe", {{
      { "dxgi.deferSurfaceCreation",        "True" },
    }} },
    /* Mafia 3                                    */
    { "mafia3.exe", {{
      { "d3d11.fakeStreamOutSupport",       "True" },
    }} },
    /* Overwatch                                  */
    { "Overwatch.exe", {{
      { "d3d11.fakeStreamOutSupport",       "True" },
    }} },
    /* Quantum Break                              */
    { "QuantumBreak.exe", {{
      { "d3d11.zeroInitWorkgroupMemory",    "True" },
    }} },
    /* Anno 2205                                  */
    { "anno2205.exe", {{
      { "dxvk.enableStateCache",            "False" },
    }} },
  }};


  static bool isWhitespace(char ch) {
    return ch == ' ' || ch == '\x9' || ch == '\r';
  }

  
  static bool isValidKeyChar(char ch) {
    return (ch >= '0' && ch <= '9')
        || (ch >= 'A' && ch <= 'Z')
        || (ch >= 'a' && ch <= 'z')
        || (ch == '.' || ch == '_');
  }


  static size_t skipWhitespace(const std::string& line, size_t n) {
    while (n < line.size() && isWhitespace(line[n]))
      n += 1;
    return n;
  }


  static void parseUserConfigLine(Config& config, const std::string& line) {
    std::stringstream key;
    std::stringstream value;

    // Extract the key
    size_t n = skipWhitespace(line, 0);
    while (n < line.size() && isValidKeyChar(line[n]))
      key << line[n++];
    
    // Check whether the next char is a '='
    n = skipWhitespace(line, n);
    if (n >= line.size() || line[n] != '=')
      return;

    // Extract the value
    n = skipWhitespace(line, n + 1);
    while (n < line.size() && !isWhitespace(line[n]))
      value << line[n++];
    
    config.setOption(key.str(), value.str());
  }


  Config::Config() { }
  Config::~Config() { }


  Config::Config(OptionMap&& options)
  : m_options(std::move(options)) { }


  void Config::merge(const Config& other) {
    for (auto& pair : other.m_options)
      m_options.insert(pair);
  }


  void Config::setOption(const std::string& key, const std::string& value) {
    m_options.insert_or_assign(key, value);
  }


  std::string Config::getOptionValue(const char* option) const {
    auto iter = m_options.find(option);

    return iter != m_options.end()
      ? iter->second : std::string();
  }


  bool Config::parseOptionValue(
    const std::string&  value,
          std::string&  result) {
    result = value;
    return true;
  }


  bool Config::parseOptionValue(
    const std::string&  value,
          bool&         result) {
    if (value == "True") {
      result = true;
      return true;
    } else if (value == "False") {
      result = false;
      return true;
    } else {
      return false;
    }
  }


  bool Config::parseOptionValue(
    const std::string&  value,
          int32_t&      result) {
    if (value.size() == 0)
      return false;
    
    // Parse sign, don't allow '+'
    int32_t sign = 1;
    size_t start = 0;

    if (value[0] == '-') {
      sign = -1;
      start = 1;
    }

    // Parse absolute number
    int32_t intval = 0;

    for (size_t i = start; i < value.size(); i++) {
      if (value[i] < '0' || value[i] > '9')
        return false;
      
      intval *= 10;
      intval += value[i] - '0';
    }

    // Apply sign and return
    result = sign * intval;
    return true;
  }


  Config Config::getAppConfig(const std::string& appName) {
    auto appConfig = g_appDefaults.find(appName);
    if (appConfig != g_appDefaults.end()) {
      // Inform the user that we loaded a default config
      Logger::info(str::format("Found built-in config: ", appName));

      return appConfig->second;
    }

    return Config();
  }


  Config Config::getUserConfig() {
    Config config;

    // Load either $DXVK_CONFIG_FILE or $PWD/dxvk.conf
    std::string filePath = env::getEnvVar("DXVK_CONFIG_FILE");

    if (filePath == "")
      filePath = "dxvk.conf";
    
    // Open the file if it exists
    std::ifstream stream(filePath);

    if (!stream)
      return config;
    
    // Inform the user that we loaded a file, might
    // help when debugging configuration issues
    Logger::info(str::format("Found config file: ", filePath));

    // Parse the file line by line
    std::string line;

    while (std::getline(stream, line))
      parseUserConfigLine(config, line);
    
    return config;
  }


  void Config::logOptions() const {
    if (!m_options.empty()) {
      Logger::info("Effective configuration:");

      for (auto& pair : m_options)
        Logger::info(str::format("  ", pair.first, " = ", pair.second));
    }
  }

}
