#include "fastfetch.h"
#include "common/color.h"
#include "common/jsonconfig.h"
#include "common/printing.h"
#include "common/io.h"
#include "common/time.h"
#include "common/strutil.h"
#include "detection/version/version.h"
#include "modules/modules.h"

#include <assert.h>
#include <ctype.h>
#include <inttypes.h>

bool ffJsonConfigParseModuleArgs(yyjson_val* key, yyjson_val* val, FFModuleArgs* moduleArgs) {
    if (unsafe_yyjson_equals_str(key, "type") || unsafe_yyjson_equals_str(key, "condition")) {
        return true;
    }

    if (unsafe_yyjson_equals_str(key, "key")) {
        ffStrbufSetJsonVal(&moduleArgs->key, val);
        return true;
    } else if (unsafe_yyjson_equals_str(key, "format")) {
        ffStrbufSetJsonVal(&moduleArgs->outputFormat, val);
        return true;
    } else if (unsafe_yyjson_equals_str(key, "outputColor")) {
        ffOptionParseColor(yyjson_get_str(val), &moduleArgs->outputColor);
        return true;
    } else if (unsafe_yyjson_equals_str(key, "keyColor")) {
        ffOptionParseColor(yyjson_get_str(val), &moduleArgs->keyColor);
        return true;
    } else if (unsafe_yyjson_equals_str(key, "keyWidth")) {
        moduleArgs->keyWidth = (uint32_t) yyjson_get_uint(val);
        return true;
    } else if (unsafe_yyjson_equals_str(key, "keyIcon")) {
        ffStrbufSetJsonVal(&moduleArgs->keyIcon, val);
        return true;
    }
    return false;
}

void ffJsonConfigGenerateModuleArgsConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFModuleArgs* moduleArgs) {
    if (moduleArgs->key.length > 0) {
        yyjson_mut_obj_add_strbuf(doc, module, "key", &moduleArgs->key);
    }
    if (moduleArgs->outputFormat.length > 0) {
        yyjson_mut_obj_add_strbuf(doc, module, "format", &moduleArgs->outputFormat);
    }
    if (moduleArgs->outputColor.length > 0) {
        yyjson_mut_obj_add_strbuf(doc, module, "outputColor", &moduleArgs->outputColor);
    }
    if (moduleArgs->keyColor.length > 0) {
        yyjson_mut_obj_add_strbuf(doc, module, "keyColor", &moduleArgs->keyColor);
    }
    if (moduleArgs->keyWidth > 0) {
        yyjson_mut_obj_add_uint(doc, module, "keyWidth", moduleArgs->keyWidth);
    }
    if (moduleArgs->keyIcon.length > 0) {
        yyjson_mut_obj_add_strbuf(doc, module, "keyIcon", &moduleArgs->keyIcon);
    }
}

const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]) {
    if (yyjson_is_int(val)) {
        int intVal = yyjson_get_int(val);

        for (const FFKeyValuePair* pPair = pairs; pPair->key; ++pPair) {
            if (intVal == pPair->value) {
                *result = pPair->value;
                return nullptr;
            }
        }

        return "Invalid enum integer";
    } else if (yyjson_is_str(val)) {
        const char* strVal = yyjson_get_str(val);
        for (const FFKeyValuePair* pPair = pairs; pPair->key; ++pPair) {
            if (ffStrEqualsIgnCase(strVal, pPair->key)) {
                *result = pPair->value;
                return nullptr;
            }
        }

        return "Invalid enum string";
    } else {
        return "Invalid enum value type; must be a string or integer";
    }
}

static bool parseModuleJsonObject(const char* type, yyjson_val* jsonVal, yyjson_mut_doc* jsonDoc) {
    if (!ffCharIsEnglishAlphabet(type[0])) {
        return false;
    }

    for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(type[0]) - 'A']; *modules; ++modules) {
        FFModuleBaseInfo* baseInfo = *modules;
        if (ffStrEqualsIgnCase(type, baseInfo->name)) {
            alignas(uint64_t) uint8_t optionBuf[FF_OPTION_MAX_SIZE];
            baseInfo->initOptions(optionBuf);
            if (jsonVal) {
                baseInfo->parseJsonObject(optionBuf, jsonVal);
            }
            bool succeeded;
            if (jsonDoc) {
                yyjson_mut_val* module = yyjson_mut_arr_add_obj(jsonDoc, jsonDoc->root);
                yyjson_mut_obj_add_str(jsonDoc, module, "type", baseInfo->name);
                if (baseInfo->generateJsonResult) {
                    succeeded = baseInfo->generateJsonResult(optionBuf, jsonDoc, module);
                } else {
                    yyjson_mut_obj_add_str(jsonDoc, module, "error", "Unsupported for JSON format");
                    succeeded = false;
                }
            } else {
                succeeded = baseInfo->printModule(optionBuf);
            }
            baseInfo->destroyOptions(optionBuf);
            return succeeded;
        }
    }

    if (jsonDoc) {
        yyjson_mut_val* module = yyjson_mut_arr_add_obj(jsonDoc, jsonDoc->root);
        yyjson_mut_obj_add_strcpy(jsonDoc, module, "type", type);
        yyjson_mut_obj_add_str(jsonDoc, module, "error", "Unknown module type");
    } else {
        FFModuleArgs moduleArgs;
        ffOptionInitModuleArg(&moduleArgs, "");
        ffPrintError(type, 0, &moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown module type");
        ffOptionDestroyModuleArg(&moduleArgs);
    }
    return false;
}

