#include "tenv.h"
#include "tsystem.h"
#include "tconvert.h"
#include "tfilepath_io.h"
#include "tversion.h"

#include <QDir>
#include <QSettings>
#include <QStandardPaths>

#ifdef LEVO_MACOSX

#include "macofflinegl.h"
#include "tofflinegl.h"

// Imposto l'offlineGL usando AGL (per togliere la dipendenza da X)

TOfflineGL::Imp *MacOfflineGenerator1(const TDimension &dim) {
  return new MacImplementation(dim);
}
#endif

#include <map>
#include <sstream>

using namespace TEnv;
using namespace TVER;

//=========================================================
//
// root dir
//
//=========================================================

namespace {
const std::map<std::string, std::string> systemPathMap{
    {"LIBRARY", "library"}, {"STUDIOPALETTE", "studiopalette"},
    {"FXPRESETS", "fxs"},   {"PROFILES", "profiles"},
    {"CONFIG", "config"},   {"PROJECTS", "projects"}};

class EnvGlobals {  // singleton

  ToonzVersion m_version;
  std::string m_applicationFileName;  // May differ from application name
  std::string m_applicationVersion;
  std::string m_applicationFullName;
  std::string m_moduleName;
  std::string m_rootVarName;
  std::string m_systemVarPrefix;
  std::string m_workingDirectory;
  TFilePath m_registryRoot;
  TFilePath m_envFile;
  TFilePath *m_stuffDir;
  TFilePath *m_dllRelativeDir;
  bool m_isPortable = false;

  // path values specified with command line arguments
  std::map<std::string, std::string> m_argPathValues;

  EnvGlobals() : m_stuffDir(0) {
    setWorkingDirectory();
    init();
  }

public:
  ~EnvGlobals() { delete m_stuffDir; }

  static EnvGlobals *instance() {
    static EnvGlobals _instance;
    return &_instance;
  }

  TFilePath getSystemVarPath(std::string varName) {
#ifdef _WIN32
    return m_registryRoot + varName;
#else
    QString settingsPath;

#ifdef MACOSX
    settingsPath = QString::fromStdString(getApplicationFileName()) +
                   QString(".app") +
                   QString("/Contents/Resources/SystemVar.ini");
#else
#ifdef HAIKU
    settingsPath =
        QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) +
        "/SystemVar.ini";
#else /* Generic Unix */
    // TODO: use QStandardPaths::ConfigLocation when we drop Qt4
    settingsPath = QDir::homePath();
    settingsPath.append("/.config/");
    settingsPath.append(getApplicationName().c_str());
    settingsPath.append("/SystemVar.ini");
#endif
#endif

    QSettings settings(settingsPath, QSettings::IniFormat);
    QString qStr      = QString::fromStdString(varName);
    QString systemVar = settings.value(qStr).toString();
    // printf("getSystemVarPath: path:%s key:%s var:%s\n",
    // settingsPath.toStdString().data(), varName.data(),
    // systemVar.toStdString().data());
    return TFilePath(systemVar.toStdWString());
#endif
  }

  TFilePath getRootVarPath() { return getSystemVarPath(m_rootVarName); }

  std::string getSystemVarValue(std::string varName) {
    if (getIsPortable()) return "";
#ifdef _WIN32
    return TSystem::getSystemValue(getSystemVarPath(varName)).toStdString();
#else
    TFilePath systemVarPath = getSystemVarPath(varName);
    if (systemVarPath.isEmpty()) {
      std::cout << "varName:" << varName << " TOONZROOT not set..."
                << std::endl;
      return "";
    }
    return ::to_string(systemVarPath);
/*
                        char *value = getenv(varName.c_str());
                        if (!value)
                                {
                                std::cout << varName << " not set, returning
   TOONZROOT" << std::endl;
        //value = getenv("TOONZROOT");
                        value="";
                        std::cout << "!!!value= "<< value << std::endl;
                        if (!value)
                                        {
                                        std::cout << varName << "TOONZROOT not
   set..." << std::endl;
                                        //exit(-1);
                                        return "";
                                        }
                                }
      return string(value);
        */
#endif
  }

