#include "content/views/view_diff.hpp"

#include <hex/api/imhex_api.hpp>

#include <hex/helpers/fmt.hpp>
#include <hex/providers/buffered_reader.hpp>

namespace hex::plugin::builtin {

    namespace {

        constexpr u32 getDiffColor(u32 color) {
            return (color & 0x00FFFFFF) | 0x40000000;
        }

    }

    ViewDiff::ViewDiff() : View::Window("hex.builtin.view.diff.name") {

        // Clear the selected diff providers when a provider is closed
        EventManager::subscribe<EventProviderClosed>(this, [this](prv::Provider *) {
            for (u8 i = 0; i < 2; i++) {
                this->m_columns[i].provider = -1;
                this->m_columns[i].hexEditor.setSelectionUnchecked(std::nullopt, std::nullopt);
            }

            this->m_diffs.clear();
        });

        // Set the background highlight callbacks for the two hex editor columns
        this->m_columns[0].hexEditor.setBackgroundHighlightCallback(this->createCompareFunction(1));
        this->m_columns[1].hexEditor.setBackgroundHighlightCallback(this->createCompareFunction(0));
    }

    ViewDiff::~ViewDiff() {
        EventManager::unsubscribe<EventProviderClosed>(this);
    }

    namespace {

        bool drawDiffColumn(ViewDiff::Column &column, float height) {
            bool scrolled = false;
            ImGui::PushID(&column);

            // Draw the hex editor
            float prevScroll = column.hexEditor.getScrollPosition();
            column.hexEditor.draw(height);
            float currScroll = column.hexEditor.getScrollPosition();

            // Check if the user scrolled the hex editor
            if (prevScroll != currScroll) {
                scrolled = true;
                column.scrollLock = 5;
            }

            ImGui::PopID();

            return scrolled;
        }

        bool drawProviderSelector(ViewDiff::Column &column) {
            bool shouldReanalyze = false;

            ImGui::PushID(&column);

            auto &providers = ImHexApi::Provider::getProviders();
            auto &providerIndex = column.provider;

            // Get the name of the currently selected provider
            std::string preview;
            if (ImHexApi::Provider::isValid() && providerIndex >= 0)
                preview = providers[providerIndex]->getName();

            // Draw combobox with all available providers
            ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
            if (ImGui::BeginCombo("", preview.c_str())) {

                for (size_t i = 0; i < providers.size(); i++) {
                    ImGui::PushID(i + 1);
                    if (ImGui::Selectable(providers[i]->getName().c_str())) {
                        providerIndex = i;
                        shouldReanalyze = true;
                    }
                    ImGui::PopID();
                }

                ImGui::EndCombo();
            }

            ImGui::PopID();

            return shouldReanalyze;
        }

    }

    std::function<std::optional<color_t>(u64, const u8*, size_t)> ViewDiff::createCompareFunction(size_t otherIndex) const {
        // Create a function that will handle highlighting the differences between the two providers
        // This is a stupidly simple diffing implementation. It will highlight bytes that are different in yellow
        // and if one provider is larger than the other it will highlight the extra bytes in green or red depending on which provider is larger
        // TODO: Use an actual binary diffing algorithm that searches for the longest common subsequences

        return [this, otherIndex](u64 address, const u8 *data, size_t) -> std::optional<color_t> {
            const auto &providers = ImHexApi::Provider::getProviders();
            auto otherId = this->m_columns[otherIndex].provider;

            // Check if the other provider is valid
            if (otherId < 0 || size_t(otherId) >= providers.size())
                return std::nullopt;

            auto &otherProvider = providers[otherId];

            // Handle the case where one provider is larger than the other one
            if (address > otherProvider->getActualSize()) {
                if (otherIndex == 1)
                    return getDiffColor(ImGuiExt::GetCustomColorU32(ImGuiCustomCol_DiffAdded));
                else
                    return getDiffColor(ImGuiExt::GetCustomColorU32(ImGuiCustomCol_DiffRemoved));
            }

            // Read the current byte from the other provider
            u8 otherByte = 0x00;
            otherProvider->read(address, &otherByte, 1);

            // Compare the two bytes, highlight both in yellow if they are different
            if (otherByte != *data)
                return getDiffColor(ImGuiExt::GetCustomColorU32(ImGuiCustomCol_DiffChanged));

            // No difference
            return std::nullopt;
        };
    }

