/**
 * Copyright 2011 - 2022 José Expósito <jose.exposito89@gmail.com>
 *
 * This file is part of Touchégg.
 *
 * Touchégg is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License  as  published by  the  Free Software
 * Foundation,  either version 3 of the License,  or (at your option)  any later
 * version.
 *
 * Touchégg is distributed in the hope that it will be useful,  but  WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE.  See the  GNU General Public License  for more details.
 *
 * You should have received a copy of the  GNU General Public License along with
 * Touchégg. If not, see <http://www.gnu.org/licenses/>.
 */
#include "config/xml-config-loader.h"

#include <sys/inotify.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <unistd.h>

#include <array>
#include <chrono>  // NOLINT
#include <cstdlib>
#include <exception>
#include <iostream>
#include <pugixml.hpp>
#include <stdexcept>      // NOLINT
#include <string>         // NOLINT
#include <thread>         // NOLINT
#include <unordered_map>  // NOLINT
#include <utility>        // NOLINT
#include <vector>         // NOLINT

#include "config/config.h"
#include "utils/filesystem.h"
#include "utils/logger.h"
#include "utils/paths.h"
#include "utils/string.h"

namespace {
constexpr std::size_t WATCH_EVENT_SIZE = sizeof(struct inotify_event);
constexpr std::size_t WATCH_BUFFER_SIZE = (100 * (WATCH_EVENT_SIZE + 16));
}  // namespace

XmlConfigLoader::XmlConfigLoader(Config *config) : config(config) {}

void XmlConfigLoader::load() {
  this->parseConfig();
  this->watchConfig();
}

std::filesystem::path XmlConfigLoader::getConfigFilePath() {
  const std::filesystem::path usrConfigFile = Paths::getSystemConfigFilePath();
  const std::filesystem::path homeConfigFile = Paths::getUserConfigFilePath();

  if (!std::filesystem::exists(usrConfigFile)) {
    throw std::runtime_error{
        "File /usr/share/touchegg/touchegg.conf not found.\n"
        "Reinstall Touchégg to solve this issue"};
  }

  return std::filesystem::exists(homeConfigFile) ? homeConfigFile
                                                 : usrConfigFile;
}

void XmlConfigLoader::parseConfig() {
  std::filesystem::path configPath = XmlConfigLoader::getConfigFilePath();
  tlg::info << "Using configuration file " << configPath << std::endl;

  pugi::xml_document doc;
  pugi::xml_parse_result parsedSuccessfully = doc.load_file(configPath.c_str());

  if (!parsedSuccessfully) {
    throw std::runtime_error{"Error parsing configuration file"};
  }

  pugi::xml_node rootNode = doc.document_element();
  this->parseGlobalSettings(rootNode);
  this->parseApplicationXmlNodes(rootNode);
}

void XmlConfigLoader::parseGlobalSettings(const pugi::xml_node &rootNode) {
  pugi::xml_node settingsNode = rootNode.child("settings");
  for (pugi::xml_node propertyNode : settingsNode.children("property")) {
    const std::string property = propertyNode.attribute("name").value();
    const std::string value = propertyNode.child_value();
    this->config->saveGlobalSetting(property, value);
  }
}

void XmlConfigLoader::parseApplicationXmlNodes(const pugi::xml_node &rootNode) {
  for (pugi::xml_node applicationNode : rootNode.children("application")) {
    const std::string appsStr = applicationNode.attribute("name").value();
    const std::vector<std::string> applications = split(appsStr, ',');

    for (pugi::xml_node gestureNode : applicationNode.children("gesture")) {
      const std::string gestureType = gestureNode.attribute("type").value();
      const std::string fingers = gestureNode.attribute("fingers").value();
      const std::string direction = gestureNode.attribute("direction").value();

      pugi::xml_node actionNode = gestureNode.child("action");
      const std::string actionType = actionNode.attribute("type").value();

      std::unordered_map<std::string, std::string> actionSettings;
      for (pugi::xml_node settingNode : actionNode.children()) {
        const std::string settingName = settingNode.name();
        const std::string settingValue = settingNode.child_value();
        actionSettings[settingName] = settingValue;
      }

      // Save the gesture config for each application
      for (const std::string &application : applications) {
        this->config->saveGestureConfig(
            trim(application), gestureTypeFromStr(gestureType), fingers,
            gestureDirectionFromStr(direction), actionTypeFromStr(actionType),
            actionSettings);
      }
    }
  }
}

void XmlConfigLoader::watchConfig() {
  // https://developer.ibm.com/tutorials/l-ubuntu-inotify/

  std::filesystem::path homeConfigDir = Paths::getUserConfigDirPath();
  Paths::createUserConfigDir();

  const std::string warningMessage =
      "It was not posssible to monitor your configuration file for changes. "
      "Touchégg will not be able to automatically reload your configuration "
      "when you change it. You will need to restart Touchégg to apply your "
      "configuration changes";

  int fd = inotify_init();
  if (fd < 0) {
    tlg::warning << warningMessage << std::endl;
    return;
  }

  int wd = inotify_add_watch(fd, homeConfigDir.c_str(),
                             IN_MODIFY | IN_CREATE | IN_MOVE | IN_DELETE |
                                 IN_MOVE_SELF | IN_DELETE_SELF);
  if (wd < 0) {
    tlg::warning << warningMessage << std::endl;
    return;
  }

  std::thread watchThread{[fd, this]() {
    std::array<char, WATCH_BUFFER_SIZE> buffer{};
    while (true) {
      bool reloadSettings = false;
      bool allEventsRead = false;

      while (!allEventsRead) {
        const std::size_t length = read(fd, buffer.data(), buffer.size());

        std::this_thread::sleep_for(std::chrono::milliseconds(100));

        unsigned int available = 0;
        ioctl(fd, FIONREAD, &available);  // NOLINT

        reloadSettings = (length > 0);
        allEventsRead = (available <= 0);
      }

      if (reloadSettings) {
        tlg::info << "Your configuration file changed, reloading your settings"
                  << std::endl;
        this->config->clear();
        this->parseConfig();
      }
    }
  }};
  watchThread.detach();
}
