text: #include "configurator.h"
  
  #include <QCoreApplication>
  #include <QDebug>
  #include <QFile>
  #include <QGuiApplication>
  #include <QJsonDocument>
  #include <QLocale>
  #include <QProcessEnvironment>
  #include <QStandardPaths>
  
  #include "constants.h"
  
  QJsonObject Configurator::DEFAULT_APP_CONFIG = {
    {"autoStart", DEFAULT_AUTO_START},
    {"hideWindow", DEFAULT_HIDE_WINDOW},
    {"enableSysProxy", AUTO_ENABLE_SYS_PROXY},
    {"defaultSysProxyProtocol", DEFAULT_SYS_PROXY_PROTOCOL},
    {"language", DEFAULT_LANGUAGE},
    {"serverIp", DEFAULT_SERVER_IP},
    {"httpPort", DEFAULT_HTTP_PORT},
    {"socksPort", DEFAULT_SOCKS_PORT},
    {"dns", DEFAULT_DNS_SERVER},
    {"proxyMode", DEFAULT_PROXY_MODE},
    {"gfwListUrl", DEFAULT_GFW_LIST_URL},
    {"gfwListLastUpdated", "Never"}};
  
  QString Configurator::getDefaultLanguage() {
    const static QMap<QLocale::Language, QString> LANGUAGES{
      {QLocale::Chinese, "zh-CN"}};
    QLocale::Language systemLocale = QLocale::system().language();
    return LANGUAGES.contains(systemLocale) ? LANGUAGES[systemLocale] : "en-US";
  }
  
  Configurator::Configurator() { connectedServerNames = getAutoConnectServers(); }
  
  QStringList Configurator::getAutoConnectServers() {
    QStringList autoConnectedServers;
    QJsonArray servers = getServers();
    for (auto itr = servers.begin(); itr != servers.end(); ++itr) {
      QJsonObject server = (*itr).toObject();
      if (server["autoConnect"].toBool() && server.contains("name")) {
        autoConnectedServers.append(server["name"].toString());
      }
    }
    return autoConnectedServers;
  }
  
  Configurator &Configurator::getInstance() {
    static Configurator configuratorInstance;
    return configuratorInstance;
  }
  
  QDir Configurator::getAppConfigDir() {
    QDir appConfigDir =
      QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation))
        .filePath(QCoreApplication::applicationName());
    if (!appConfigDir.exists()) {
      appConfigDir.mkpath(".");
    }
    return appConfigDir;
  }
  
  QDir Configurator::getAppTempDir() {
    QDir tempDir =
      QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation))
        .filePath(QCoreApplication::applicationName());
    if (!tempDir.exists()) {
      tempDir.mkpath(".");
    }
    return tempDir;
  }
  
  QString Configurator::getV2RayInstallDirPath() {
    if (!V2RAY_USE_LOCAL_INSTALL) {
      return V2RAY_SYS_INSTALL_DIR;
    }
    return QDir(QCoreApplication::applicationDirPath())
      .filePath(V2RAY_LOCAL_INSTALL_DIR);
  }
  
  QString Configurator::getLocaleDirPath() {
    return QDir(QCoreApplication::applicationDirPath()).filePath(LOCALE_DIR);
  }
  
  QString Configurator::getAppFilePath() {
    if (QProcessEnvironment::systemEnvironment().contains("APPIMAGE")) {
      return QProcessEnvironment::systemEnvironment().value("APPIMAGE");
    }
    return QGuiApplication::applicationFilePath();
  }
  
  QString Configurator::getAppWorkingDirPath() {
    if (QProcessEnvironment::systemEnvironment().contains("OWD")) {
      return QProcessEnvironment::systemEnvironment().value("OWD");
    }
    return QCoreApplication::applicationDirPath();
  }
  
  QString Configurator::getAppLogFilePath() {
    return getAppConfigDir().filePath(APP_LOG_FILE_NAME);
  }
  
  QString Configurator::getAppConfigFilePath() {
    return getAppConfigDir().filePath(APP_CFG_FILE_NAME);
  }
  
  QString Configurator::getV2RayLogFilePath() {
    return getAppConfigDir().filePath(V2RAY_CORE_LOG_FILE_NAME);
  }
  
  QString Configurator::getV2RayConfigFilePath() {
    return getAppConfigDir().filePath(V2RAY_CORE_CFG_FILE_NAME);
  }
  
  QString Configurator::getGfwListFilePath() {
    return getAppConfigDir().filePath(GFW_LIST_FILE_NAME);
  }
  
  QJsonObject Configurator::getAppConfig() {
    QJsonObject config = DEFAULT_APP_CONFIG;
    QFile appCfgFile(getAppConfigFilePath());
    if (appCfgFile.exists() &&
        appCfgFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
      QJsonDocument configDoc = QJsonDocument::fromJson(appCfgFile.readAll());
      config                  = configDoc.object();
      if (config.empty()) {
        qWarning() << "Failed to parse the app config.";
        config = DEFAULT_APP_CONFIG;
      }
      appCfgFile.close();
    }
    // Replace old proxy mode with a newer value
    QString proxyMode = config["proxyMode"].toString();
    if (proxyMode == "pac" || proxyMode == "global" || proxyMode == "manual") {
      config["proxyMode"] = "rule";
    }
    // Replace old GFW List URL with a newer value
    QString _glu =
      "https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt";
    QString gfwListUrl = config["gfwListUrl"].toString();
    if (gfwListUrl == _glu) {
      config["gfwListUrl"]         = DEFAULT_GFW_LIST_URL;
      config["gfwListLastUpdated"] = "Never";
    }
    return config;
  }
  
  QString Configurator::getLanguage() {
    QJsonObject appConfig = getAppConfig();
    return appConfig.contains("language") ? appConfig["language"].toString()
                                          : getDefaultLanguage();
  }
  
  void Configurator::setAppConfig(QJsonObject config) {
    // Load current configuration
    QJsonObject _config = getAppConfig();
    // Overwrite new values to current configuration
    for (auto itr = config.begin(); itr != config.end(); ++itr) {
      QString configName   = itr.key();
      QVariant configValue = itr.value().toVariant();
      switch (configValue.type()) {
        case QVariant::Bool: _config[configName] = configValue.toBool(); break;
        case QVariant::Double:
        case QVariant::Int: _config[configName] = configValue.toInt(); break;
        case QVariant::String:
          _config[configName] = configValue.toString();
          break;
        case QVariant::List:
          _config[configName] = configValue.toJsonArray();
          break;
        default:
          qWarning() << "Ignore unknown config item [Name=" << configName
                     << ", Type=" << configValue.type() << "]";
          break;
      }
    }
    // Save config to file
    QFile configFile(getAppConfigFilePath());
    configFile.open(QFile::WriteOnly);
    configFile.write(QJsonDocument(_config).toJson());
  }
  
  QJsonObject Configurator::getV2RayConfig() {
    QJsonObject appConfig = getAppConfig();
    QJsonArray connectedServerNames;
    for (QString csn : getConnectedServerNames()) {
      connectedServerNames.append(csn);
    }
  
    QJsonObject v2RayConfig{
      {"port", appConfig["httpPort"].toInt()},
      {"socks-port", appConfig["socksPort"].toInt()},
      {"allow-lan", true},
      {"bind-address", appConfig["serverIp"].toString()},
      {"mode", "rule"},
      {"log-level", "info"},
      {"dns", QJsonObject{{"enable", false},
                          {"listen", "0.0.0.0:53"},
                          {"nameserver",
                           getPrettyDnsServers(appConfig["dns"].toString())}}},
      {"proxies", getConnectedServers()},
      {"proxy-groups",
       QJsonArray{{QJsonObject{{"name", "PROXY"},
                               {"type", "load-balance"},
                               {"proxies", connectedServerNames},
                               {"url", "http://www.gstatic.com/generate_204"},
                               {"interval", 300}}}}},
      {"rules", getRules()}};
  
    return v2RayConfig;
  }
  
  QJsonArray Configurator::getPrettyDnsServers(QString dnsString) {
    QJsonArray dnsServers;
    QStringList _dnsServers = dnsString.split(';');
    for (QString ds : _dnsServers) {
      dnsServers.append(ds.trimmed());
    }
    return dnsServers;
  }
  
  QJsonArray Configurator::getServers() {
    QJsonObject appConfig = getAppConfig();
    QJsonArray servers    = appConfig.contains("servers")
                           ? appConfig["servers"].toArray()
                           : QJsonArray();
    return servers;
  }
  
  QJsonObject Configurator::getServer(QString serverName) {
    QJsonArray servers = getServers();
    QJsonObject server;
    for (auto itr = servers.begin(); itr != servers.end(); ++itr) {
      QJsonObject _server = (*itr).toObject();
      if (_server.contains("name") && _server["name"].toString() == serverName) {
        server = (*itr).toObject();
        break;
      }
    }
    return server;
  }
  
  QStringList Configurator::getSubscriptionUrls() {
    QStringList subscriptionUrls;
    QJsonArray servers = getServers();
    for (auto itr = servers.begin(); itr != servers.end(); ++itr) {
      QJsonObject server = (*itr).toObject();
      if (server.contains("subscription")) {
        QString subscriptionUrl = server["subscription"].toString();
        if (!subscriptionUrls.contains(subscriptionUrl)) {
          subscriptionUrls.append(subscriptionUrl);
        }
      }
    }
    return subscriptionUrls;
  }
  
  int Configurator::addServer(QJsonObject serverConfig) {
    QJsonArray servers = getServers();
    servers.append(serverConfig);
    setAppConfig(QJsonObject{{"servers", servers}});
    return 1;
  }
  
  int Configurator::editServer(QString serverName, QJsonObject serverConfig) {
    QJsonArray servers = getServers();
    bool isEdited      = false;
    for (auto itr = servers.begin(); itr != servers.end(); ++itr) {
      QJsonObject server = (*itr).toObject();
      if (server.contains("name") && server["name"].toString() == serverName) {
        serverConfig["subscription"] = server.contains("subscription")
                                         ? server["subscription"].toString()
                                         : "";
        *itr     = serverConfig;
        isEdited = true;
      }
    }
    // Update the server in connected servers
    if (connectedServerNames.contains(serverName)) {
      connectedServerNames.removeAt(connectedServerNames.indexOf(serverName));
      connectedServerNames.append(serverConfig["name"].toString());
    }
    // Update servers in the config
    setAppConfig(QJsonObject{{"servers", servers}});
    return isEdited;
  }
  
  int Configurator::removeServer(QString serverName) {
    QJsonArray servers = getServers();
    int serverIndex    = -1;
    for (int i = 0; i < servers.size(); ++i) {
      QJsonObject server = servers[i].toObject();
      if (server.contains("name") && server["name"].toString() == serverName) {
        serverIndex = i;
        break;
      }
    }
    servers.removeAt(serverIndex);
    // Remove the server from connected servers
    if (connectedServerNames.contains(serverName)) {
      connectedServerNames.removeAt(connectedServerNames.indexOf(serverName));
    }
    // Update servers in the config
    setAppConfig(QJsonObject{{"servers", servers}});
    return serverIndex != -1;
  }
  
  QMap<QString, QJsonObject> Configurator::removeSubscriptionServers(
    QString subscriptionUrl) {
    QJsonArray servers = getServers();
    QMap<QString, QJsonObject> removedServers;
    QJsonArray remainingServers;
  
    for (int i = 0; i < servers.size(); ++i) {
      QJsonObject server = servers[i].toObject();
      QString serverName =
        server.contains("name") ? server["name"].toString() : "";
  
      if (server.contains("subscription") &&
          server["subscription"].toString() == subscriptionUrl) {
        removedServers[serverName] = server;
        // Remove the server from connected servers
        if (connectedServerNames.contains(serverName)) {
          connectedServerNames.removeAt(connectedServerNames.indexOf(serverName));
        }
        continue;
      }
      remainingServers.append(server);
    }
    // Update servers in the config
    setAppConfig(QJsonObject{{"servers", remainingServers}});
    return removedServers;
  }
  
  QJsonArray Configurator::getConnectedServers() {
    QJsonObject appConfig = getAppConfig();
  
    QJsonArray servers = getServers();
    QJsonArray connectedServers;
    for (auto itr = servers.begin(); itr != servers.end(); ++itr) {
      QJsonObject server = (*itr).toObject();
      QString serverName =
        server.contains("name") ? server["name"].toString() : "";
  
      if (connectedServerNames.contains(serverName)) {
        server.remove("autoConnect");
        server.remove("subscription");
        connectedServers.append(server);
      }
    }
    return connectedServers;
  }
  
  QStringList Configurator::getConnectedServerNames() {
    return connectedServerNames;
  }
  
  void Configurator::setServerConnection(QString serverName, bool connected) {
    int serverIndex = connectedServerNames.indexOf(serverName);
    if (connected && serverIndex == -1) {
      connectedServerNames.append(serverName);
    } else if (!connected && serverIndex != -1) {
      connectedServerNames.removeAt(serverIndex);
    }
  }
  
  QJsonArray Configurator::getRules() {
    QJsonObject appConfig = getAppConfig();
    QString proxyMode     = appConfig["proxyMode"].toString();
    QString defaultAct    = "MATCH, PROXY";
  
    QJsonArray rules;
    if (proxyMode == "Direct") {
      defaultAct = "MATCH, DIRECT";
    } else if (proxyMode == "Global") {
      defaultAct = "MATCH, PROXY";
    } else {
      QJsonArray userRules    = getGfwListRules();
      QJsonArray gfwListRules = getGfwListRules();
      if (userRules.size() + gfwListRules.size() == 0) {
        defaultAct = "MATCH, PROXY";
      }
      rules.append("IP-CIDR, 127.0.0.0/8, DIRECT");
      rules.append("IP-CIDR, 10.0.0.0/8, DIRECT");
      rules.append("IP-CIDR, 172.16.0.0/12, DIRECT");
      rules.append("IP-CIDR, 192.168.0.0/16, DIRECT");
      for (auto itr = userRules.begin(); itr != userRules.end(); ++itr) {
        rules.append(*itr);
      }
      for (auto itr = gfwListRules.begin(); itr != gfwListRules.end(); ++itr) {
        rules.append(*itr);
      }
      rules.append("GEOIP, CN, DIRECT");
    }
    rules.append(defaultAct);
    return rules;
  }
  
  QJsonArray Configurator::getGfwListRules() {
    return getRules(getGfwListFilePath());
  }
  
  QJsonArray Configurator::getUserRules() {
    QJsonArray rules;
    // TODO
    return rules;
  }
  
  QJsonArray Configurator::getRules(const QString &fileName) {
    static const QStringList SUPPORTED_MATCHES{
      "DOMAIN-SUFFIX", "DOMAIN",   "DOMAIN-KEYWORD", "IP-CIDR",
      "SRC-IP-CIDR",   "DST-PORT", "SRC-PORT"};
    static const QStringList SUPPORTED_ACTS{"PROXY", "DIRECT", "REJECT"};
    QJsonArray rules;
    QFile ruleFile(fileName);
  
    if (ruleFile.exists() &&
        ruleFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
      while (!ruleFile.atEnd()) {
        QString line = ruleFile.readLine().trimmed();
        if (!line.startsWith('-')) {
          continue;
        }
  
        QStringList rule = line.mid(1).split(',');
        if (rule.size() != 3) {
          qWarning() << QString("Ignore rule: %1 in %2").arg(line, fileName);
          continue;
        }
        QString match  = rule.at(0).trimmed().toUpper();
        QString value  = rule.at(1).trimmed().toLower();
        QString action = rule.at(2).trimmed().toUpper();
  
        if (!SUPPORTED_MATCHES.contains(match)) {
          qWarning() << QString("Unsupported match: %1 for rule: %2 in %3")
                          .arg(match, line, fileName);
          continue;
        }
        if (!SUPPORTED_ACTS.contains(action)) {
          qWarning() << QString("Unsupported match: %1 for rule: %2 in %3")
                          .arg(match, line, fileName);
          continue;
        }
        rules.append(QString("%1, %2, %3").arg(match, value, action));
      }
    }
    return rules;
  }
  