#include "postprocessorhud.hpp"

#include <MyGUI_Window.h>
#include <MyGUI_Button.h>
#include <MyGUI_TabItem.h>
#include <MyGUI_ScrollView.h>
#include <MyGUI_FactoryManager.h>

#include <components/fx/widgets.hpp>
#include <components/fx/technique.hpp>

#include <components/misc/utf8stream.hpp>

#include <components/widgets/box.hpp>

#include "../mwrender/postprocessor.hpp"

#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"

namespace MWGui
{
    void PostProcessorHud::ListWrapper::onKeyButtonPressed(MyGUI::KeyCode key, MyGUI::Char ch)
    {
        if (MyGUI::InputManager::getInstance().isShiftPressed() && (key == MyGUI::KeyCode::ArrowUp || key == MyGUI::KeyCode::ArrowDown))
            return;

        MyGUI::ListBox::onKeyButtonPressed(key, ch);
    }

    PostProcessorHud::PostProcessorHud()
        : WindowBase("openmw_postprocessor_hud.layout")
    {
        getWidget(mActiveList, "ActiveList");
        getWidget(mInactiveList, "InactiveList");
        getWidget(mConfigLayout, "ConfigLayout");
        getWidget(mFilter, "Filter");
        getWidget(mButtonActivate, "ButtonActivate");
        getWidget(mButtonDeactivate, "ButtonDeactivate");
        getWidget(mButtonUp, "ButtonUp");
        getWidget(mButtonDown, "ButtonDown");

        mButtonActivate->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyActivatePressed);
        mButtonDeactivate->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyDeactivatePressed);
        mButtonUp->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyShaderUpPressed);
        mButtonDown->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyShaderDownPressed);

        mActiveList->eventKeyButtonPressed += MyGUI::newDelegate(this, &PostProcessorHud::notifyKeyButtonPressed);
        mInactiveList->eventKeyButtonPressed += MyGUI::newDelegate(this, &PostProcessorHud::notifyKeyButtonPressed);

        mActiveList->eventListChangePosition += MyGUI::newDelegate(this, &PostProcessorHud::notifyListChangePosition);
        mInactiveList->eventListChangePosition += MyGUI::newDelegate(this, &PostProcessorHud::notifyListChangePosition);

        mFilter->eventEditTextChange += MyGUI::newDelegate(this, &PostProcessorHud::notifyFilterChanged);

        mMainWidget->castType<MyGUI::Window>()->eventWindowChangeCoord += MyGUI::newDelegate(this, &PostProcessorHud::notifyWindowResize);

        mShaderInfo = mConfigLayout->createWidget<Gui::AutoSizedEditBox>("HeaderText", {}, MyGUI::Align::Default);
        mShaderInfo->setUserString("VStretch", "true");
        mShaderInfo->setUserString("HStretch", "true");
        mShaderInfo->setTextAlign(MyGUI::Align::Left | MyGUI::Align::Top);
        mShaderInfo->setEditReadOnly(true);
        mShaderInfo->setEditWordWrap(true);
        mShaderInfo->setEditMultiLine(true);
        mShaderInfo->setNeedMouseFocus(false);

        mConfigLayout->setVisibleVScroll(true);

        mConfigArea = mConfigLayout->createWidget<MyGUI::Widget>("", {}, MyGUI::Align::Default);

        mConfigLayout->eventMouseWheel += MyGUI::newDelegate(this, &PostProcessorHud::notifyMouseWheel);
        mConfigArea->eventMouseWheel += MyGUI::newDelegate(this, &PostProcessorHud::notifyMouseWheel);
    }

    void PostProcessorHud::notifyFilterChanged(MyGUI::EditBox* sender)
    {
        updateTechniques();
    }

    void PostProcessorHud::notifyWindowResize(MyGUI::Window* sender)
    {
        layout();
    }

    void PostProcessorHud::notifyResetButtonClicked(MyGUI::Widget* sender)
    {
        for (size_t i = 1; i < mConfigArea->getChildCount(); ++i)
        {
            if (auto* child = dynamic_cast<fx::Widgets::UniformBase*>(mConfigArea->getChildAt(i)))
                child->toDefault();
        }
    }

    void PostProcessorHud::notifyListChangePosition(MyGUI::ListBox* sender, size_t index)
    {
        if (sender == mActiveList)
            mInactiveList->clearIndexSelected();
        else if (sender == mInactiveList)
            mActiveList->clearIndexSelected();

        if (index >= sender->getItemCount())
            return;

        updateConfigView(sender->getItemNameAt(index));
    }

    void PostProcessorHud::toggleTechnique(bool enabled)
    {
        auto* list = enabled ? mInactiveList : mActiveList;

        size_t selected = list->getIndexSelected();

        if (selected != MyGUI::ITEM_NONE)
        {
            auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor();
            mOverrideHint = list->getItemNameAt(selected);

            auto technique = *list->getItemDataAt<std::shared_ptr<fx::Technique>>(selected);
            if (technique->getDynamic())
                return;

            if (enabled)
                processor->enableTechnique(technique);
            else
                processor->disableTechnique(technique);
            processor->saveChain();
        }
    }

    void PostProcessorHud::notifyActivatePressed(MyGUI::Widget* sender)
    {
        toggleTechnique(true);
    }

    void PostProcessorHud::notifyDeactivatePressed(MyGUI::Widget* sender)
    {
        toggleTechnique(false);
    }

    void PostProcessorHud::moveShader(Direction direction)
    {
        auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor();

        size_t selected = mActiveList->getIndexSelected();

        if (selected == MyGUI::ITEM_NONE)
            return;

        int index = direction == Direction::Up ? static_cast<int>(selected) - 1 : selected + 1;
        index = std::clamp<int>(index, 0, mActiveList->getItemCount() - 1);

        if (static_cast<size_t>(index) != selected)
        {
            auto technique = *mActiveList->getItemDataAt<std::shared_ptr<fx::Technique>>(selected);
            if (technique->getDynamic())
                return;

            if (processor->enableTechnique(technique, index) != MWRender::PostProcessor::Status_Error)
                processor->saveChain();
        }
    }

    void PostProcessorHud::notifyShaderUpPressed(MyGUI::Widget* sender)
    {
        moveShader(Direction::Up);
    }

    void PostProcessorHud::notifyShaderDownPressed(MyGUI::Widget* sender)
    {
        moveShader(Direction::Down);
    }

    void PostProcessorHud::notifyKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char ch)
    {
        MyGUI::ListBox* list = static_cast<MyGUI::ListBox*>(sender);

        if (list->getIndexSelected() == MyGUI::ITEM_NONE)
            return;

        if (key == MyGUI::KeyCode::ArrowLeft && list == mActiveList)
        {
            if (MyGUI::InputManager::getInstance().isShiftPressed())
            {
                toggleTechnique(false);
            }
            else
            {
                MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mInactiveList);
                mActiveList->clearIndexSelected();
                select(mInactiveList, 0);
            }
        }
        else if (key == MyGUI::KeyCode::ArrowRight && list == mInactiveList)
        {
            if (MyGUI::InputManager::getInstance().isShiftPressed())
            {
                toggleTechnique(true);
            }
            else
            {
                MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mActiveList);
                mInactiveList->clearIndexSelected();
                select(mActiveList, 0);
            }
        }
        else if (list == mActiveList && MyGUI::InputManager::getInstance().isShiftPressed() && (key == MyGUI::KeyCode::ArrowUp || key == MyGUI::KeyCode::ArrowDown))
        {
            moveShader(key == MyGUI::KeyCode::ArrowUp ? Direction::Up : Direction::Down);
        }
    }

    void PostProcessorHud::onOpen()
    {
        toggleMode(Settings::ShaderManager::Mode::Debug);
        updateTechniques();
    }

    void PostProcessorHud::onClose()
    {
        toggleMode(Settings::ShaderManager::Mode::Normal);
    }

    void PostProcessorHud::layout()
    {
        constexpr int padding = 12;
        constexpr int padding2 = padding * 2;
        mShaderInfo->setCoord(padding, padding, mConfigLayout->getSize().width - padding2 - padding, mShaderInfo->getTextSize().height);

        int totalHeight = mShaderInfo->getTop() + mShaderInfo->getTextSize().height + padding;

        mConfigArea->setCoord({padding, totalHeight, mShaderInfo->getSize().width, mConfigLayout->getHeight()});

        int childHeights = 0;
		MyGUI::EnumeratorWidgetPtr enumerator = mConfigArea->getEnumerator();
		while (enumerator.next())
		{
			enumerator.current()->setCoord(padding, childHeights + padding, mShaderInfo->getSize().width - padding2, enumerator.current()->getHeight());
            childHeights += enumerator.current()->getHeight() + padding;
		}
        totalHeight += childHeights;

        mConfigArea->setSize(mConfigArea->getWidth(), childHeights);

        mConfigLayout->setCanvasSize(mConfigLayout->getWidth() - padding2, totalHeight);
        mConfigLayout->setSize(mConfigLayout->getWidth(), mConfigLayout->getParentSize().height - padding2);
    }

    void PostProcessorHud::notifyMouseWheel(MyGUI::Widget *sender, int rel)
    {
        int offset = mConfigLayout->getViewOffset().top + rel * 0.3;
        if (offset > 0)
            mConfigLayout->setViewOffset(MyGUI::IntPoint(0, 0));
        else
            mConfigLayout->setViewOffset(MyGUI::IntPoint(0, static_cast<int>(offset)));
    }

    void PostProcessorHud::select(ListWrapper* list, size_t index)
    {
        list->setIndexSelected(index);
        notifyListChangePosition(list, index);
    }

    void PostProcessorHud::toggleMode(Settings::ShaderManager::Mode mode)
    {
        Settings::ShaderManager::get().setMode(mode);

        MWBase::Environment::get().getWorld()->getPostProcessor()->toggleMode();

        if (!isVisible())
            return;

        if (mInactiveList->getIndexSelected() != MyGUI::ITEM_NONE)
            updateConfigView(mInactiveList->getItemNameAt(mInactiveList->getIndexSelected()));
        else if (mActiveList->getIndexSelected() != MyGUI::ITEM_NONE)
            updateConfigView(mActiveList->getItemNameAt(mActiveList->getIndexSelected()));
    }

    void PostProcessorHud::updateConfigView(const std::string& name)
    {
        auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor();

        auto technique = processor->loadTechnique(name);

        if (!technique || technique->getStatus() == fx::Technique::Status::File_Not_exists)
            return;

        while (mConfigArea->getChildCount() > 0)
            MyGUI::Gui::getInstance().destroyWidget(mConfigArea->getChildAt(0));

        mShaderInfo->setCaption("");

        std::ostringstream ss;

        const std::string NA = "#{Interface:NotAvailableShort}";
        const std::string endl = "\n";

        std::string author = technique->getAuthor().empty() ? NA : std::string(technique->getAuthor());
        std::string version = technique->getVersion().empty() ? NA : std::string(technique->getVersion());
        std::string description = technique->getDescription().empty() ? NA : std::string(technique->getDescription());

        auto serializeBool = [](bool value) {
            return value ? "#{sYes}" : "#{sNo}";
        };

        const auto flags = technique->getFlags();

        const auto flag_interior = serializeBool(!(flags & fx::Technique::Flag_Disable_Interiors));
        const auto flag_exterior = serializeBool(!(flags & fx::Technique::Flag_Disable_Exteriors));
        const auto flag_underwater = serializeBool(!(flags & fx::Technique::Flag_Disable_Underwater));
        const auto flag_abovewater = serializeBool(!(flags & fx::Technique::Flag_Disable_Abovewater));

        switch (technique->getStatus())
        {
            case fx::Technique::Status::Success:
            case fx::Technique::Status::Uncompiled:
            {
                if (technique->getDynamic())
                    ss  << "#{fontcolourhtml=header}#{PostProcessing:ShaderLocked}:      #{fontcolourhtml=normal} #{PostProcessing:ShaderLockedDescription}" << endl << endl;
                ss  << "#{fontcolourhtml=header}#{PostProcessing:Author}:      #{fontcolourhtml=normal} " << author << endl << endl
                    << "#{fontcolourhtml=header}#{PostProcessing:Version}:     #{fontcolourhtml=normal} " << version << endl << endl
                    << "#{fontcolourhtml=header}#{PostProcessing:Description}: #{fontcolourhtml=normal} " << description << endl << endl
                    << "#{fontcolourhtml=header}#{PostProcessing:InInteriors}: #{fontcolourhtml=normal} " << flag_interior
                    << "#{fontcolourhtml=header}   #{PostProcessing:InExteriors}: #{fontcolourhtml=normal} " << flag_exterior
                    << "#{fontcolourhtml=header}   #{PostProcessing:Underwater}: #{fontcolourhtml=normal} " << flag_underwater
                    << "#{fontcolourhtml=header}   #{PostProcessing:Abovewater}: #{fontcolourhtml=normal} " << flag_abovewater;
                break;
            }
            case fx::Technique::Status::Parse_Error:
                ss  << "#{fontcolourhtml=negative}Shader Compile Error: #{fontcolourhtml=normal} <" << std::string(technique->getName()) << "> failed to compile." << endl << endl
                    << technique->getLastError();
                break;
            case fx::Technique::Status::File_Not_exists:
                break;
        }

        mShaderInfo->setCaptionWithReplacing(ss.str());

        if (Settings::ShaderManager::get().getMode() == Settings::ShaderManager::Mode::Debug)
        {
            if (technique->getUniformMap().size() > 0)
            {
                MyGUI::Button* resetButton = mConfigArea->createWidget<MyGUI::Button>("MW_Button", {0,0,0,24}, MyGUI::Align::Default);
                resetButton->setCaptionWithReplacing("#{PostProcessing:ResetShader}");
                resetButton->setTextAlign(MyGUI::Align::Center);
                resetButton->eventMouseWheel += MyGUI::newDelegate(this, &PostProcessorHud::notifyMouseWheel);
                resetButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PostProcessorHud::notifyResetButtonClicked);
            }

            for (const auto& uniform : technique->getUniformMap())
            {
                if (!uniform->mStatic || uniform->mSamplerType)
                    continue;

                if (!uniform->mHeader.empty())
                {
                    Gui::AutoSizedTextBox* divider = mConfigArea->createWidget<Gui::AutoSizedTextBox>("MW_UniformGroup", {0,0,0,34}, MyGUI::Align::Default);
                    divider->setNeedMouseFocus(false);
                    divider->setCaptionWithReplacing(uniform->mHeader);
                }

                fx::Widgets::UniformBase* uwidget = mConfigArea->createWidget<fx::Widgets::UniformBase>("MW_UniformEdit", {0,0,0,22}, MyGUI::Align::Default);
                uwidget->init(uniform);
                uwidget->getLabel()->eventMouseWheel += MyGUI::newDelegate(this, &PostProcessorHud::notifyMouseWheel);
            }
        }

        layout();
    }

    void PostProcessorHud::updateTechniques()
    {
        if (!isVisible())
            return;

        std::string hint;
        ListWrapper* hintWidget = nullptr;
        if (mInactiveList->getIndexSelected() != MyGUI::ITEM_NONE)
        {
            hint = mInactiveList->getItemNameAt(mInactiveList->getIndexSelected());
            hintWidget = mInactiveList;
        }
        else if (mActiveList->getIndexSelected() != MyGUI::ITEM_NONE)
        {
            hint = mActiveList->getItemNameAt(mActiveList->getIndexSelected());
            hintWidget = mActiveList;
        }

        mInactiveList->removeAllItems();
        mActiveList->removeAllItems();

        auto* processor = MWBase::Environment::get().getWorld()->getPostProcessor();

        for (const auto& [name, _] : processor->getTechniqueMap())
        {
            auto technique = processor->loadTechnique(name);

            if (!technique)
                continue;

            if (!technique->getHidden() && !processor->isTechniqueEnabled(technique))
            {
                std::string lowerName = Utf8Stream::lowerCaseUtf8(name);
                std::string lowerCaption = mFilter->getCaption();
                lowerCaption = Utf8Stream::lowerCaseUtf8(lowerCaption);
                if (lowerName.find(lowerCaption) != std::string::npos)
                    mInactiveList->addItem(name, technique);
            }
        }

        for (auto technique : processor->getTechniques())
        {
            if (!technique->getHidden())
                mActiveList->addItem(technique->getName(), technique);
        }

        auto tryFocus = [this](ListWrapper* widget, const std::string& hint)
        {
            MyGUI::Widget* oldFocus = MyGUI::InputManager::getInstance().getKeyFocusWidget();
            if (oldFocus == mFilter)
                return;

            size_t index = widget->findItemIndexWith(hint);

            if (index != MyGUI::ITEM_NONE)
            {
                MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(widget);
                select(widget, index);
            }
        };

        if (!mOverrideHint.empty())
        {
            tryFocus(mActiveList, mOverrideHint);
            tryFocus(mInactiveList, mOverrideHint);

            mOverrideHint.clear();
        }
        else if (hintWidget && !hint.empty())
            tryFocus(hintWidget, hint);
    }

    void PostProcessorHud::registerMyGUIComponents()
    {
        MyGUI::FactoryManager& factory = MyGUI::FactoryManager::getInstance();
        factory.registerFactory<fx::Widgets::UniformBase>("Widget");
        factory.registerFactory<fx::Widgets::EditNumberFloat4>("Widget");
        factory.registerFactory<fx::Widgets::EditNumberFloat3>("Widget");
        factory.registerFactory<fx::Widgets::EditNumberFloat2>("Widget");
        factory.registerFactory<fx::Widgets::EditNumberFloat>("Widget");
        factory.registerFactory<fx::Widgets::EditNumberInt>("Widget");
        factory.registerFactory<fx::Widgets::EditBool>("Widget");
        factory.registerFactory<ListWrapper>("Widget");
    }
}
