#include "CommandLineArgs.h"

#include "CefSettingsParser.h"
#include "Utils.h"
#include "log/Log.h"

#if defined(OS_MAC)
#include <boost/filesystem.hpp>
namespace CefUtils {
std::string getFrameworkDir();
}
#elif defined(OS_WIN)
#include <boost/filesystem.hpp>
#endif

namespace {
const bool doTrace = getBoolEnv("CEF_SERVER_TRACE_CommandLineArgs");
const bool dontUseDefaultChromiumSwitches = getBoolEnv("CEF_SERVER_TRACE_DontUseDefaultChromiumSwitches");
}

CommandLineArgs::CommandLineArgs() {
  const long defVal = myOpenTransportCooldownMs;
  myOpenTransportCooldownMs = getLongEnv("CEF_SERVER_TRANSPORT_OPEN_COOLDOWN_MS", defVal);
  if (myOpenTransportCooldownMs != defVal) {
    if (myOpenTransportCooldownMs < 0) myOpenTransportCooldownMs = 0;
    if (myOpenTransportCooldownMs > 500) myOpenTransportCooldownMs = 500;
    fprintf(stderr, "\tUse OpenTransportCooldownMs=%d\n", myOpenTransportCooldownMs);
  }
}

void trace(const std::string & from, const std::vector<std::string> & cmdlineSwitches, const std::vector<std::pair<std::string, std::string>> & parsedSettings, const std::vector<std::pair<std::string, int>> & schemes) {
  if (!doTrace || !Log::isTraceEnabled())
    return;

  if (!cmdlineSwitches.empty()) {
    Log::trace("Command line switches (%s):", from.c_str());
    for (auto& sw : cmdlineSwitches)
      Log::trace("\t%s", sw.c_str());
  }
  if (!parsedSettings.empty()) {
    Log::trace("Settings (%s):", from.c_str());
    for (auto& st : parsedSettings)
      Log::trace("\t%s=%s", st.first.c_str(), st.second.c_str());
  }
  if (!schemes.empty()) {
    Log::trace("Custom schemes (%s):", from.c_str());
    for (auto& sch : schemes)
      Log::trace("\t%s [%d]", sch.first.c_str(), sch.second);
  }
}