static void prepareModuleJsonObject(const char* type, yyjson_val* module) {
    switch (type[0]) {
        #if !FF_MODULE_DISABLE_CPUUSAGE
        case 'c':
        case 'C': {
            if (ffStrEqualsIgnCase(type, ffCPUUsageModuleInfo.name)) {
                ffPrepareCPUUsage();
            } else if (ffStrEqualsIgnCase(type, ffCommandModuleInfo.name)) {
                [[gnu::cleanup(ffDestroyCommandOptions)]] FFCommandOptions options;
                ffInitCommandOptions(&options);
                if (module) {
                    ffCommandModuleInfo.parseJsonObject(&options, module);
                }
                ffPrepareCommand(&options);
            }
            break;
        }
        #endif

        #if !FF_MODULE_DISABLE_DISKIO
        case 'd':
        case 'D': {
            if (ffStrEqualsIgnCase(type, ffDiskIOModuleInfo.name)) {
                [[gnu::cleanup(ffDestroyDiskIOOptions)]] FFDiskIOOptions options;
                ffInitDiskIOOptions(&options);
                if (module) {
                    ffDiskIOModuleInfo.parseJsonObject(&options, module);
                }
                ffPrepareDiskIO(&options);
            }
            break;
        }
        #endif

        #if !FF_MODULE_DISABLE_NETIO
        case 'n':
        case 'N': {
            if (ffStrEqualsIgnCase(type, ffNetIOModuleInfo.name)) {
                [[gnu::cleanup(ffDestroyNetIOOptions)]] FFNetIOOptions options;
                ffInitNetIOOptions(&options);
                if (module) {
                    ffNetIOModuleInfo.parseJsonObject(&options, module);
                }
                ffPrepareNetIO(&options);
            }
            break;
        }
        #endif

        #if !FF_MODULE_DISABLE_PUBLICIP
        case 'p':
        case 'P': {
            if (ffStrEqualsIgnCase(type, ffPublicIPModuleInfo.name)) {
                [[gnu::cleanup(ffDestroyPublicIpOptions)]] FFPublicIPOptions options;
                ffInitPublicIpOptions(&options);
                if (module) {
                    ffPublicIPModuleInfo.parseJsonObject(&options, module);
                }
                ffPreparePublicIp(&options);
            }
            break;
        }
        #endif

        #if !FF_MODULE_DISABLE_TOP
        case 't':
        case 'T': {
            if (ffStrEqualsIgnCase(type, ffTopModuleInfo.name)) {
                [[gnu::cleanup(ffDestroyTopOptions)]] FFTopOptions options;
                ffInitTopOptions(&options);
                if (module) {
                    ffTopModuleInfo.parseJsonObject(&options, module);
                }
                ffPrepareTopProcesses(options.showTypes);
            }
            break;
        }
        #endif

        #if !FF_MODULE_DISABLE_WEATHER
        case 'w':
        case 'W': {
            if (ffStrEqualsIgnCase(type, ffWeatherModuleInfo.name)) {
                [[gnu::cleanup(ffDestroyWeatherOptions)]] FFWeatherOptions options;
                ffInitWeatherOptions(&options);
                if (module) {
                    ffWeatherModuleInfo.parseJsonObject(&options, module);
                }
                ffPrepareWeather(&options);
            }
            break;
        }
        #endif
    }
}

static bool matchesJsonArray(const char* str, yyjson_val* val) {
    assert(val);

    if (unsafe_yyjson_is_str(val)) {
        return ffStrEqualsIgnCase(str, unsafe_yyjson_get_str(val));
    }

    if (!unsafe_yyjson_is_arr(val)) {
        return false;
    }

    size_t idx, max;
    yyjson_val* item;
    yyjson_arr_foreach (val, idx, max, item) {
        if (yyjson_is_str(item) && ffStrEqualsIgnCase(str, unsafe_yyjson_get_str(item))) {
            return true;
        }
    }
    return false;
}