    void ViewDiff::analyze(prv::Provider *providerA, prv::Provider *providerB) {
        auto commonSize = std::min(providerA->getActualSize(), providerB->getActualSize());
        this->m_diffTask = TaskManager::createTask("Diffing...", commonSize, [this, providerA, providerB](Task &task) {
            std::vector<Diff> differences;

            // Set up readers for both providers
            auto readerA = prv::ProviderReader(providerA);
            auto readerB = prv::ProviderReader(providerB);

            // Iterate over both providers and compare the bytes
            for (auto itA = readerA.begin(), itB = readerB.begin(); itA < readerA.end() && itB < readerB.end(); ++itA, ++itB) {
                // Stop comparing if the diff task was canceled
                if (task.wasInterrupted())
                    break;

                // If the bytes are different, find the end of the difference
                if (*itA != *itB) {
                    u64 start = itA.getAddress();
                    size_t end = 0;

                    while (itA != readerA.end() && itB != readerB.end() && *itA != *itB) {
                        ++itA;
                        ++itB;
                        ++end;
                    }

                    // Add the difference to the list
                    differences.push_back(Diff { Region{ start, end }, ViewDiff::DifferenceType::Modified });
                }

                // Update the progress bar
                task.update(itA.getAddress());
            }

            // If one provider is larger than the other, add the extra bytes to the list
            if (providerA->getActualSize() != providerB->getActualSize()) {
                auto endA = providerA->getActualSize() + 1;
                auto endB = providerB->getActualSize() + 1;

                if (endA > endB)
                    differences.push_back(Diff { Region{ endB, endA - endB }, ViewDiff::DifferenceType::Added });
                else
                    differences.push_back(Diff { Region{ endA, endB - endA }, ViewDiff::DifferenceType::Removed });
            }

            // Move the calculated differences over so they can be displayed
            this->m_diffs = std::move(differences);
            this->m_analyzed = true;
        });
    }