  TFilePath getSystemVarPathValue(std::string varName) {
    // return if the path is registered by command line argument
    std::string argVar = getArgPathValue(varName);
    if (argVar != "") return TFilePath(argVar);
    return TFilePath(getSystemVarValue(varName));
  }

  TFilePath getStuffDir() {
    if (m_stuffDir) return *m_stuffDir;
    if (m_isPortable)
      return TFilePath((getWorkingDirectory() + "\\portablestuff\\"));

    return TFilePath(getSystemVarValue(m_rootVarName));
  }
  void setStuffDir(const TFilePath &stuffDir) {
    delete m_stuffDir;
    m_stuffDir = new TFilePath(stuffDir);
  }

  void updateEnvFile() {
    TFilePath profilesDir =
        getSystemVarPathValue(getSystemVarPrefix() + "PROFILES");
    if (profilesDir == TFilePath())
      profilesDir = getStuffDir() + systemPathMap.at("PROFILES");
    m_envFile =
        profilesDir + "env" + (TSystem::getUserName().toStdString() + ".env");
  }

  void init() {
    if (m_version.getAppRevision() != 0) {
      m_applicationVersion = m_version.getAppVersionString() + "." +
                             m_version.getAppRevisionString();
    } else {
      m_applicationVersion = m_version.getAppVersionString();
    }

    m_applicationFullName = m_version.getAppName() + " " + m_applicationVersion;
    if (m_version.hasAppNote())
      m_applicationFullName += " " + m_version.getAppNote();

    m_moduleName  = m_version.getAppName();
    m_rootVarName = m_version.getSystemVarPrefix() + "ROOT";
#ifdef _WIN32
    // from v1.3, registry root is moved to SOFTWARE\\OpenToonz\\OpenToonz
    m_registryRoot =
        TFilePath("SOFTWARE\\OpenToonz\\") + m_version.getAppName();
#endif
    m_systemVarPrefix = m_version.getSystemVarPrefix();
    updateEnvFile();
  }

  void setApplicationFileName(std::string appFileName) {
    m_applicationFileName = appFileName;
    setWorkingDirectory();
  }
  std::string getApplicationFileName() { return m_applicationFileName; }
  std::string getApplicationName() { return m_version.getAppName(); }
  std::string getApplicationVersion() { return m_applicationVersion; }
  std::string getApplicationVersionWithoutRevision() {
    return m_version.getAppVersionString();
  }

  TFilePath getEnvFile() { return m_envFile; }
  TFilePath getTemplateEnvFile() {
    return m_envFile.getParentDir() + TFilePath("template.env");
  }

  void setApplicationFullName(std::string applicationFullName) {
    m_applicationFullName = applicationFullName;
  }
  std::string getApplicationFullName() { return m_applicationFullName; }

  void setModuleName(std::string moduleName) { m_moduleName = moduleName; }
  std::string getModuleName() { return m_moduleName; }

  void setRootVarName(std::string varName) {
    if (m_rootVarName == varName) return;
    m_rootVarName = varName;
    updateEnvFile();
  }
  std::string getRootVarName() { return m_rootVarName; }

  void setSystemVarPrefix(std::string prefix) {
    if (m_systemVarPrefix == prefix) return;
    m_systemVarPrefix = prefix;
    updateEnvFile();
  }
  std::string getSystemVarPrefix() { return m_systemVarPrefix; }

