/*
 * Copyright (c) Souldbminer, Lightos_ and Horizon OC Contributors
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/* --------------------------------------------------------------------------
 * "THE BEER-WARE LICENSE" (Revision 42):
 * <p-sam@d3vs.net>, <natinusala@gmail.com>, <m4x@m4xw.net>
 * wrote this file. As long as you retain this notice you can do whatever you
 * want with this stuff. If you meet any of us some day, and you think this
 * stuff is worth it, you can buy us a beer in return.  - The sys-clk authors
 * --------------------------------------------------------------------------
 */

#include "config.hpp"
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sstream>
#include <algorithm>
#include <cstring>
#include <ctime>
#include <map>
#include <string>
#include <atomic>
#include <initializer_list>
#include <minIni.h>
#include "../hos/apm_ext.h"
#include <i2c.h>
#include <t210.h>
#include <max17050.h>
#include <tmp451.h>
#include <ipc_server.h>
#include <lockable_mutex.h>
#include "../board/board.hpp"
#include "errors.hpp"
#include "file_utils.hpp"

namespace config {

    uint64_t configValues[HocClkConfigValue_EnumMax];

    namespace {

        bool gLoaded = false;
        std::string gPath;
        time_t gMtime = 0;
        std::atomic_bool gEnabled{false};
        std::uint32_t gOverrideFreqs[HocClkModule_EnumMax];
        std::map<std::tuple<std::uint64_t, HocClkProfile, HocClkModule>, std::uint32_t> gProfileMHzMap;
        std::map<std::uint64_t, std::uint8_t> gProfileCountMap;
        LockableMutex gConfigMutex;
        LockableMutex gOverrideMutex;

        time_t CheckModificationTime() {
            time_t mtime = 0;
            struct stat st;
            if (stat(gPath.c_str(), &st) == 0) {
                mtime = st.st_mtime;
            }
            return mtime;
        }

        std::uint32_t FindClockMHz(std::uint64_t tid, HocClkModule module, HocClkProfile profile) {
            if (gLoaded) {
                auto it = gProfileMHzMap.find(std::make_tuple(tid, profile, module));
                if (it != gProfileMHzMap.end()) {
                    return it->second;
                }
            }
            return 0;
        }

        std::uint32_t FindClockHzFromProfiles(std::uint64_t tid, HocClkModule module, std::initializer_list<HocClkProfile> profiles, u32 mhzMultiplier = 1000000) {
            std::uint32_t mhz = 0;

            if (gLoaded) {
                for (auto profile: profiles) {
                    mhz = FindClockMHz(tid, module, profile);
                    if (mhz) {
                        break;
                    }
                }
            }

            return std::max((std::uint32_t)0, mhz * mhzMultiplier);
        }

        int BrowseIniFunc(const char* section, const char* key, const char* value, void* userdata) {
            (void)userdata;
            std::uint64_t input;
            if (!strcmp(section, CONFIG_VAL_SECTION)) {
                for (unsigned int kval = 0; kval < HocClkConfigValue_EnumMax; kval++) {
                    if (!strcmp(key, hocclkFormatConfigValue((HocClkConfigValue)kval, false))) {
                        input = strtoul(value, NULL, 0);
                        if (!hocclkValidConfigValue((HocClkConfigValue)kval, input)) {
                            input = hocclkDefaultConfigValue((HocClkConfigValue)kval);
                            fileUtils::LogLine("[cfg] Invalid value for key '%s' in section '%s': using default %d", key, section, input);
                        }
                        configValues[kval] = input;
                        return 1;
                    }
                }

                fileUtils::LogLine("[cfg] Skipping key '%s' in section '%s': Unrecognized config value", key, section);
                return 1;
            }

            std::uint64_t tid = strtoul(section, NULL, 16);

            if (!tid || strlen(section) != 16) {
                fileUtils::LogLine("[cfg] Skipping key '%s' in section '%s': Invalid TitleID", key, section);
                return 1;
            }

            HocClkProfile parsedProfile = HocClkProfile_EnumMax;
            HocClkModule parsedModule = HocClkModule_EnumMax;

            for (unsigned int profile = 0; profile < HocClkProfile_EnumMax; profile++) {
                const char* profileCode = board::GetProfileName((HocClkProfile)profile, false);
                size_t profileCodeLen = strlen(profileCode);

                if (!strncmp(key, profileCode, profileCodeLen) && key[profileCodeLen] == '_') {
                    const char* subkey = key + profileCodeLen + 1;

                    for (unsigned int module = 0; module < HocClkModule_EnumMax; module++) {
                        const char* moduleCode = board::GetModuleName((HocClkModule)module, false);
                        size_t moduleCodeLen = strlen(moduleCode);
                        if (!strncmp(subkey, moduleCode, moduleCodeLen) && subkey[moduleCodeLen] == '\0') {
                            parsedProfile = (HocClkProfile)profile;
                            parsedModule = (HocClkModule)module;
                        }
                    }
                }
            }

            if (parsedModule == HocClkModule_EnumMax || parsedProfile == HocClkProfile_EnumMax) {
                fileUtils::LogLine("[cfg] Skipping key '%s' in section '%s': Unrecognized key", key, section);
                return 1;
            }

            std::uint32_t mhz = strtoul(value, NULL, 10);
            if (!mhz) {
                fileUtils::LogLine("[cfg] Skipping key '%s' in section '%s': Invalid value", key, section);
                return 1;
            }

            gProfileMHzMap[std::make_tuple(tid, parsedProfile, parsedModule)] = mhz;
            auto it = gProfileCountMap.find(tid);
            if (it == gProfileCountMap.end()) {
                gProfileCountMap[tid] = 1;
            } else {
                it->second++;
            }

            return 1;
        }