static const char* printJsonConfig(FFdata* data, bool prepare) {
    yyjson_mut_doc* jsonDoc = data->resultDoc;
    yyjson_val* const root = yyjson_doc_get_root(data->configDoc);
    assert(root);

    if (!yyjson_is_obj(root)) {
        return "Invalid JSON config format. Root value must be an object";
    }

    yyjson_val* modules = yyjson_obj_get(root, "modules");
    if (!modules) {
        return nullptr;
    }
    if (!yyjson_is_arr(modules)) {
        return "Property 'modules' must be an array of strings or objects";
    }

    bool succeeded = true;
    int32_t thres = instance.config.display.stat;
    yyjson_val* item;
    size_t idx, max;
    yyjson_arr_foreach (modules, idx, max, item) {
        double ms = 0;
        if (!prepare && thres >= 0) {
            ms = ffTimeGetTick();
        }

        yyjson_val* module = item;
        const char* type = yyjson_get_str(module);
        if (type) {
            module = nullptr;
        } else if (yyjson_is_obj(module)) {
            yyjson_val* conditions = yyjson_obj_get(module, "condition");
            if (conditions) {
                if (!yyjson_is_obj(conditions)) {
                    return "Property 'condition' must be an object";
                }

                yyjson_val* system = yyjson_obj_get(conditions, "system");
                if (system && !matchesJsonArray(ffVersionResult.sysName, system)) {
                    continue;
                }

                system = yyjson_obj_get(conditions, "!system");
                if (system && matchesJsonArray(ffVersionResult.sysName, system)) {
                    continue;
                }

                yyjson_val* arch = yyjson_obj_get(conditions, "arch");
                if (arch && !matchesJsonArray(ffVersionResult.architecture, arch)) {
                    continue;
                }

                arch = yyjson_obj_get(conditions, "!arch");
                if (arch && matchesJsonArray(ffVersionResult.architecture, arch)) {
                    continue;
                }

                yyjson_val* previousSucceeded = yyjson_obj_get(conditions, "succeeded");
                if (previousSucceeded && !unsafe_yyjson_is_null(previousSucceeded)) {
                    if (!unsafe_yyjson_is_bool(previousSucceeded)) {
                        return "Property 'succeeded' in 'condition' must be a boolean";
                    }
                    if (succeeded != unsafe_yyjson_get_bool(previousSucceeded)) {
                        continue;
                    }
                }
            }

            type = yyjson_get_str(yyjson_obj_get(module, "type"));
            if (!type) {
                return "module object must contain a \"type\" key ( case sensitive )";
            }
            if (yyjson_obj_size(module) == 1) { // contains only Property type
                module = nullptr;
            }
        } else {
            return "modules must be an array of strings or objects";
        }

        if (ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, type, ':')) {
            continue;
        }

        if (prepare) {
            prepareModuleJsonObject(type, module);
        } else {
            succeeded = parseModuleJsonObject(type, module, jsonDoc);
        }

        if (!prepare && thres >= 0) {
            ms = ffTimeGetTick() - ms;
            if (jsonDoc) {
                yyjson_mut_val* moduleJson = yyjson_mut_arr_get_last(jsonDoc->root);
                yyjson_mut_obj_add_real(jsonDoc, moduleJson, "stat", ms);
            } else {
                char str[64];
                int len = snprintf(str, sizeof str, "%.3fms", ms);
                if (thres > 0) {
                    snprintf(str, sizeof str, "\e[%sm%.3fms\e[m", (ms <= thres ? FF_COLOR_FG_GREEN : ms <= 2 * thres ? FF_COLOR_FG_YELLOW
                                                                                                                     : FF_COLOR_FG_RED),
                        ms);
                }
                printf("\e7\e[1A\e[9999999C\e[%dD%s\e8", len - 1, str); // Save; Up 1; Right 9999999; Left <len - 1>; Print <str>; Load
            }
        }

#if defined(_WIN32)
        if (!instance.config.display.noBuffer && !jsonDoc) {
            fflush(stdout);
        }
#endif
    }

    return nullptr;
}

void ffPrintJsonConfig(FFdata* data, bool prepare) {
    yyjson_mut_doc* jsonDoc = data->resultDoc;
    const char* error = printJsonConfig(data, prepare);
    if (error) {
        if (jsonDoc) {
            yyjson_mut_val* obj = yyjson_mut_obj(jsonDoc);
            yyjson_mut_obj_add_str(jsonDoc, obj, "error", error);
            yyjson_mut_doc_set_root(jsonDoc, obj);
        } else {
            ffPrintError("JsonConfig", 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "%s", error);
        }
    }
}