  void setWorkingDirectory() {
    QString workingDirectoryTmp  = QDir::currentPath();
    QByteArray ba                = workingDirectoryTmp.toLatin1();
    const char *workingDirectory = ba.data();
    m_workingDirectory           = workingDirectory;

    // check if portable
    TFilePath portableCheck =
        TFilePath(m_workingDirectory + "\\portablestuff\\");
    TFileStatus portableStatus(portableCheck);
    m_isPortable = portableStatus.doesExist();

#ifdef MACOSX
    // macOS 10.12 (Sierra) translocates applications before running them
    // depending on how it was installed. This separates the app from the
    // portablestuff folder and we don't know where it is so we stop treating it
    // as a portable. Placing portablestuff inside OpenToonz.app will keep
    // everything together when it translocates.
    if (!m_isPortable) {
      portableCheck =
          TFilePath(m_workingDirectory + "\\" + getApplicationFileName() +
                    ".app\\portablestuff\\");
      portableStatus = TFileStatus(portableCheck);
      m_isPortable   = portableStatus.doesExist();
      if (m_isPortable)
        m_workingDirectory =
            portableCheck.getParentDir().getQString().toStdString();
    }
#endif
  }
  std::string getWorkingDirectory() { return m_workingDirectory; }

  bool getIsPortable() { return m_isPortable; }

  void setDllRelativeDir(const TFilePath &dllRelativeDir) {
    delete m_dllRelativeDir;
    m_dllRelativeDir = new TFilePath(dllRelativeDir);
  }

  TFilePath getDllRelativeDir() {
    if (m_dllRelativeDir) return *m_dllRelativeDir;
    return TFilePath(".");
  }

  void setArgPathValue(std::string key, std::string value) {
    m_argPathValues.insert_or_assign(key, value);
    if (key == m_systemVarPrefix + "PROFILES") updateEnvFile();
  }

  std::string getArgPathValue(std::string key) {
    decltype(m_argPathValues)::iterator it = m_argPathValues.find(key);
    if (it != m_argPathValues.end())
      return it->second;
    else
      return "";
  }
};

/*
TFilePath EnvGlobals::getSystemPath(int id)
{
  std::map<int, TFilePath>::iterator it = m_systemPaths.find(id);
  if(it != m_systemPaths.end()) return it->second;
  switch(id)
    {
     case StuffDir:        return TFilePath();
     case ConfigDir:       return getSystemPath(StuffDir) + "config";
     case ProfilesDir:      return getSystemPath(StuffDir) + "profiles";
     default: return TFilePath();
    }
}

void EnvGlobals::setSystemPath(int id, const TFilePath &fp)
{
  m_systemPaths[id] = fp;
}
*/

}  // namespace

//=========================================================
//
// Variable::Imp
//
//=========================================================

class Variable::Imp {
public:
  std::string m_name;
  std::string m_value;
  bool m_loaded, m_defaultDefined, m_assigned;

  Imp(std::string name)
      : m_name(name)
      , m_value("")
      , m_loaded(false)
      , m_defaultDefined(false)
      , m_assigned(false) {}
};

//=========================================================
//
// variable manager (singleton)
//
//=========================================================

namespace {

class VariableSet {
  std::map<std::string, Variable::Imp *> m_variables;
  bool m_loaded;

public:
  VariableSet() : m_loaded(false) {}

  ~VariableSet() {
    std::map<std::string, Variable::Imp *>::iterator it;
    for (it = m_variables.begin(); it != m_variables.end(); ++it)
      delete it->second;
  }

  static VariableSet *instance() {
    static VariableSet instance;
    return &instance;
  }

  Variable::Imp *getImp(std::string name) {
    std::map<std::string, Variable::Imp *>::iterator it;
    it = m_variables.find(name);
    if (it == m_variables.end()) {
      Variable::Imp *imp = new Variable::Imp(name);
      m_variables[name]  = imp;
      return imp;
    } else
      return it->second;
  }

  void commit() {
    // save();
  }

  void loadIfNeeded() {
    if (m_loaded) return;
    m_loaded = true;
    try {
      load();
    } catch (...) {
    }
  }

