/* Configuration file parsing.
 *
 * Author: Steffen Vogel <post@steffenvogel.de>
 * SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University
 * SPDX-License-Identifier: Apache-2.0
 */

#include <glob.h>
#include <libgen.h>
#include <linux/limits.h>
#include <unistd.h>

#include <fstream>
#include <iomanip>
#include <iostream>
#include <string>

#include <villas/boxes.hpp>
#include <villas/config_class.hpp>
#include <villas/config_helper.hpp>
#include <villas/log.hpp>
#include <villas/node/config.hpp>
#include <villas/node/exceptions.hpp>
#include <villas/utils.hpp>

#ifdef WITH_CONFIG
#include <libconfig.h>
#endif

using namespace villas;
using namespace villas::node;

Config::Config() : logger(logging.get("config")), root(nullptr) {}

Config::Config(const std::string &u) : Config() { root = load(u); }

Config::~Config() { json_decref(root); }

json_t *Config::load(std::FILE *f, bool resolveInc, bool resolveEnvVars) {
  json_t *root = decode(f);

  if (resolveInc) {
    json_t *root_old = root;
    root = expandIncludes(root);
    json_decref(root_old);
  }

  if (resolveEnvVars) {
    json_t *root_old = root;
    root = expandEnvVars(root);
    json_decref(root_old);
  }

  return root;
}

json_t *Config::load(const std::string &u, bool resolveInc,
                     bool resolveEnvVars) {
  FILE *f;

  if (u == "-")
    f = loadFromStdio();
  else
    f = loadFromLocalFile(u);

  json_t *root = load(f, resolveInc, resolveEnvVars);

  fclose(f);

  return root;
}

FILE *Config::loadFromStdio() {
  logger->info("Reading configuration from standard input");

  auto *cwd = new char[PATH_MAX];

  configPath = getcwd(cwd, PATH_MAX);

  delete[] cwd;

  return stdin;
}

FILE *Config::loadFromLocalFile(const std::string &u) {
  logger->info("Reading configuration from local file: {}", u);

  configPath = u;
  FILE *f = fopen(u.c_str(), "r");
  if (!f)
    throw RuntimeError("Failed to open configuration from: {}", u);

  return f;
}

json_t *Config::decode(FILE *f) {
  json_error_t err;

  // Update list of include directories
  auto incDirs = getIncludeDirectories(f);
  includeDirectories.insert(includeDirectories.end(), incDirs.begin(),
                            incDirs.end());

  json_t *root = json_loadf(f, 0, &err);
  if (root == nullptr) {
#ifdef WITH_CONFIG
    // We try again to parse the config in the legacy format
    root = libconfigDecode(f);
#else
    throw JanssonParseError(err);
#endif // WITH_CONFIG
  }

  return root;
}

std::list<std::string> Config::getIncludeDirectories(FILE *f) const {
  int ret, fd;
  char buf[PATH_MAX];
  char *dir;

  std::list<std::string> dirs;

  // Adding directory of base configuration file
  fd = fileno(f);
  if (fd < 0)
    throw SystemError("Failed to get file descriptor");

  auto path = fmt::format("/proc/self/fd/{}", fd);

  ret = readlink(path.c_str(), buf, sizeof(buf));
  if (ret > 0) {
    buf[ret] = 0;
    if (isLocalFile(buf)) {
      dir = dirname(buf);
      dirs.push_back(dir);
    }
  }

  // Adding current working directory
  dir = getcwd(buf, sizeof(buf));
  if (dir != nullptr)
    dirs.push_back(dir);

  return dirs;
}

std::list<std::string> Config::resolveIncludes(const std::string &n) {
  glob_t gb;
  int ret, flags = 0;

  memset(&gb, 0, sizeof(gb));

  auto name = n;
  resolveEnvVars(name);

  if (name.size() >= 1 && name[0] == '/') { // absolute path
    ret = glob(name.c_str(), flags, nullptr, &gb);
    if (ret && ret != GLOB_NOMATCH)
      gb.gl_pathc = 0;
  } else { // relative path
    for (auto &dir : includeDirectories) {
      auto pattern = fmt::format("{}/{}", dir, name.c_str());

      ret = glob(pattern.c_str(), flags, nullptr, &gb);
      if (ret && ret != GLOB_NOMATCH) {
        gb.gl_pathc = 0;

        goto out;
      }

      flags |= GLOB_APPEND;
    }
  }

out:
  std::list<std::string> files;
  for (unsigned i = 0; i < gb.gl_pathc; i++)
    files.push_back(gb.gl_pathv[i]);

  globfree(&gb);

  return files;
}

