/*
 * This file is part of smolbote. It's copyrighted by the contributors recorded
 * in the version control history of the file, available from its original
 * location: git://neueland.iserlohn-fortress.net/smolbote.git
 *
 * SPDX-License-Identifier: GPL-3.0
 */

#include "configuration.h"
#include <libconfig.h++>
#include <sstream>

using namespace libconfig;

Configuration::Configuration()
{
    m_userCfg = new Config();
#ifdef C_LIKE_CONFIG
    m_userCfg->setOptions(Config::OptionSemicolonSeparators | Config::OptionOpenBraceOnSeparateLine);
#endif
    // fsync after writing and before closing
    m_userCfg->setOption(Config::OptionFsync, true);

    m_defaultCfg = new Config();
}

Configuration::~Configuration()
{
    if(!m_userCfgPath.empty()) {
        m_userCfg->writeFile(m_userCfgPath.c_str());
    }

    delete m_userCfg;
    delete m_defaultCfg;
}

bool Configuration::readUserConfiguration(const std::string &path)
{
    m_userCfgPath = path;
    try {
        m_userCfg->readFile(path.c_str());
    } catch (FileIOException) {
        return false;
    } catch (ParseException) {
        return false;
    }
    return  true;
}

bool Configuration::writeUserConfiguration(const std::string &path)
{
    m_userCfgPath = path;
    try {
        m_userCfg->writeFile(path.c_str());
    } catch (FileIOException) {
        return false;
    }
    return true;
}

bool Configuration::readDefaultConfiguration(const std::string &data)
{
    try {
        m_defaultCfg->readString(data);
    } catch(ParseException) {
        return false;
    }
    return true;
}

std::vector<std::string> Configuration::childrenSettings(const char* name)
{
    std::vector<std::string> groupNames;
    const Setting &root = m_defaultCfg->lookup(name);

    for(const Setting &setting : root) {
        if(setting.getType() != Setting::TypeGroup) {
            groupNames.push_back(setting.getName());
        }
    }

    return groupNames;
}

std::vector<std::string> Configuration::childrenGroups(const char* name)
{
    std::vector<std::string> groupNames;
    const Setting &root = m_defaultCfg->lookup(name);

    for(const Setting &setting : root) {
        if(setting.getType() == Setting::TypeGroup) {
            groupNames.push_back(setting.getName());
        }
    }
    return groupNames;
}

template<typename T>
std::optional<T> Configuration::value(const char* path) const
{
    Config *conf = getConfig(path);
    if(conf == nullptr) {
        return std::nullopt;
    }

    // setting was found
    const Setting &setting = conf->lookup(path);
    std::optional<T> r;

    // cast depending on type
    // type checks are done during compile time
    switch (setting.getType()) {
    case Setting::TypeNone:
        r = std::nullopt;
        break;

    case Setting::TypeInt:
        // int, unsigned int, long, unsigned long
        if constexpr(std::is_same_v<T, std::string>) {
            r = std::optional<std::string>(std::to_string(static_cast<int>(setting)));
        } else if constexpr(std::is_same_v<T, int> || std::is_same_v<T, unsigned int> || std::is_same_v<T, long> || std::is_same_v<T, unsigned long>) {
            r = std::optional<T>(static_cast<T>(setting));
        } else {
            r = std::nullopt;
        }
        break;

    case Setting::TypeInt64:
        // int, unsigned int; long long, unsigned long long
        if constexpr(std::is_same_v<T, std::string>) {
            r = std::optional<std::string>(std::to_string(static_cast<long long>(setting)));
        } else if constexpr(std::is_same_v<T, int> || std::is_same_v<T, unsigned int> || std::is_same_v<T, long long> || std::is_same_v<T, unsigned long long>) {
            r = std::optional<T>(static_cast<T>(setting));
        } else {
            r = std::nullopt;
        }
        break;

    case Setting::TypeFloat:
        // float, double
        if constexpr(std::is_same_v<T, std::string>) {
            r = std::optional<std::string>(std::to_string(static_cast<float>(setting)));
        } else if constexpr(std::is_same_v<T, float> || std::is_same_v<T, double>) {
            r = std::optional<T>(static_cast<T>(setting));
        } else {
            r = std::nullopt;
        }
        break;

    case Setting::TypeString:
        // const char*, std::string
        if constexpr(std::is_same<T, std::string>::value) {
            r = std::optional<std::string>(static_cast<const char*>(setting));
        } else {
            r = std::nullopt;
        }
        break;

    case Setting::TypeBoolean:
        // bool
        if constexpr(std::is_same<T, std::string>::value) {
            r = std::optional<std::string>(static_cast<bool>(setting) ? "true" : "false");
        } else if constexpr(std::is_same<T, bool>::value) {
            r = std::optional<bool>(static_cast<bool>(setting));
        } else {
            r = std::nullopt;
        }
        break;

    case Setting::TypeGroup:
        r = std::nullopt;
        break;

    case Setting::TypeArray:
        r = std::nullopt;
        break;

    case Setting::TypeList:
        r = std::nullopt;
        break;
    }

    return r;
}