  void load();
  void save();
};

//-------------------------------------------------------------------

void VariableSet::load() {
#ifndef WIN32
  EnvGlobals::instance()->updateEnvFile();
#endif
  TFilePath fp = EnvGlobals::instance()->getEnvFile();
  if (fp == TFilePath()) return;
  // if the personal env is not found, then try to find the template
  if (!TFileStatus(fp).doesExist())
    fp = EnvGlobals::instance()->getTemplateEnvFile();
  Tifstream is(fp);
  if (!is.isOpen()) return;
  char buffer[1024];
  while (is.getline(buffer, sizeof(buffer))) {
    char *s = buffer;
    while (*s == ' ') s++;
    char *t = s;
    while (('a' <= *s && *s <= 'z') || ('A' <= *s && *s <= 'Z') ||
           ('0' <= *s && *s <= '9') || *s == '_')
      s++;
    std::string name(t, s - t);
    if (name.size() == 0) continue;
    while (*s == ' ') s++;
    if (*s != '\"') continue;
    s++;
    std::string value;
    while (*s != '\n' && *s != '\0' && *s != '\"') {
      if (*s != '\\')
        value.push_back(*s);
      else {
        s++;
        if (*s == '\\')
          value.push_back('\\');
        else if (*s == '"')
          value.push_back('"');
        else if (*s == 'n')
          value.push_back('\n');
        else
          continue;
      }
      s++;
    }
    Variable::Imp *imp = getImp(name);
    imp->m_value       = value;
    imp->m_loaded      = true;
  }
}

//-------------------------------------------------------------------

void VariableSet::save() {
  TFilePath fp = EnvGlobals::instance()->getEnvFile();
  if (fp == TFilePath()) return;
  bool exists = TFileStatus(fp.getParentDir()).doesExist();
  if (!exists) {
    try {
      TSystem::mkDir(fp.getParentDir());
    } catch (...) {
      return;
    }
  }
  Tofstream os(fp);
  if (!os) return;
  std::map<std::string, Variable::Imp *>::iterator it;
  for (it = m_variables.begin(); it != m_variables.end(); ++it) {
    os << it->first << " \"";
    std::string s = it->second->m_value;
    for (int i = 0; i < (int)s.size(); i++)
      if (s[i] == '\"')
        os << "\\\"";
      else if (s[i] == '\\')
        os << "\\\\";
      else if (s[i] == '\n')
        os << "\\n";
      else
        os.put(s[i]);
    os << "\"" << std::endl;
  }
}

//-------------------------------------------------------------------

}  // namespace

//=========================================================

Variable::Variable(std::string name)
    : m_imp(VariableSet::instance()->getImp(name)) {}

//-------------------------------------------------------------------

Variable::Variable(std::string name, std::string defaultValue)
    : m_imp(VariableSet::instance()->getImp(name)) {
  // assert(!m_imp->m_defaultDefined);
  m_imp->m_defaultDefined = true;
  if (!m_imp->m_loaded) m_imp->m_value = defaultValue;
}

//-------------------------------------------------------------------

Variable::~Variable() {}

//-------------------------------------------------------------------

std::string Variable::getName() const { return m_imp->m_name; }

//-------------------------------------------------------------------

std::string Variable::getValue() const {
  VariableSet::instance()->loadIfNeeded();
  return m_imp->m_value;
}

//-------------------------------------------------------------------

void Variable::assignValue(std::string value) {
  VariableSet *vs = VariableSet::instance();
  vs->loadIfNeeded();
  m_imp->m_value = value;
  try {
    vs->commit();
  } catch (...) {
  }
}

//===================================================================

void TEnv::setApplicationFileName(std::string appFileName) {
  TFilePath fp(appFileName);
#ifdef MACOSX
  if (fp.getWideName().find(L".app"))
    for (int i = 0; i < 3; i++) fp = fp.getParentDir();
#elif defined(LINUX) || defined(FREEBSD)
  if (fp.getWideName().find(L".appimage"))
    for (int i = 0; i < 2; i++) fp = fp.getParentDir();
#endif
  EnvGlobals::instance()->setApplicationFileName(fp.getName());
}