void Config::resolveEnvVars(std::string &text) {
  static const std::regex env_re{R"--(\$\{([^}]+)\})--"};

  std::smatch match;
  while (std::regex_search(text, match, env_re)) {
    auto const from = match[0];
    auto const var_name = match[1].str().c_str();
    char *var_value = std::getenv(var_name);
    if (!var_value)
      throw RuntimeError("Unresolved environment variable: {}", var_name);

    text.replace(from.first - text.begin(), from.second - from.first,
                 var_value);

    logger->debug("Replace env var {} in \"{}\" with value \"{}\"", var_name,
                  text, var_value);
  }
}

#ifdef WITH_CONFIG
#if (LIBCONFIG_VER_MAJOR > 1) ||                                               \
    ((LIBCONFIG_VER_MAJOR == 1) && (LIBCONFIG_VER_MINOR >= 7))
const char **Config::includeFuncStub(config_t *cfg, const char *include_dir,
                                     const char *path, const char **error) {
  void *ctx = config_get_hook(cfg);

  return reinterpret_cast<Config *>(ctx)->includeFunc(cfg, include_dir, path,
                                                      error);
}

const char **Config::includeFunc(config_t *cfg, const char *include_dir,
                                 const char *path, const char **error) {
  auto paths = resolveIncludes(path);

  unsigned i = 0;
  auto files = (const char **)malloc(sizeof(char **) * (paths.size() + 1));

  for (auto &path : paths)
    files[i++] = strdup(path.c_str());

  files[i] = NULL;

  return files;
}
#endif

json_t *Config::libconfigDecode(FILE *f) {
  int ret;

  config_t cfg;
  config_setting_t *cfg_root;
  config_init(&cfg);
  config_set_auto_convert(&cfg, 1);

  // Setup libconfig include path
#if (LIBCONFIG_VER_MAJOR > 1) ||                                               \
    ((LIBCONFIG_VER_MAJOR == 1) && (LIBCONFIG_VER_MINOR >= 7))
  config_set_hook(&cfg, this);

  config_set_include_func(&cfg, includeFuncStub);
#else
  if (includeDirectories.size() > 0) {
    logger->info("Setting include dir to: {}", includeDirectories.front());

    config_set_include_dir(&cfg, includeDirectories.front().c_str());

    if (includeDirectories.size() > 1) {
      logger->warn(
          "Ignoring all but the first include directories for libconfig");
      logger->warn(
          "  libconfig does not support more than a single include dir!");
    }
  }
#endif

  // Rewind before re-reading
  rewind(f);

  ret = config_read(&cfg, f);
  if (ret != CONFIG_TRUE)
    throw LibconfigParseError(&cfg);

  cfg_root = config_root_setting(&cfg);

  json_t *root = config_to_json(cfg_root);
  if (!root)
    throw RuntimeError("Failed to convert JSON to configuration file");

  config_destroy(&cfg);

  return root;
}
#endif // WITH_CONFIG

json_t *Config::walkStrings(json_t *root, str_walk_fcn_t cb) {
  const char *key;
  size_t index;
  json_t *val, *new_val, *new_root;

  switch (json_typeof(root)) {
  case JSON_STRING:
    return cb(root);

  case JSON_OBJECT:
    new_root = json_object();

    json_object_foreach(root, key, val) {
      new_val = walkStrings(val, cb);

      json_object_set_new(new_root, key, new_val);
    }

    return new_root;

  case JSON_ARRAY:
    new_root = json_array();

    json_array_foreach(root, index, val) {
      new_val = walkStrings(val, cb);

      json_array_append_new(new_root, new_val);
    }

    return new_root;

  default:
    return json_incref(root);
  };
}

json_t *Config::expandEnvVars(json_t *in) {
  return walkStrings(in, [this](json_t *str) -> json_t * {
    std::string text = json_string_value(str);

    resolveEnvVars(text);

    return json_string(text.c_str());
  });
}

json_t *Config::expandIncludes(json_t *in) {
  return walkStrings(in, [this](json_t *str) -> json_t * {
    int ret;
    std::string text = json_string_value(str);
    static const std::string kw = "@include ";

    if (text.find(kw) != 0)
      return json_incref(str);
    else {
      std::string pattern = text.substr(kw.size());

      resolveEnvVars(pattern);

      json_t *incl = nullptr;

      for (auto &path : resolveIncludes(pattern)) {
        json_t *other = load(path);
        if (!other)
          throw ConfigError(str, "Failed to include config file from {}", path);

        if (!incl)
          incl = other;
        else if (json_is_object(incl) && json_is_object(other)) {
          ret = json_object_update_recursive(incl, other);
          if (ret)
            throw ConfigError(
                str, "Can not mix object and array-typed include files");
        } else if (json_is_array(incl) && json_is_array(other)) {
          ret = json_array_extend(incl, other);
          if (ret)
            throw ConfigError(
                str, "Can not mix object and array-typed include files");
        }

        logger->debug("Included config from: {}", path);
      }

      return incl;
    }
  });
}