        void Close() {
            gLoaded = false;
            gProfileMHzMap.clear();
            gProfileCountMap.clear();
            for (unsigned int i = 0; i < HocClkConfigValue_EnumMax; i++) {
                configValues[i] = hocclkDefaultConfigValue((HocClkConfigValue)i);
            }
        }

        void Load() {
            fileUtils::LogLine("[cfg] Reading %s", gPath.c_str());

            Close();
            gMtime = CheckModificationTime();
            if (!gMtime) {
                fileUtils::LogLine("[cfg] Error finding file");
            } else if (!ini_browse(&BrowseIniFunc, nullptr, gPath.c_str())) {
                fileUtils::LogLine("[cfg] Error loading file");
            }

            gLoaded = true;
        }

    }

    void Initialize() {
        gPath = FILE_CONFIG_DIR "/config.ini";
        gLoaded = false;
        gProfileMHzMap.clear();
        gProfileCountMap.clear();
        gMtime = 0;
        gEnabled = false;
        for (unsigned int i = 0; i < HocClkModule_EnumMax; i++) {
            gOverrideFreqs[i] = 0;
        }
        for (unsigned int i = 0; i < HocClkConfigValue_EnumMax; i++) {
            configValues[i] = hocclkDefaultConfigValue((HocClkConfigValue)i);
        }
    }

    void Exit() {
        std::scoped_lock lock{gConfigMutex};
        Close();
    }

    bool Refresh() {
        std::scoped_lock lock{gConfigMutex};
        if (!gLoaded || gMtime != CheckModificationTime()) {
            Load();
            return true;
        }
        return false;
    }

    bool HasProfilesLoaded() {
        std::scoped_lock lock{gConfigMutex};
        return gLoaded;
    }

    std::uint32_t GetAutoClockHz(std::uint64_t tid, HocClkModule module, HocClkProfile profile, bool returnRaw) {
        std::scoped_lock lock{gConfigMutex};
        switch (profile) {
            case HocClkProfile_Handheld:
                return FindClockHzFromProfiles(tid, module, {HocClkProfile_Handheld}, returnRaw ? 1 : 1000000);
            case HocClkProfile_HandheldCharging:
            case HocClkProfile_HandheldChargingUSB:
                return FindClockHzFromProfiles(tid, module, {HocClkProfile_HandheldChargingUSB, HocClkProfile_HandheldCharging, HocClkProfile_Handheld}, returnRaw ? 1 : 1000000);
            case HocClkProfile_HandheldChargingOfficial:
                return FindClockHzFromProfiles(tid, module, {HocClkProfile_HandheldChargingOfficial, HocClkProfile_HandheldCharging, HocClkProfile_Handheld}, returnRaw ? 1 : 1000000);
            case HocClkProfile_Docked:
                return FindClockHzFromProfiles(tid, module, {HocClkProfile_Docked}, returnRaw ? 1 : 1000000);
            default:
                ERROR_THROW("Unhandled HocClkProfile: %u", profile);
        }
        return 0;
    }

