/*
 * 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: https://neueland.iserlohn-fortress.net/smolbote.hg
 *
 * SPDX-License-Identifier: GPL-3.0
 */

#include "configuration.h"
#include <QtCore/QStandardPaths>
#include <sstream>

using namespace libconfig;

Configuration::Configuration(const std::string &path, const std::string &home)
{
    m_userCfg = new Config();
    // prettier output
    m_userCfg->setOptions(Config::OptionSemicolonSeparators | Config::OptionOpenBraceOnSeparateLine);
    m_userCfg->setOption(Config::OptionFsync, true);

    m_userCfgPath = path;
    m_homePath = home;

    m_defaultCfg = new Config();
}

Configuration::~Configuration()
{
    delete m_userCfg;
    delete m_defaultCfg;
}

bool Configuration::read()
{
    try {
        m_userCfg->readFile(m_userCfgPath.c_str());
    } catch(const FileIOException &e) {
        return false;
    } catch(const ParseException &e) {
        return false;
    }
    return true;
}

bool Configuration::parse(const std::string &contents)
{
    try {
        m_userCfg->readString(contents);
    } catch(const ParseException &e) {
        return false;
    }
    return true;
}

bool Configuration::writeIfNeeded(const std::string &path)
{
    if(!path.empty())
        m_userCfgPath = path;

    if(!changed)
        return true;

    try {
        m_userCfg->writeFile(m_userCfgPath.c_str());
    } catch(const FileIOException &e) {
        return false;
    }

    changed = false;
    return true;
}

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

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

    for(const Setting &setting : root) {
        if(setting.getType() != Setting::TypeGroup) {
            groupNames.emplace_back(setting.getName());
            //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_userCfg->lookup(name);

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

void Configuration::resetValue(const char *path)
{
    if(!m_userCfg->exists(path) && m_defaultCfg->exists(path)) {
        // entry doesn't exist, create path to it
        // split path into array of items, sep=.
        std::string s(path);
        std::vector<std::string> pathTokens;
        std::string::size_type prev_pos = 0, pos = 0;
        while((pos = s.find('.', pos)) != std::string::npos) {
            std::string substring(s.substr(prev_pos, pos - prev_pos));
            pathTokens.push_back(substring);
            prev_pos = ++pos;
        }
        pathTokens.push_back(s.substr(prev_pos, pos - prev_pos)); // Last word

        s.clear();
        // iterate through items, creating the path along the way
        for(const std::string &t : pathTokens) {
            Setting &v = m_userCfg->lookup(s);
            Setting &d = m_defaultCfg->lookup(s).lookup(t);

            if(!v.exists(t)) {
                v.add(t, d.getType());
            }

            if(s.empty())
                s.append(t);
            else
                s.append('.' + t);
        }
    }

    // if the path doesn't exist in the defaultCfg, this will crash

    // entry exists, reset it
    Setting &v = m_userCfg->lookup(path);
    switch(v.getType()) {
    case Setting::TypeNone:
        break;
    case Setting::TypeInt:
        v = static_cast<int>(m_defaultCfg->lookup(path));
        break;
    case Setting::TypeInt64:
        v = static_cast<long>(m_defaultCfg->lookup(path));
        break;
    case Setting::TypeFloat:
        v = static_cast<double>(m_defaultCfg->lookup(path));
        break;
    case Setting::TypeString:
        v = static_cast<const char *>(m_defaultCfg->lookup(path));
        break;
    case Setting::TypeBoolean:
        v = static_cast<bool>(m_defaultCfg->lookup(path));
        break;
    case Setting::TypeGroup:
        break;
    case Setting::TypeArray:
        break;
    case Setting::TypeList:
        break;
    }

    // if it's a string, it may contain a path
    if(v.getType() == Setting::TypeString) {
        std::string val(static_cast<const char *>(v));
        v = patchHome(val, m_homePath).c_str();
    }

    changed = true;
}

std::string Configuration::castToString(const libconfig::Setting &v) const
{
    // cast depending on type
    // type checks are done during compile time
    switch(v.getType()) {
    case Setting::TypeNone:
        return std::string();

    case Setting::TypeInt:
        // int, unsigned int, long, unsigned long
        return std::to_string(static_cast<int32_t>(v));

    case Setting::TypeInt64:
        // int, unsigned int; long long, unsigned long long
        return std::to_string(static_cast<int64_t>(v));

    case Setting::TypeFloat:
        // float, double
        return std::to_string(static_cast<double>(v));

    case Setting::TypeString:
        // const char*, std::string
        return std::string(static_cast<const char *>(v));

    case Setting::TypeBoolean:
        // bool
        return std::string(static_cast<bool>(v) ? "true" : "false");

    case Setting::TypeGroup:
    case Setting::TypeArray:
    case Setting::TypeList:
        return std::string();
    }
}

template <typename T>
bool 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 if constexpr(std::is_same_v<T, std::string>) {
            switch(setting.getType()) {
            case Setting::TypeNone:
                break;

            case Setting::TypeInt:
            case Setting::TypeInt64:
                setting = std::stoi(static_cast<std::string>(val));
                break;

            case Setting::TypeFloat:
                setting = std::stod(static_cast<std::string>(val));
                break;

            case Setting::TypeString:
                setting = static_cast<std::string>(val).c_str();
                break;

            case Setting::TypeBoolean:
                if(static_cast<std::string>(val) == "true") {
                    setting = true;
                } else if(static_cast<std::string>(val) == "false") {
                    setting = false;
                }
                break;

            case Setting::TypeGroup:
                break;
            case Setting::TypeArray:
                break;
            case Setting::TypeList:
                break;
            }

        } else {
            setting = val;
        }

        changed = true;
        return true;
    }

    // the entry doesn't exist
    return false;
}

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

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

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

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

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

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