    void ViewDiff::drawContent() {
        auto &[a, b] = this->m_columns;

        a.hexEditor.enableSyncScrolling(false);
        b.hexEditor.enableSyncScrolling(false);

        if (a.scrollLock > 0) a.scrollLock--;
        if (b.scrollLock > 0) b.scrollLock--;

        // Change the hex editor providers if the user selected a new provider
        {
            const auto &providers = ImHexApi::Provider::getProviders();
            if (a.provider >= 0 && size_t(a.provider) < providers.size())
                a.hexEditor.setProvider(providers[a.provider]);
            else
                a.hexEditor.setProvider(nullptr);

            if (b.provider >= 0 && size_t(b.provider) < providers.size())
                b.hexEditor.setProvider(providers[b.provider]);
            else
                b.hexEditor.setProvider(nullptr);
        }

        // Analyze the providers if they are valid and the user selected a new provider
        if (!this->m_analyzed && a.provider != -1 && b.provider != -1 && !this->m_diffTask.isRunning()) {
            const auto &providers = ImHexApi::Provider::getProviders();
            auto providerA = providers[a.provider];
            auto providerB = providers[b.provider];

            this->analyze(providerA, providerB);
        }

        const auto height = ImGui::GetContentRegionAvail().y;

        // Draw the two hex editor columns side by side
        if (ImGui::BeginTable("##binary_diff", 2, ImGuiTableFlags_None, ImVec2(0, height - 250_scaled))) {
            ImGui::TableSetupColumn("hex.builtin.view.diff.provider_a"_lang);
            ImGui::TableSetupColumn("hex.builtin.view.diff.provider_b"_lang);
            ImGui::TableHeadersRow();

            ImGui::BeginDisabled(this->m_diffTask.isRunning());
            {
                // Draw first provider selector
                ImGui::TableNextColumn();
                if (drawProviderSelector(a)) this->m_analyzed = false;

                // Draw second provider selector
                ImGui::TableNextColumn();
                if (drawProviderSelector(b)) this->m_analyzed = false;
            }
            ImGui::EndDisabled();

            ImGui::TableNextRow();

            // Draw first hex editor column
            ImGui::TableNextColumn();
            bool scrollB = drawDiffColumn(a, height - 250_scaled);

            // Draw second hex editor column
            ImGui::TableNextColumn();
            bool scrollA = drawDiffColumn(b, height - 250_scaled);

            // Sync the scroll positions of the hex editors
            {
                if (scrollA && a.scrollLock == 0) {
                    a.hexEditor.setScrollPosition(b.hexEditor.getScrollPosition());
                    a.hexEditor.forceUpdateScrollPosition();
                }
                if (scrollB && b.scrollLock == 0) {
                    b.hexEditor.setScrollPosition(a.hexEditor.getScrollPosition());
                    b.hexEditor.forceUpdateScrollPosition();
                }
            }

            ImGui::EndTable();
        }

        // Draw the differences table
        if (ImGui::BeginTable("##differences", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Resizable, ImVec2(0, 200_scaled))) {
            ImGui::TableSetupScrollFreeze(0, 1);
            ImGui::TableSetupColumn("hex.builtin.common.begin"_lang);
            ImGui::TableSetupColumn("hex.builtin.common.end"_lang);
            ImGui::TableSetupColumn("hex.builtin.common.type"_lang);
            ImGui::TableHeadersRow();

            // Draw the differences if the providers have been analyzed
            if (this->m_analyzed) {
                ImGuiListClipper clipper;
                clipper.Begin(int(this->m_diffs.size()));

                while (clipper.Step())
                    for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) {
                        ImGui::TableNextRow();

                        // Prevent the list from trying to access non-existing diffs
                        if (size_t(i) >= this->m_diffs.size())
                            break;

                        ImGui::PushID(i);

                        const auto &diff = this->m_diffs[i];

                        // Draw a clickable row for each difference that will select the difference in both hex editors

                        // Draw start address
                        ImGui::TableNextColumn();
                        if (ImGui::Selectable(hex::format("0x{:02X}", diff.region.getStartAddress()).c_str(), false, ImGuiSelectableFlags_SpanAllColumns)) {
                            a.hexEditor.setSelection(diff.region);
                            a.hexEditor.jumpToSelection();
                            b.hexEditor.setSelection(diff.region);
                            b.hexEditor.jumpToSelection();
                        }

                        // Draw end address
                        ImGui::TableNextColumn();
                        ImGui::TextUnformatted(hex::format("0x{:02X}", diff.region.getEndAddress()).c_str());

                        // Draw difference type
                        ImGui::TableNextColumn();
                        switch (diff.type) {
                            case DifferenceType::Modified:
                                ImGuiExt::TextFormattedColored(ImGuiExt::GetCustomColorVec4(ImGuiCustomCol_DiffChanged), "hex.builtin.view.diff.modified"_lang);
                                break;
                            case DifferenceType::Added:
                                ImGuiExt::TextFormattedColored(ImGuiExt::GetCustomColorVec4(ImGuiCustomCol_DiffAdded), "hex.builtin.view.diff.added"_lang);
                                break;
                            case DifferenceType::Removed:
                                ImGuiExt::TextFormattedColored(ImGuiExt::GetCustomColorVec4(ImGuiCustomCol_DiffRemoved), "hex.builtin.view.diff.removed"_lang);
                                break;
                        }

                        ImGui::PopID();
                    }
            }

            ImGui::EndTable();
        }
    }

}