#include "user_data.h"

#include <izo/Paths.hpp>

extern "C"
{
#include <ikv.h>
}

#include <algorithm>
#include <fstream>
#include <iomanip>
#include <utility>

namespace
{
    std::filesystem::path roaming_directory()
    {
        if (const auto config = izo::GetKnownPath(izo::KnownPath::Config); !config.empty())
            return config;
        if (const auto temporary = izo::GetKnownPath(izo::KnownPath::Temporary); !temporary.empty())
            return temporary;
        return std::filesystem::temp_directory_path();
    }

    const ikv_node_t *object_value(const ikv_node_t *object, const char *key, ikv_type_t type)
    {
        const ikv_node_t *value = object ? ikv_object_get(object, key) : nullptr;
        return value && ikv_node_type(value) == type ? value : nullptr;
    }
}

UserData::UserData()
{
    directory_ = roaming_directory() / "Identity" / "Gitree";
    std::filesystem::create_directories(directory_);
    load();
}

UserData::~UserData()
{
    save();
}

std::filesystem::path UserData::dataPath() const
{
    return directory_ / "user_data.ikv2b";
}

void UserData::removeLegacyFiles() const
{
    std::error_code error;
    for (const char *name : {"imgui.ini", "settings.ini", "history.txt", "session.txt", "user_data.ikv"})
        std::filesystem::remove(directory_ / name, error);
}