bool CommandLineArgs::init(int argc, char* argv[]) {
  // This method is called very early.

  // 1. Initialize logger at first and check '--help'
  if (doTrace)
    Log::trace("CommandLineArgs: init with args:");
  for (int c = 0; c < argc; ++c) {
    const char * arg = argv[c];
    if (arg == nullptr || arg[0] == 0) continue;

    std::string word(arg);
    size_t tokenPos;
    if (word.compare("--help") == 0 || word.compare("-h") == 0) {
      fprintf(stdout, "Usage: cef-server [--port=PORT] [--params=PARAMS_FILE] [--loglevel=LEVEL] [--logfile=FILE]\nOther switches are enumerated in CommandLineArgs.cpp\n");
      return false;
    }
    if ((tokenPos = word.find("--logfile=")) != word.npos) {
      myPathLogFile = word.substr(tokenPos + 10);
    } else if ((tokenPos = word.find("--loglevel=")) != word.npos) {
      std::string sval = word.substr(tokenPos + 11);
      try {
        myLogLevel = std::stoi(sval);
      } catch (const std::exception&) {
        myLogLevel = Log::str2level(word);
      }
    }
    if (doTrace)
      Log::trace("\t%s", word.c_str());
  } // for

  Log::init(myLogLevel, myPathLogFile);

  // 2. Parse other command line arguments.
  for (int c = 0; c < argc; ++c) {
    const char * arg = argv[c];
    if (arg == nullptr || arg[0] == 0) continue;

    // NOTE: these switches don't conflict with chromium one.
    // See https://peter.sh/experiments/chromium-command-line-switches/
    std::string word(arg);
    if (word.find("--loglevel") == 0 || word.find("--logfile") == 0)
      continue;

    if (doTrace)
      Log::trace("\tprocess cmd line arg: %s", word.c_str());

    if (word == "--cef-server-wait-debugger") {
      myWaitDebugger = true;
      continue;
    }

    std::string stmp;
    int ntmp;
    size_t tokenPos;
    if ((tokenPos = word.find("--port=")) != word.npos) {
      std::string val = word.substr(tokenPos + 7);
      myPort = std::stoi(val);
      myUseTcp = true;
    } else if ((tokenPos = word.find("--pipe=")) != word.npos) {
      myPathPipe = word.substr(tokenPos + 7);
      myUseTcp = false;
    } else if ((tokenPos = word.find("--params=")) != word.npos) {
      myPathParamsFile = word.substr(tokenPos + 9);
    } else if ((tokenPos = word.find("--root=")) != word.npos) {
      myPathRootCache = word.substr(tokenPos + 7);
    } else if ((tokenPos = word.find("--logchromiumfile=")) != word.npos) {
      myPathChromiumLogFile = word.substr(tokenPos + 18);
    } else if ((tokenPos = word.find("--logchromiumlevel=")) != word.npos) {
      std::string sval = word.substr(tokenPos + 19);
      try {
        myLogLevelChromium = std::stoi(sval);
      } catch (const std::exception&) {}
    } else if ((tokenPos = word.find("--deleteRootCacheDir")) != word.npos) {
      myDeleteRootCacheDir = true;
    } else if (CefSettingsParser::parseCefCmdLineSwitch(word, stmp)) {
      myChromiumSwitches.push_back(stmp);
    } else if (CefSettingsParser::parseCefSchemeWord(word, stmp, ntmp)) {
      myCustomSchemes.push_back(std::make_pair(stmp, ntmp));
    } else if (CefSettingsParser::parseCefSettingWord(word, myParsedCefSettings)) {
      ; // nothing to do
    } else if (word.find("--") == 0) {
      myChromiumSwitches.push_back(word);
    } else {
      if (doTrace)
        Log::trace("Parse command line: skip unknown word %s", word.c_str());
    }
  } // for

  trace("command line", myChromiumSwitches, myParsedCefSettings, myCustomSchemes);

  if (!myPathParamsFile.empty()) {
    std::vector<std::pair<std::string, std::string>> fileSettings;
    std::vector<std::string> fileSwitches;
    std::vector<std::pair<std::string, int>> fileSchemes;
    CefSettingsParser::parseParamsFile(myPathParamsFile, fileSwitches, fileSettings, fileSchemes);
    if (!fileSwitches.empty() || !fileSettings.empty() || !fileSchemes.empty()) {
      Log::debug("Params file isn't empty, some command line arguments can be overriden.");
      trace("file", fileSwitches, fileSettings, fileSchemes);
      myChromiumSwitches.insert(myChromiumSwitches.end(), fileSwitches.begin(), fileSwitches.end());
      myParsedCefSettings.insert(myParsedCefSettings.end(), fileSettings.begin(), fileSettings.end());
      myCustomSchemes.insert(myCustomSchemes.end(), fileSchemes.begin(), fileSchemes.end());
    }
  }

  // Init default chromium switches and CefSettings (if necessary)
  if (myChromiumSwitches.empty() && !dontUseDefaultChromiumSwitches) { // NOTE: dontUseDefaultChromiumSwitches == false by default
    Log::debug("Use default chromium switches.");
#if defined(OS_WIN)
     myChromiumSwitches.push_back("--disable-features=SpareRendererForSitePerProcess");
     myChromiumSwitches.push_back("--disable-gpu-process-crash-limit");
     myChromiumSwitches.push_back("--autoplay-policy=no-user-gesture-required");
     myChromiumSwitches.push_back("--disable-component-update");
#elif defined(OS_MAC)
    myChromiumSwitches.push_back("--disable-in-process-stack-traces");
    myChromiumSwitches.push_back("--use-mock-keychain");
    myChromiumSwitches.push_back("--disable-features=SpareRendererForSitePerProcess");
    myChromiumSwitches.push_back("--disable-notifications");
    myChromiumSwitches.push_back("--disable-gpu-process-crash-limit");
    myChromiumSwitches.push_back("--autoplay-policy=no-user-gesture-required");
    myChromiumSwitches.push_back("--disable-component-update");
  #else
    myChromiumSwitches.push_back("--disable-features=SpareRendererForSitePerProcess");
    myChromiumSwitches.push_back("--disable-gpu-process-crash-limit");
    myChromiumSwitches.push_back("--autoplay-policy=no-user-gesture-required");
    myChromiumSwitches.push_back("--disable-component-update");
    myChromiumSwitches.push_back("--no-proxy-server");
  #endif
  } // myChromiumSwitches.empty()

  if (myParsedCefSettings.empty()) {
    Log::debug("Use default cef settings.");
    myParsedCefSettings.push_back(std::make_pair("log_severity", Log::cefLogLevel2str(myLogLevelChromium)));
    if (!myPathChromiumLogFile.empty())
      myParsedCefSettings.push_back(std::make_pair("log_file", myPathChromiumLogFile));
  }

  trace("merged", myChromiumSwitches, myParsedCefSettings, myCustomSchemes);
  return true;
}