    void GetProfiles(std::uint64_t tid, HocClkTitleProfileList* out_profiles) {
        std::scoped_lock lock{gConfigMutex};
        for (unsigned int profile = 0; profile < HocClkProfile_EnumMax; profile++) {
            for (unsigned int module = 0; module < HocClkModule_EnumMax; module++) {
                out_profiles->mhzMap[profile][module] = FindClockMHz(tid, (HocClkModule)module, (HocClkProfile)profile);
            }
        }
    }

    bool SetProfiles(std::uint64_t tid, HocClkTitleProfileList* profiles, bool immediate) {
        std::scoped_lock lock{gConfigMutex};
        uint8_t numProfiles = 0;

        char section[17] = {0};
        snprintf(section, sizeof(section), "%016lX", tid);

        std::vector<std::string> keys;
        std::vector<std::string> values;
        keys.reserve(+HocClkProfile_EnumMax * +HocClkModule_EnumMax);
        values.reserve(+HocClkProfile_EnumMax * +HocClkModule_EnumMax);

        std::uint32_t* mhz = &profiles->mhz[0];

        for (unsigned int profile = 0; profile < HocClkProfile_EnumMax; profile++) {
            for (unsigned int module = 0; module < HocClkModule_EnumMax; module++) {
                if (*mhz) {
                    numProfiles++;

                    std::string key = std::string(board::GetProfileName((HocClkProfile)profile, false)) +
                                      "_" +
                                      board::GetModuleName((HocClkModule)module, false);
                    std::string value = std::to_string(*mhz);

                    keys.push_back(key);
                    values.push_back(value);
                }
                mhz++;
            }
        }

        std::vector<const char*> keyPointers;
        std::vector<const char*> valuePointers;
        keyPointers.reserve(keys.size() + 1);
        valuePointers.reserve(values.size() + 1);

        for (size_t i = 0; i < keys.size(); i++) {
            keyPointers.push_back(keys[i].c_str());
            valuePointers.push_back(values[i].c_str());
        }
        keyPointers.push_back(NULL);
        valuePointers.push_back(NULL);

        if (!ini_putsection(section, keyPointers.data(), valuePointers.data(), gPath.c_str())) {
            return false;
        }

        if (immediate) {
            mhz = &profiles->mhz[0];
            gProfileCountMap[tid] = numProfiles;
            for (unsigned int profile = 0; profile < HocClkProfile_EnumMax; profile++) {
                for (unsigned int module = 0; module < HocClkModule_EnumMax; module++) {
                    if (*mhz) {
                        gProfileMHzMap[std::make_tuple(tid, (HocClkProfile)profile, (HocClkModule)module)] = *mhz;
                    } else {
                        gProfileMHzMap.erase(std::make_tuple(tid, (HocClkProfile)profile, (HocClkModule)module));
                    }
                    mhz++;
                }
            }
        }

        return true;
    }

    std::uint8_t GetProfileCount(std::uint64_t tid) {
        auto it = gProfileCountMap.find(tid);
        if (it == gProfileCountMap.end()) {
            return 0;
        }
        return it->second;
    }

    void SetEnabled(bool enabled) {
        gEnabled = enabled;
    }

    bool Enabled() {
        return gEnabled;
    }

    void SetOverrideHz(HocClkModule module, std::uint32_t hz) {
        ASSERT_ENUM_VALID(HocClkModule, module);
        std::scoped_lock lock{gOverrideMutex};
        gOverrideFreqs[module] = hz;
    }

    std::uint32_t GetOverrideHz(HocClkModule module) {
        ASSERT_ENUM_VALID(HocClkModule, module);
        std::scoped_lock lock{gOverrideMutex};
        return gOverrideFreqs[module];
    }

    std::uint64_t GetConfigValue(HocClkConfigValue kval) {
        ASSERT_ENUM_VALID(HocClkConfigValue, kval);
        std::scoped_lock lock{gConfigMutex};
        return configValues[kval];
    }

    const char* GetConfigValueName(HocClkConfigValue kval, bool pretty) {
        ASSERT_ENUM_VALID(HocClkConfigValue, kval);
        return hocclkFormatConfigValue(kval, pretty);
    }