void UserData::load()
{
    const std::filesystem::path binary_path = dataPath();
    if (ikv_node_t *root = ikvb_parse_file(binary_path.string().c_str()))
    {
        if (const ikv_node_t *settings = object_value(root, "settings", IKV_OBJECT))
        {
            if (const ikv_node_t *value = object_value(settings, "sidebar_width", IKV_FLOAT))
                sidebar_width_ = static_cast<float>(ikv_as_float(value));
            if (const ikv_node_t *value = object_value(settings, "details_width", IKV_FLOAT))
                details_width_ = static_cast<float>(ikv_as_float(value));
            if (const ikv_node_t *value = object_value(settings, "sidebar_collapsed", IKV_BOOL))
                sidebar_collapsed_ = ikv_as_bool(value) != 0;
            if (const ikv_node_t *value = object_value(settings, "pull_mode", IKV_INT))
                pull_mode_ = static_cast<int>(ikv_as_int(value));
            if (const ikv_node_t *value = object_value(settings, "zoom_percent", IKV_INT))
                zoom_percent_ = static_cast<int>(ikv_as_int(value));
            if (const ikv_node_t *heights = object_value(settings, "sidebar_sections", IKV_ARRAY))
            {
                const uint32_t count = std::min<uint32_t>(
                    ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size()));
                for (uint32_t index = 0; index < count; ++index)
                    sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index)));
            }
            if (const ikv_node_t *open = object_value(settings, "sidebar_section_open", IKV_ARRAY))
            {
                const uint32_t count = std::min<uint32_t>(
                    ikv_array_size(open), static_cast<uint32_t>(sidebar_section_open_.size()));
                for (uint32_t index = 0; index < count; ++index)
                    sidebar_section_open_[index] = ikv_as_bool(ikv_array_get(open, index)) != 0;
            }
            if (const ikv_node_t *widths = object_value(settings, "commit_table_columns", IKV_ARRAY))
            {
                const uint32_t count = std::min<uint32_t>(
                    ikv_array_size(widths), static_cast<uint32_t>(commit_table_column_widths_.size()));
                for (uint32_t index = 0; index < count; ++index)
                    commit_table_column_widths_[index] = static_cast<float>(ikv_as_float(ikv_array_get(widths, index)));
            }
        }
        if (const ikv_node_t *history = object_value(root, "recently_closed", IKV_ARRAY))
        {
            const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 100);
            for (uint32_t index = 0; index < count; ++index)
            {
                const char *path = ikv_as_string(ikv_array_get(history, index));
                if (path && *path)
                    recently_closed_.emplace_back(path);
            }
        }
        if (const ikv_node_t *recent = object_value(root, "recent_repositories", IKV_ARRAY))
        {
            const uint32_t count = std::min<uint32_t>(ikv_array_size(recent), 100);
            for (uint32_t index = 0; index < count; ++index)
            {
                const char *path = ikv_as_string(ikv_array_get(recent, index));
                if (path && *path)
                    recent_repositories_.emplace_back(path);
            }
        }
        if (const ikv_node_t *session = object_value(root, "session", IKV_OBJECT))
        {
            if (const ikv_node_t *active = object_value(session, "active_tab", IKV_INT))
                active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active)));
            if (const ikv_node_t *tabs = object_value(session, "tabs", IKV_ARRAY))
            {
                for (uint32_t index = 0; index < ikv_array_size(tabs); ++index)
                {
                    const char *path = ikv_as_string(ikv_array_get(tabs, index));
                    open_repositories_.emplace_back(path ? path : "");
                }
            }
        }
        ikv_free(root);
        sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
        details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
        pull_mode_ = std::clamp(pull_mode_, 0, 3);
        zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
        for (float &height : sidebar_section_heights_)
            height = std::clamp(height, 42.0f, 500.0f);
        for (float &width : commit_table_column_widths_)
            width = std::clamp(width, 0.0f, 1200.0f);
        if (open_repositories_.empty())
            active_repository_ = 0;
        else
            active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
        return;
    }

    const std::filesystem::path text_data_path = directory_ / "user_data.ikv";
    if (ikv_node_t *root = ikv_parse_file(text_data_path.string().c_str()))
    {
        if (const ikv_node_t *settings = object_value(root, "settings", IKV_OBJECT))
        {
            if (const ikv_node_t *value = object_value(settings, "sidebar_width", IKV_FLOAT))
                sidebar_width_ = static_cast<float>(ikv_as_float(value));
            if (const ikv_node_t *value = object_value(settings, "details_width", IKV_FLOAT))
                details_width_ = static_cast<float>(ikv_as_float(value));
            if (const ikv_node_t *value = object_value(settings, "sidebar_collapsed", IKV_BOOL))
                sidebar_collapsed_ = ikv_as_bool(value) != 0;
            if (const ikv_node_t *value = object_value(settings, "pull_mode", IKV_INT))
                pull_mode_ = static_cast<int>(ikv_as_int(value));
            if (const ikv_node_t *value = object_value(settings, "zoom_percent", IKV_INT))
                zoom_percent_ = static_cast<int>(ikv_as_int(value));
            if (const ikv_node_t *heights = object_value(settings, "sidebar_sections", IKV_ARRAY))
            {
                const uint32_t count = std::min<uint32_t>(
                    ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size()));
                for (uint32_t index = 0; index < count; ++index)
                    sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index)));
            }
            if (const ikv_node_t *open = object_value(settings, "sidebar_section_open", IKV_ARRAY))
            {
                const uint32_t count = std::min<uint32_t>(
                    ikv_array_size(open), static_cast<uint32_t>(sidebar_section_open_.size()));
                for (uint32_t index = 0; index < count; ++index)
                    sidebar_section_open_[index] = ikv_as_bool(ikv_array_get(open, index)) != 0;
            }
            if (const ikv_node_t *widths = object_value(settings, "commit_table_columns", IKV_ARRAY))
            {
                const uint32_t count = std::min<uint32_t>(
                    ikv_array_size(widths), static_cast<uint32_t>(commit_table_column_widths_.size()));
                for (uint32_t index = 0; index < count; ++index)
                    commit_table_column_widths_[index] = static_cast<float>(ikv_as_float(ikv_array_get(widths, index)));
            }
        }
        if (const ikv_node_t *history = object_value(root, "recently_closed", IKV_ARRAY))
        {
            const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 100);
            for (uint32_t index = 0; index < count; ++index)
            {
                const char *path = ikv_as_string(ikv_array_get(history, index));
                if (path && *path)
                    recently_closed_.emplace_back(path);
            }
        }
        if (const ikv_node_t *recent = object_value(root, "recent_repositories", IKV_ARRAY))
        {
            const uint32_t count = std::min<uint32_t>(ikv_array_size(recent), 100);
            for (uint32_t index = 0; index < count; ++index)
            {
                const char *path = ikv_as_string(ikv_array_get(recent, index));
                if (path && *path)
                    recent_repositories_.emplace_back(path);
            }
        }
        if (const ikv_node_t *session = object_value(root, "session", IKV_OBJECT))
        {
            if (const ikv_node_t *active = object_value(session, "active_tab", IKV_INT))
                active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active)));
            if (const ikv_node_t *tabs = object_value(session, "tabs", IKV_ARRAY))
            {
                for (uint32_t index = 0; index < ikv_array_size(tabs); ++index)
                {
                    const char *path = ikv_as_string(ikv_array_get(tabs, index));
                    open_repositories_.emplace_back(path ? path : "");
                }
            }
        }
        ikv_free(root);
        sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
        details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
        pull_mode_ = std::clamp(pull_mode_, 0, 3);
        zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
        for (float &height : sidebar_section_heights_)
            height = std::clamp(height, 42.0f, 500.0f);
        for (float &width : commit_table_column_widths_)
            width = std::clamp(width, 0.0f, 1200.0f);
        if (open_repositories_.empty())
            active_repository_ = 0;
        else
            active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
        save();
        return;
    }

    // Import the original files once when upgrading an existing installation.
    std::ifstream settings(directory_ / "settings.ini");
    std::string key;
    while (settings >> key)
    {
        if (key == "sidebar_width")
            settings >> sidebar_width_;
        else if (key == "details_width")
            settings >> details_width_;
        else if (key == "pull_mode")
            settings >> pull_mode_;
        else if (key == "zoom_percent")
            settings >> zoom_percent_;
        else if (key.rfind("sidebar_section_", 0) == 0)
        {
            const size_t index = static_cast<size_t>(std::stoul(key.substr(16)));
            float height = 0.0f;
            settings >> height;
            if (index < sidebar_section_heights_.size())
                sidebar_section_heights_[index] = height;
        }
    }
    sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
    details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
    pull_mode_ = std::clamp(pull_mode_, 0, 3);
    zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
    for (float &height : sidebar_section_heights_)
        height = std::clamp(height, 42.0f, 500.0f);

    std::ifstream history(directory_ / "history.txt");
    std::string path;
    while (history >> std::quoted(path))
    {
        if (!path.empty())
            recently_closed_.push_back(path);
        if (recently_closed_.size() == 100)
            break;
    }
    recent_repositories_ = recently_closed_;

    std::ifstream session(directory_ / "session.txt");
    session >> active_repository_;
    while (session >> std::quoted(path))
        open_repositories_.push_back(path);
    if (open_repositories_.empty())
        active_repository_ = 0;
    else
        active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
    save();
}