void CommandLineArgs::prepareCefSettings(void * pCefSettings) {
  CefSettings & settings = *reinterpret_cast<CefSettings*>(pCefSettings);
  for (const auto & p: myParsedCefSettings) {
    if (p.first.compare("cache_path") == 0 && !myPathRootCache.empty()) {
      Log::debug("Setting 'cache_path' from params file (or cmd line) with value '%s' will be overrriden with cmd line arg '--root=%s'", p.second.c_str(), myPathRootCache.c_str());
      CefString(&settings.cache_path) = myPathRootCache;
    } else
      CefSettingsParser::setSettingItem(settings, p.first, p.second);
  }

  settings.windowless_rendering_enabled = true;
  settings.multi_threaded_message_loop = false;
  settings.external_message_pump = false;
  settings.no_sandbox = true; // TODO: support sandbox later.

#if defined(OS_MAC)
  CefString(&settings.framework_dir_path) = CefUtils::getFrameworkDir();
  if (settings.browser_subprocess_path.length == 0) {
    // example: browser_subprocess_path=/Users/bocha/Downloads/jbr_jcef-25.0.1-osx-aarch64-b245.32/Contents/Frameworks/cef_server.app/Contents/Frameworks/cef_server Helper.app/Contents/MacOS/cef_server Helper
    boost::filesystem::path path = boost::filesystem::current_path()
                                       .append("..")
                                       .append("Frameworks")
                                       .append("cef_server Helper.app")
                                       .append("Contents")
                                       .append("MacOS")
                                       .append("cef_server Helper")
                                       .lexically_normal();
    if (utils::isFileExist(path.c_str())) {
      Log::debug("Set CefSettings.browser_subprocess_path=%s", path.c_str());
      CefString(&settings.browser_subprocess_path) = path.string();
    } else
      Log::error("Empty browser_subprocess_path.");
  }
#elif defined(OS_WIN)
  const bool doSetResourcesPath = getBoolEnv("CEF_SERVER_CommandLineArgs_SetResourcesPath");
  if (doSetResourcesPath) {
    auto installation_root =
        boost::filesystem::current_path().append("..").lexically_normal();

    boost::filesystem::path resources_dir_path =
        installation_root.append("lib");
    boost::filesystem::path framework_dir_path =
        installation_root.append("bin");

    std::string resources_path = resources_dir_path.string();
    std::string locales_dir_path =
        resources_dir_path.append("locales").string();

    CefString(&settings.resources_dir_path).FromString(resources_path);
    CefString(&settings.locales_dir_path).FromString(locales_dir_path);
    Log::debug("Set CefSettings.resources_dir_path=%s", resources_path.c_str());
    Log::debug("Set CefSettings.locales_dir_path=%s", locales_dir_path.c_str());
  }
#endif

#if defined(OS_POSIX)
  settings.disable_signal_handlers = true;
#endif

  if (Log::isTraceEnabled()) {
    std::stringstream ss;
    ss << "browser_subprocess_path=" << CefString(&settings.browser_subprocess_path).c_str() << std::endl;
    ss << "cache_path=" << CefString(&settings.cache_path).c_str() << std::endl;
    ss << "user_agent_product=" << CefString(&settings.user_agent_product).c_str() << std::endl;
    ss << "user_agent=" << CefString(&settings.user_agent).c_str() << std::endl;
    ss << "locales_dir_path=" << CefString(&settings.locales_dir_path).c_str() << std::endl;
    ss << "locale=" << CefString(&settings.locale).c_str() << std::endl;
    ss << "log_file=" << CefString(&settings.log_file).c_str() << std::endl;
    ss << "log_severity=" << settings.log_severity << std::endl;
    ss << "javascript_flags=" << CefString(&settings.javascript_flags).c_str() << std::endl;
    ss << "resources_dir_path=" << CefString(&settings.resources_dir_path).c_str() << std::endl;
    ss << "cookieable_schemes_list=" << CefString(&settings.cookieable_schemes_list).c_str() << std::endl;
    ss << "windowless_rendering_enabled=" << settings.windowless_rendering_enabled << std::endl;
    ss << "command_line_args_disabled=" << settings.command_line_args_disabled << std::endl;
    ss << "persist_session_cookies=" << settings.persist_session_cookies << std::endl;
    ss << "cookieable_schemes_exclude_defaults=" << settings.cookieable_schemes_exclude_defaults << std::endl;
    ss << "no_sandbox=" << settings.no_sandbox << std::endl;
    ss << "remote_debugging_port=" << settings.remote_debugging_port << std::endl;
    ss << "uncaught_exception_stack_size=" << settings.uncaught_exception_stack_size << std::endl;
    ss << "background_color=" << settings.background_color << std::endl;
    Log::trace("Prepared CefSettings:\n%s", ss.str().c_str());
  }
}