    void GetConfigValues(HocClkConfigValueList* out_configValues) {
        std::scoped_lock lock{gConfigMutex};
        for (unsigned int kval = 0; kval < HocClkConfigValue_EnumMax; kval++) {
            out_configValues->values[kval] = configValues[kval];
        }
    }

    bool SetConfigValues(HocClkConfigValueList* configValues, bool immediate) {
        std::scoped_lock lock{gConfigMutex};

        std::vector<const char*> iniKeys;
        std::vector<std::string> iniValues;
        iniKeys.reserve(HocClkConfigValue_EnumMax + 1);
        iniValues.reserve(HocClkConfigValue_EnumMax);

        for (unsigned int kval = 0; kval < HocClkConfigValue_EnumMax; kval++) {
            if (!hocclkValidConfigValue((HocClkConfigValue)kval, configValues->values[kval]) ||
               configValues->values[kval] == hocclkDefaultConfigValue((HocClkConfigValue)kval)) {
                continue;
            }
            iniValues.push_back(std::to_string(configValues->values[kval]));
            iniKeys.push_back(hocclkFormatConfigValue((HocClkConfigValue)kval, false));
        }

        iniKeys.push_back(NULL);

        std::vector<const char*> valuePointers;
        valuePointers.reserve(iniValues.size() + 1);
        for (const auto& val : iniValues) {
            valuePointers.push_back(val.c_str());
        }
        valuePointers.push_back(NULL);

        if (!ini_putsection(CONFIG_VAL_SECTION, iniKeys.data(), valuePointers.data(), gPath.c_str())) {
            return false;
        }

        if (immediate) {
            for (unsigned int kval = 0; kval < HocClkConfigValue_EnumMax; kval++) {
                if (hocclkValidConfigValue((HocClkConfigValue)kval, configValues->values[kval])) {
                    config::configValues[kval] = configValues->values[kval];
                } else {
                    config::configValues[kval] = hocclkDefaultConfigValue((HocClkConfigValue)kval);
                }
            }
        }

        return true;
    }

    bool ResetConfigValue(HocClkConfigValue kval) {
        if (!HOCCLK_ENUM_VALID(HocClkConfigValue, kval)) {
            fileUtils::LogLine("[cfg] Invalid HocClkConfigValue: %u", kval);
            return false;
        }

        std::scoped_lock lock{gConfigMutex};

        std::uint64_t defaultValue = hocclkDefaultConfigValue(kval);

        std::vector<const char*> iniKeys;
        std::vector<std::string> iniValues;
        iniKeys.reserve(2);
        iniValues.reserve(1);

        iniKeys.push_back(hocclkFormatConfigValue(kval, false));
        iniValues.push_back("");
        iniKeys.push_back(NULL);

        std::vector<const char*> valuePointers;
        valuePointers.reserve(iniValues.size() + 1);
        for (const auto& val : iniValues) {
            valuePointers.push_back(val.c_str());
        }
        valuePointers.push_back(NULL);

        if (!ini_putsection(CONFIG_VAL_SECTION, iniKeys.data(), valuePointers.data(), gPath.c_str())) {
            fileUtils::LogLine("[cfg] Failed to reset config value %u in INI", kval);
            return false;
        }

        configValues[kval] = defaultValue;
        fileUtils::LogLine("[cfg] Reset config value %u to default: %llu", kval, defaultValue);

        return true;
    }

    bool SetConfigValue(HocClkConfigValue kval, std::uint64_t value, bool immediate) {
        if (!HOCCLK_ENUM_VALID(HocClkConfigValue, kval)) {
            return false;
        }
        if (!hocclkValidConfigValue(kval, value)) {
            return false;
        }

        std::scoped_lock lock{gConfigMutex};

        std::vector<const char*> iniKeys;
        std::vector<std::string> iniValues;
        iniKeys.reserve(2);
        iniValues.reserve(1);

        iniKeys.push_back(hocclkFormatConfigValue(kval, false));
        iniValues.push_back(std::to_string(value));
        iniKeys.push_back(NULL);

        std::vector<const char*> valuePointers;
        valuePointers.reserve(2);
        valuePointers.push_back(iniValues[0].c_str());
        valuePointers.push_back(NULL);

        if (!ini_putsection(CONFIG_VAL_SECTION, iniKeys.data(), valuePointers.data(), gPath.c_str())) {
            return false;
        }

        if (immediate) {
            configValues[kval] = value;
        }

        return true;
    }

}