std::string TEnv::getApplicationFileName() {
  return EnvGlobals::instance()->getApplicationFileName();
}

std::string TEnv::getApplicationName() {
  return EnvGlobals::instance()->getApplicationName();
}

std::string TEnv::getApplicationVersion() {
  return EnvGlobals::instance()->getApplicationVersion();
}

void TEnv::setApplicationFullName(std::string applicationFullName) {
  EnvGlobals::instance()->setApplicationFullName(applicationFullName);
}

std::string TEnv::getApplicationFullName() {
  return EnvGlobals::instance()->getApplicationFullName();
}

void TEnv::setModuleName(std::string moduleName) {
  EnvGlobals::instance()->setModuleName(moduleName);
}

std::string TEnv::getModuleName() {
  return EnvGlobals::instance()->getModuleName();
}

void TEnv::setRootVarName(std::string varName) {
  EnvGlobals::instance()->setRootVarName(varName);
}

std::string TEnv::getRootVarName() {
  return EnvGlobals::instance()->getRootVarName();
}

TFilePath TEnv::getRootVarPath() {
  return EnvGlobals::instance()->getRootVarPath();
}

std::string TEnv::getSystemVarStringValue(std::string varName) {
  return EnvGlobals::instance()->getSystemVarValue(varName);
}

TFilePath TEnv::getSystemVarPathValue(std::string varName) {
  return EnvGlobals::instance()->getSystemVarPathValue(varName);
}

TFilePathSet TEnv::getSystemVarPathSetValue(std::string varName) {
  TFilePathSet lst;
  EnvGlobals *eg = EnvGlobals::instance();
  // if the path is registered by command line argument, then use it
  std::string value = eg->getArgPathValue(varName);
  if (value == "") value = eg->getSystemVarValue(varName);
  int len = (int)value.size();
  int i   = 0;
  int j   = value.find(';');
  while (j != std::string::npos) {
    std::string s = value.substr(i, j - i);
    lst.push_back(TFilePath(s));
    i = j + 1;
    if (i >= len) return lst;
    j = value.find(';', i);
  }
  if (i < len) lst.push_back(TFilePath(value.substr(i)));
  return lst;
}

void TEnv::setSystemVarPrefix(std::string varName) {
  EnvGlobals::instance()->setSystemVarPrefix(varName);
}

std::string TEnv::getSystemVarPrefix() {
  return EnvGlobals::instance()->getSystemVarPrefix();
}

TFilePath TEnv::getStuffDir() {
  // #ifdef MACOSX
  //  return TFilePath("/Applications/Toonz 5.0/Toonz 5.0 stuff");
  // #else
  return EnvGlobals::instance()->getStuffDir();
  // #endif
}

bool TEnv::getIsPortable() { return EnvGlobals::instance()->getIsPortable(); }

TFilePath TEnv::getConfigDir() {
  TFilePath configDir = getSystemVarPathValue(getSystemVarPrefix() + "CONFIG");
  if (configDir == TFilePath())
    configDir = getStuffDir() + systemPathMap.at("CONFIG");
  return configDir;
}

/*TFilePath TEnv::getProfilesDir()
{
  TFilePath fp(getStuffDir());
  return fp != TFilePath() ? fp + "profiles" : fp;
}
*/
void TEnv::setStuffDir(const TFilePath &stuffDir) {
  EnvGlobals::instance()->setStuffDir(stuffDir);
}

void TEnv::saveAllEnvVariables() { VariableSet::instance()->save(); }

bool TEnv::setArgPathValue(std::string key, std::string value) {
  EnvGlobals *eg = EnvGlobals::instance();
  // in case of "-TOONZROOT" , set the all unregistered paths
  if (key == getRootVarName()) {
    TFilePath rootPath(value);
    eg->setStuffDir(rootPath);
    for (auto itr = systemPathMap.begin(); itr != systemPathMap.end(); ++itr) {
      std::string k   = getSystemVarPrefix() + (*itr).first;
      std::string val = value + "\\" + (*itr).second;
      // set all unregistered values
      if (eg->getArgPathValue(k) == "") eg->setArgPathValue(k, val);
    }
    return true;
  } else {
    for (auto itr = systemPathMap.begin(); itr != systemPathMap.end(); ++itr) {
      // found the corresponding registry key
      if (key == getSystemVarPrefix() + (*itr).first) {
        eg->setArgPathValue(key, value);
        return true;
      }
    }
    // registry key not found. failed to register
    return false;
  }
}