// tell the compiler to export these functions, otherwise you get linking errors
template std::optional<int> Configuration::value<int>(const char* path) const;
template std::optional<unsigned int> Configuration::value<unsigned int>(const char* path) const;
template std::optional<long> Configuration::value<long>(const char* path) const;
template std::optional<unsigned long> Configuration::value<unsigned long>(const char* path) const;

template std::optional<long long> Configuration::value<long long>(const char* path) const;
template std::optional<unsigned long long> Configuration::value<unsigned long long>(const char* path) const;

template std::optional<float> Configuration::value<float>(const char* path) const;
template std::optional<double> Configuration::value<double>(const char* path) const;

template std::optional<bool> Configuration::value<bool>(const char* path) const;

template std::optional<std::string> Configuration::value<std::string>(const char* path) const;

template<typename T>
void Configuration::setValue(std::string path, const T &val)
{
    if(m_userCfg->exists(path)) {
        Setting &setting = m_userCfg->lookup(path);
        // compiler complained about operator= not taking unsinged ints, longs and long longs
        if constexpr(std::is_unsigned_v<T> && !std::is_same_v<T, bool>) {
            setting = static_cast<typename std::make_signed_t<T>>(val);
        } else {
            setting = val;
        }
        return;
    }

    // the entry doesn't exist, so we need to create it
    // libconfig can't create an entry from a path, we need to get its root, and then add it

    // this will error out if entry being added is not in the default config

    std::istringstream p(path);
    Setting *userSetting = &m_userCfg->getRoot();
    Setting *defaultSetting = &m_defaultCfg->getRoot();

    std::string i;
    while (std::getline(p, i, '.')) {
        defaultSetting = &defaultSetting->lookup(i);
        // check if user setting exists, if not, create it
        if(!userSetting->exists(i)) {
            userSetting = &userSetting->add(i, defaultSetting->getType());
        }
    }

    if constexpr(std::is_unsigned_v<T> && !std::is_same_v<T, bool>) {
        *userSetting = static_cast<typename std::make_signed_t<T>>(val);
    } else {
        *userSetting = val;
    }

}

template void Configuration::setValue<int>(std::string path, const int &val);
template void Configuration::setValue<unsigned int>(std::string path, const unsigned int &val);
template void Configuration::setValue<long>(std::string path, const long &val);
template void Configuration::setValue<unsigned long>(std::string path, const unsigned long &val);

template void Configuration::setValue<long long>(std::string path, const long long &val);
template void Configuration::setValue<unsigned long long>(std::string path, const unsigned long long &val);

template void Configuration::setValue<float>(std::string path, const float &val);
template void Configuration::setValue<double>(std::string path, const double &val);

template void Configuration::setValue<bool>(std::string path, const bool &val);

template void Configuration::setValue<std::string>(std::string path, const std::string &val);

Config *Configuration::getConfig(const char* path) const
{
    if(m_userCfg->exists(path)) {
        return m_userCfg;
    } else if(m_defaultCfg->exists(path)) {
        return m_defaultCfg;
    } else {
        return nullptr;
    }
}

std::string &patchHome(std::string &path, const std::string &home)
{
    const size_t location = path.find("~");
    if(location != std::string::npos) {
        return path.replace(location, 1, home);
    }
    return path;
}