void UserData::addRecentRepository(const std::string &path)
{
    if (path.empty())
        return;
    std::erase(recent_repositories_, path);
    recent_repositories_.insert(recent_repositories_.begin(), path);
    if (recent_repositories_.size() > 100)
        recent_repositories_.resize(100);
    save();
}

void UserData::addRecentlyClosed(const std::string &path)
{
    if (path.empty())
        return;
    std::erase(recent_repositories_, path);
    recent_repositories_.insert(recent_repositories_.begin(), path);
    if (recent_repositories_.size() > 100)
        recent_repositories_.resize(100);
    std::erase(recently_closed_, path);
    recently_closed_.insert(recently_closed_.begin(), path);
    if (recently_closed_.size() > 100)
        recently_closed_.resize(100);
    save();
}

std::string UserData::takeRecentlyClosed()
{
    if (recently_closed_.empty())
        return {};
    std::string path = std::move(recently_closed_.front());
    recently_closed_.erase(recently_closed_.begin());
    save();
    return path;
}

void UserData::setRepositorySession(std::vector<std::string> paths, size_t active_repository)
{
    open_repositories_ = std::move(paths);
    active_repository_ = open_repositories_.empty()
                             ? 0
                             : std::min(active_repository, open_repositories_.size() - 1);
    save();
}

void UserData::save() const
{
    std::filesystem::create_directories(directory_);
    ikv_node_t *root = ikv_create_object("gitree");
    if (!root)
        return;

    ikv_node_t *settings = ikv_object_add_object(root, "settings");
    ikv_object_set_float(settings, "sidebar_width", sidebar_width_);
    ikv_object_set_float(settings, "details_width", details_width_);
    ikv_object_set_bool(settings, "sidebar_collapsed", sidebar_collapsed_);
    ikv_object_set_int(settings, "pull_mode", pull_mode_);
    ikv_object_set_int(settings, "zoom_percent", zoom_percent_);
    ikv_node_t *heights = ikv_object_add_array(settings, "sidebar_sections", IKV_FLOAT);
    for (const float height : sidebar_section_heights_)
        ikv_array_add_float(heights, height);
    ikv_node_t *open = ikv_object_add_array(settings, "sidebar_section_open", IKV_BOOL);
    for (const bool state : sidebar_section_open_)
        ikv_array_add_bool(open, state);
    ikv_node_t *widths = ikv_object_add_array(settings, "commit_table_columns", IKV_FLOAT);
    for (const float width : commit_table_column_widths_)
        ikv_array_add_float(widths, width);

    ikv_node_t *history = ikv_object_add_array(root, "recently_closed", IKV_STRING);
    for (const auto &path : recently_closed_)
        ikv_array_add_string(history, path.c_str());

    ikv_node_t *recent = ikv_object_add_array(root, "recent_repositories", IKV_STRING);
    for (const auto &path : recent_repositories_)
        ikv_array_add_string(recent, path.c_str());

    ikv_node_t *session = ikv_object_add_object(root, "session");
    ikv_object_set_int(session, "active_tab", static_cast<int64_t>(active_repository_));
    ikv_node_t *tabs = ikv_object_add_array(session, "tabs", IKV_STRING);
    for (const auto &path : open_repositories_)
        ikv_array_add_string(tabs, path.c_str());

    ikvb_write_file_version(dataPath().string().c_str(), root, IKV_VERSION_2);
    ikv_free(root);
    removeLegacyFiles();
}