const std::map<std::string, std::string> &TEnv::getSystemPathMap() {
  return systemPathMap;
}

/*
void TEnv::defineSystemPath(SystemFileId id, const TFilePath &registryName)
{
  string s = TSystem::getSystemValue(registryName);
  if(s=="") return;
  EnvGlobals::instance()->setSystemPath(id, TFilePath(s));
}

//---------------------------------------------------------


TFilePath TEnv::getSystemPath(SystemFileId id)
{
  return EnvGlobals::instance()->getSystemPath(id);
}
*/

//=========================================================
//
// Variabili tipizzate
//
//=========================================================

namespace {

std::istream &operator>>(std::istream &is, TFilePath &path) {
  std::string s;
  is >> s;
  return is;
}

std::istream &operator>>(std::istream &is, TRect &rect) {
  return is >> rect.x0 >> rect.y0 >> rect.x1 >> rect.y1;
}

template <class T>
std::string toString2(T value) {
  std::ostringstream ss;
  ss << value << '\0';
  return ss.str();
}

template <>
std::string toString2(TRect value) {
  std::ostringstream ss;
  ss << value.x0 << " " << value.y0 << " " << value.x1 << " " << value.y1;
  return ss.str();
}

template <class T>
void fromString(std::string s, T &value) {
  if (s.empty()) return;
  std::istringstream is(s);
  is >> value;
}

void fromString(std::string s, std::string &value) { value = s; }

}  // namespace

//-------------------------------------------------------------------

IntVar::IntVar(std::string name, int defValue)
    : Variable(name, std::to_string(defValue)) {}
IntVar::IntVar(std::string name) : Variable(name) {}
IntVar::operator int() const {
  int v;
  fromString(getValue(), v);
  return v;
}
void IntVar::operator=(int v) { assignValue(std::to_string(v)); }

//-------------------------------------------------------------------

DoubleVar::DoubleVar(std::string name, double defValue)
    : Variable(name, std::to_string(defValue)) {}
DoubleVar::DoubleVar(std::string name) : Variable(name) {}
DoubleVar::operator double() const {
  double v;
  fromString(getValue(), v);
  return v;
}
void DoubleVar::operator=(double v) { assignValue(std::to_string(v)); }

//-------------------------------------------------------------------

StringVar::StringVar(std::string name, const std::string &defValue)
    : Variable(name, defValue) {}
StringVar::StringVar(std::string name) : Variable(name) {}
StringVar::operator std::string() const {
  std::string v;
  fromString(getValue(), v);
  return v;
}
void StringVar::operator=(const std::string &v) { assignValue(v); }

//-------------------------------------------------------------------

FilePathVar::FilePathVar(std::string name, const TFilePath &defValue)
    : Variable(name, ::to_string(defValue)) {}
FilePathVar::FilePathVar(std::string name) : Variable(name) {}
FilePathVar::operator TFilePath() const {
  std::string v;
  fromString(getValue(), v);
  return TFilePath(v);
}
void FilePathVar::operator=(const TFilePath &v) { assignValue(::to_string(v)); }

//-------------------------------------------------------------------

RectVar::RectVar(std::string name, const TRect &defValue)
    : Variable(name, toString2(defValue)) {}
RectVar::RectVar(std::string name) : Variable(name) {}
RectVar::operator TRect() const {
  TRect v;
  fromString(getValue(), v);
  return v;
}
void RectVar::operator=(const TRect &v) { assignValue(toString2(v)); }

//=========================================================
