// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "../luaengine.h"

#include <utils/aspects.h>
#include <utils/environment.h>
#include <utils/layoutbuilder.h>

#include <coreplugin/dialogs/ioptionspage.h>

using namespace Utils;

namespace Lua::Internal {

class LuaAspectContainer : public AspectContainer
{
public:
    using AspectContainer::AspectContainer;

    sol::object dynamic_get(const std::string &key)
    {
        auto it = m_entries.find(key);
        if (it == m_entries.cend()) {
            return sol::lua_nil;
        }
        return it->second;
    }

    void dynamic_set(const std::string &key, const sol::stack_object &value)
    {
        if (!value.is<BaseAspect>())
            throw std::runtime_error("AspectContainer can only contain BaseAspect instances");

        registerAspect(value.as<BaseAspect *>(), false);

        auto it = m_entries.find(key);
        if (it == m_entries.cend()) {
            m_entries.insert(it, {std::move(key), std::move(value)});
        } else {
            std::pair<const std::string, sol::object> &kvp = *it;
            sol::object &entry = kvp.second;
            entry = sol::object(std::move(value));
        }
    }

    size_t size() const { return m_entries.size(); }

public:
    std::unordered_map<std::string, sol::object> m_entries;
};

std::unique_ptr<LuaAspectContainer> aspectContainerCreate(const sol::table &options)
{
    auto container = std::make_unique<LuaAspectContainer>();

    for (const auto &[k, v] : options) {
        if (k.is<std::string>()) {
            std::string key = k.as<std::string>();
            if (key == "autoApply") {
                container->setAutoApply(v.as<bool>());
            } else if (key == "layouter") {
                if (v.is<sol::function>())
                    container->setLayouter(
                        [func = v.as<sol::function>()]() -> Layouting::Layout {
                            auto res = Lua::LuaEngine::safe_call<Layouting::Layout>(func);
                            QTC_ASSERT_EXPECTED(res, return {});
                            return *res;
                        });
            } else {
                container->m_entries[key] = v;
                if (v.is<BaseAspect>()) {
                    container->registerAspect(v.as<BaseAspect *>());
                }
            }
        }
    }

    container->readSettings();

    return container;
}

void baseAspectCreate(BaseAspect *aspect, const std::string &key, const sol::object &value)
{
    if (key == "settingsKey")
        aspect->setSettingsKey(keyFromString(value.as<QString>()));
    else if (key == "displayName")
        aspect->setDisplayName(value.as<QString>());
    else if (key == "labelText")
        aspect->setLabelText(value.as<QString>());
    else if (key == "toolTip")
        aspect->setToolTip(value.as<QString>());
    else if (key == "onValueChanged") {
        QObject::connect(aspect, &BaseAspect::changed, aspect, [func = value.as<sol::function>()]() {
            Lua::LuaEngine::void_safe_call(func);
        });
    } else if (key == "onVolatileValueChanged") {
        QObject::connect(aspect,
                         &BaseAspect::volatileValueChanged,
                         aspect,
                         [func = value.as<sol::function>()]() {
                             Lua::LuaEngine::void_safe_call(func);
                         });
    } else if (key == "enabler")
        aspect->setEnabler(value.as<BoolAspect *>());
    else
        qWarning() << "Unknown key:" << key.c_str();
}

template<class T>
void typedAspectCreate(T *aspect, const std::string &key, const sol::object &value)
{
    if (key == "defaultValue")
        aspect->setDefaultValue(value.as<typename T::valueType>());
    else if (key == "value")
        aspect->setValue(value.as<typename T::valueType>());
    else
        baseAspectCreate(aspect, key, value);
}

template<>
void typedAspectCreate(StringAspect *aspect, const std::string &key, const sol::object &value)
{
    if (key == "displayStyle")
        aspect->setDisplayStyle((StringAspect::DisplayStyle) value.as<int>());
    else if (key == "historyId")
        aspect->setHistoryCompleter(value.as<QString>().toLocal8Bit());
    else if (key == "valueAcceptor")
        aspect->setValueAcceptor([func = value.as<sol::function>()](const QString &oldValue,
                                                                    const QString &newValue)
                                     -> std::optional<QString> {
            auto res = Lua::LuaEngine::safe_call<std::optional<QString>>(func, oldValue, newValue);
            QTC_ASSERT_EXPECTED(res, return std::nullopt);
            return *res;
        });
    else if (key == "showToolTipOnLabel")
        aspect->setShowToolTipOnLabel(value.as<bool>());
    else if (key == "displayFilter")
        aspect->setDisplayFilter([func = value.as<sol::function>()](const QString &value) {
            auto res = Lua::LuaEngine::safe_call<QString>(func, value);
            QTC_ASSERT_EXPECTED(res, return value);
            return *res;
        });
    else if (key == "placeHolderText")
        aspect->setPlaceHolderText(value.as<QString>());
    else if (key == "acceptRichText")
        aspect->setAcceptRichText(value.as<bool>());
    else if (key == "autoApplyOnEditingFinished")
        aspect->setAutoApplyOnEditingFinished(value.as<bool>());
    else if (key == "elideMode")
        aspect->setElideMode((Qt::TextElideMode) value.as<int>());
    else
        typedAspectCreate(static_cast<TypedAspect<QString> *>(aspect), key, value);
}

template<>
void typedAspectCreate(FilePathAspect *aspect, const std::string &key, const sol::object &value)
{
    if (key == "defaultPath")
        aspect->setDefaultPathValue(value.as<FilePath>());
    else if (key == "historyId")
        aspect->setHistoryCompleter(value.as<QString>().toLocal8Bit());
    else if (key == "promptDialogFilter")
        aspect->setPromptDialogFilter(value.as<QString>());
    else if (key == "promptDialogTitle")
        aspect->setPromptDialogTitle(value.as<QString>());
    else if (key == "commandVersionArguments")
        aspect->setCommandVersionArguments(value.as<QStringList>());
    else if (key == "allowPathFromDevice")
        aspect->setAllowPathFromDevice(value.as<bool>());
    else if (key == "validatePlaceHolder")
        aspect->setValidatePlaceHolder(value.as<bool>());
    else if (key == "openTerminalHandler")
        aspect->setOpenTerminalHandler([func = value.as<sol::function>()]() {
            auto res = Lua::LuaEngine::void_safe_call(func);
            QTC_CHECK_EXPECTED(res);
        });
    else if (key == "expectedKind")
        aspect->setExpectedKind((PathChooser::Kind) value.as<int>());
    else if (key == "environment")
        aspect->setEnvironment(value.as<Environment>());
    else if (key == "baseFileName")
        aspect->setBaseFileName(value.as<FilePath>());
    else if (key == "valueAcceptor")
        aspect->setValueAcceptor([func = value.as<sol::function>()](const QString &oldValue,
                                                                    const QString &newValue)
                                     -> std::optional<QString> {
            auto res = Lua::LuaEngine::safe_call<std::optional<QString>>(func, oldValue, newValue);
            QTC_ASSERT_EXPECTED(res, return std::nullopt);
            return *res;
        });
    else if (key == "showToolTipOnLabel")
        aspect->setShowToolTipOnLabel(value.as<bool>());
    else if (key == "autoApplyOnEditingFinished")
        aspect->setAutoApplyOnEditingFinished(value.as<bool>());
    /*else if (key == "validationFunction")
        aspect->setValidationFunction(
            [func = value.as<sol::function>()](const QString &path) {
                return func.call<std::optional<QString>>(path);
            });
    */
    else if (key == "displayFilter")
        aspect->setDisplayFilter([func = value.as<sol::function>()](const QString &path) {
            auto res = Lua::LuaEngine::safe_call<QString>(func, path);
            QTC_ASSERT_EXPECTED(res, return path);
            return *res;
        });
    else if (key == "placeHolderText")
        aspect->setPlaceHolderText(value.as<QString>());
    else
        typedAspectCreate(static_cast<TypedAspect<QString> *>(aspect), key, value);
}

template<>
void typedAspectCreate(BoolAspect *aspect, const std::string &key, const sol::object &value)
{
    if (key == "labelPlacement") {
        aspect->setLabelPlacement((BoolAspect::LabelPlacement) value.as<int>());
    } else {
        typedAspectCreate(static_cast<TypedAspect<bool> *>(aspect), key, value);
    }
}

template<class T>
std::unique_ptr<T> createAspectFromTable(
    const sol::table &options, const std::function<void(T *, const std::string &, sol::object)> &f)
{
    auto aspect = std::make_unique<T>();

    for (const auto &[k, v] : options) {
        if (k.template is<std::string>()) {
            f(aspect.get(), k.template as<std::string>(), v);
        }
    }

    return aspect;
}

template<class T>
void addTypedAspectBaseBindings(sol::table &lua)
{
    lua.new_usertype<TypedAspect<T>>("TypedAspect<bool>",
                                     "value",
                                     sol::property(&TypedAspect<T>::value,
                                                   [](TypedAspect<T> *a, const T &v) {
                                                       a->setValue(v);
                                                   }),
                                     "volatileValue",
                                     sol::property(&TypedAspect<T>::volatileValue,
                                                   [](TypedAspect<T> *a, const T &v) {
                                                       a->setVolatileValue(v);
                                                   }),
                                     "defaultValue",
                                     sol::property(&TypedAspect<T>::defaultValue),
                                     sol::base_classes,
                                     sol::bases<BaseAspect>());
}

template<class T>
sol::usertype<T> addTypedAspect(sol::table &lua, const QString &name)
{
    addTypedAspectBaseBindings<typename T::valueType>(lua);

    return lua.new_usertype<T>(
        name,
        "create",
        [](const sol::table &options) {
            return createAspectFromTable<T>(options, &typedAspectCreate<T>);
        },
        sol::base_classes,
        sol::bases<TypedAspect<typename T::valueType>, BaseAspect>());
}

void addSettingsModule()
{
    LuaEngine::registerProvider("Settings", [](sol::state_view l) -> sol::object {
        sol::table settings = l.create_table();

        settings.new_usertype<BaseAspect>("Aspect", "apply", &BaseAspect::apply);

        settings.new_usertype<LuaAspectContainer>("AspectContainer",
                                                  "create",
                                                  &aspectContainerCreate,
                                                  "apply",
                                                  &LuaAspectContainer::apply,
                                                  sol::meta_function::index,
                                                  &LuaAspectContainer::dynamic_get,
                                                  sol::meta_function::new_index,
                                                  &LuaAspectContainer::dynamic_set,
                                                  sol::meta_function::length,
                                                  &LuaAspectContainer::size,
                                                  sol::base_classes,
                                                  sol::bases<AspectContainer, BaseAspect>());

        addTypedAspect<BoolAspect>(settings, "BoolAspect");
        addTypedAspect<ColorAspect>(settings, "ColorAspect");
        addTypedAspect<SelectionAspect>(settings, "SelectionAspect");
        addTypedAspect<MultiSelectionAspect>(settings, "MultiSelectionAspect");
        addTypedAspect<StringAspect>(settings, "StringAspect");

        auto filePathAspectType = addTypedAspect<FilePathAspect>(settings, "FilePathAspect");
        filePathAspectType.set(
            "setValue",
            sol::overload(
                [](FilePathAspect &self, const QString &value) {
                    self.setValue(FilePath::fromUserInput(value));
                },
                [](FilePathAspect &self, const FilePath &value) { self.setValue(value); }));
        filePathAspectType.set("expandedValue", sol::property(&FilePathAspect::expandedValue));
        filePathAspectType.set(
            "defaultPath",
            sol::property(
                [](FilePathAspect &self) { return FilePath::fromUserInput(self.defaultValue()); },
                &FilePathAspect::setDefaultPathValue));

        addTypedAspect<IntegerAspect>(settings, "IntegerAspect");
        addTypedAspect<DoubleAspect>(settings, "DoubleAspect");
        addTypedAspect<StringListAspect>(settings, "StringListAspect");
        addTypedAspect<FilePathListAspect>(settings, "FilePathListAspect");
        addTypedAspect<IntegersAspect>(settings, "IntegersAspect");
        addTypedAspect<StringSelectionAspect>(settings, "StringSelectionAspect");

        settings.new_usertype<ToggleAspect>(
            "ToggleAspect",
            "create",
            [](const sol::table &options) {
                return createAspectFromTable<ToggleAspect>(
                    options,
                    [](ToggleAspect *aspect, const std::string &key, const sol::object &value) {
                        if (key == "offIcon")
                            aspect->setOffIcon(QIcon(value.as<QString>()));
                        else if (key == "offTooltip")
                            aspect->setOffTooltip(value.as<QString>());
                        else if (key == "onIcon")
                            aspect->setOnIcon(QIcon(value.as<QString>()));
                        else if (key == "onTooltip")
                            aspect->setOnTooltip(value.as<QString>());
                        else if (key == "onText")
                            aspect->setOnText(value.as<QString>());
                        else if (key == "offText")
                            aspect->setOffText(value.as<QString>());
                        else
                            typedAspectCreate(aspect, key, value);
                    });
            },
            "action",
            &ToggleAspect::action,
            sol::base_classes,
            sol::bases<BoolAspect, TypedAspect<bool>, BaseAspect>());

        static auto triStateFromString = [](const QString &str) -> TriState {
            const QString l = str.toLower();
            if (l == "enabled")
                return TriState::Enabled;
            else if (l == "disabled")
                return TriState::Disabled;
            else if (l == "default")
                return TriState::Default;
            else
                return TriState::Default;
        };

        static auto triStateToString = [](TriState state) -> QString {
            if (state == TriState::Enabled)
                return "enabled";
            else if (state == TriState::Disabled)
                return "disabled";
            return "default";
        };

        settings.new_usertype<TriStateAspect>(
            "TriStateAspect",
            "create",
            [](const sol::table &options) {
                return createAspectFromTable<TriStateAspect>(
                    options,
                    [](TriStateAspect *aspect, const std::string &key, const sol::object &value) {
                        if (key == "defaultValue")
                            aspect->setDefaultValue(triStateFromString(value.as<QString>()));
                        else if (key == "value")
                            aspect->setValue(triStateFromString(value.as<QString>()));
                        else
                            baseAspectCreate(aspect, key, value);
                    });
            },
            "value",
            sol::property(
                [](TriStateAspect *a) { return triStateToString(a->value()); },
                [](TriStateAspect *a, const QString &v) { a->setValue(triStateFromString(v)); }),
            "volatileValue",
            sol::property(
                [](TriStateAspect *a) {
                    return triStateToString(TriState::fromInt(a->volatileValue()));
                },
                [](TriStateAspect *a, const QString &v) {
                    a->setVolatileValue(triStateFromString(v).toInt());
                }),
            "defaultValue",
            sol::property([](TriStateAspect *a) { return triStateToString(a->defaultValue()); }),
            sol::base_classes,
            sol::bases<SelectionAspect, TypedAspect<int>, BaseAspect>());

        settings.new_usertype<TextDisplay>(
            "TextDisplay",
            "create",
            [](const sol::table &options) {
                return createAspectFromTable<TextDisplay>(
                    options,
                    [](TextDisplay *aspect, const std::string &key, const sol::object &value) {
                        if (key == "text") {
                            aspect->setText(value.as<QString>());
                        } else if (key == "iconType") {
                            const QString type = value.as<QString>().toLower();

                            if (type.isEmpty() || type == "None")
                                aspect->setIconType(Utils::InfoLabel::InfoType::None);
                            else if (type == "information")
                                aspect->setIconType(Utils::InfoLabel::InfoType::Information);
                            else if (type == "warning")
                                aspect->setIconType(Utils::InfoLabel::InfoType::Warning);
                            else if (type == "error")
                                aspect->setIconType(Utils::InfoLabel::InfoType::Error);
                            else if (type == "ok")
                                aspect->setIconType(Utils::InfoLabel::InfoType::Ok);
                            else if (type == "notok")
                                aspect->setIconType(Utils::InfoLabel::InfoType::NotOk);
                            else
                                aspect->setIconType(Utils::InfoLabel::InfoType::None);
                        } else {
                            baseAspectCreate(aspect, key, value);
                        }
                    });
            },
            sol::base_classes,
            sol::bases<BaseAspect>());

        settings.new_usertype<AspectList>(
            "AspectList",
            "create",
            [](const sol::table &options) {
                return createAspectFromTable<AspectList>(
                    options,
                    [](AspectList *aspect, const std::string &key, const sol::object &value) {
                        if (key == "createItemFunction") {
                            aspect->setCreateItemFunction([func = value.as<sol::function>()]()
                                                              -> std::shared_ptr<BaseAspect> {
                                auto res = Lua::LuaEngine::safe_call<std::shared_ptr<BaseAspect>>(
                                    func);
                                QTC_ASSERT_EXPECTED(res, return nullptr);
                                return *res;
                            });
                        } else if (key == "onItemAdded") {
                            aspect->setItemAddedCallback([func = value.as<sol::function>()](
                                                             std::shared_ptr<BaseAspect> item) {
                                auto res = Lua::LuaEngine::void_safe_call(func, item);
                                QTC_CHECK_EXPECTED(res);
                            });
                        } else if (key == "onItemRemoved") {
                            aspect->setItemRemovedCallback([func = value.as<sol::function>()](
                                                               std::shared_ptr<BaseAspect> item) {
                                auto res = Lua::LuaEngine::void_safe_call(func, item);
                                QTC_CHECK_EXPECTED(res);
                            });
                        } else {
                            baseAspectCreate(aspect, key, value);
                        }
                    });
            },
            "createAndAddItem",
            &AspectList::createAndAddItem,
            "foreach",
            [](AspectList *a, const sol::function &clbk) {
                a->forEachItem<BaseAspect>([clbk](std::shared_ptr<BaseAspect> item) {
                    auto res = Lua::LuaEngine::void_safe_call(clbk, item);
                    QTC_CHECK_EXPECTED(res);
                });
            },
            "enumerate",
            [](AspectList *a, const sol::function &clbk) {
                a->forEachItem<BaseAspect>([clbk](std::shared_ptr<BaseAspect> item, int idx) {
                    auto res = Lua::LuaEngine::void_safe_call(clbk, item, idx);
                    QTC_CHECK_EXPECTED(res);
                });
            },
            sol::base_classes,
            sol::bases<BaseAspect>());

        class OptionsPage : public Core::IOptionsPage
        {
        public:
            OptionsPage(const sol::table &options)
            {
                setId(Id::fromString(options.get<QString>("id")));
                setDisplayName(options.get<QString>("displayName"));
                setCategory(Id::fromString(options.get<QString>("categoryId")));
                setDisplayCategory(options.get<QString>("displayCategory"));
                setCategoryIconPath(
                    FilePath::fromUserInput(options.get<QString>("categoryIconPath")));
                AspectContainer *container = options.get<AspectContainer *>("aspectContainer");
                setSettingsProvider([container]() { return container; });
            }
        };

        settings.new_usertype<OptionsPage>("OptionsPage", "create", [](const sol::table &options) {
            return std::make_unique<OptionsPage>(options);
        });

        // clang-format off
        settings["StringDisplayStyle"] = l.create_table_with(
            "Label", StringAspect::DisplayStyle::LabelDisplay,
            "LineEdit", StringAspect::DisplayStyle::LineEditDisplay,
            "TextEdit", StringAspect::DisplayStyle::TextEditDisplay,
            "PasswordLineEdit", StringAspect::DisplayStyle::PasswordLineEditDisplay
        );

        settings["CheckBoxPlacement"] = l.create_table_with(
            "Top", CheckBoxPlacement::Top,
            "Right", CheckBoxPlacement::Right
        );
        settings["Kind"] = l.create_table_with(
            "ExistingDirectory", PathChooser::Kind::ExistingDirectory,
            "Directory", PathChooser::Kind::Directory,
            "File", PathChooser::Kind::File,
            "SaveFile", PathChooser::Kind::SaveFile,
            "ExistingCommand", PathChooser::Kind::ExistingCommand,
            "Command", PathChooser::Kind::Command,
            "Any", PathChooser::Kind::Any
        );
        settings["LabelPlacement"] = l.create_table_with(
            "AtCheckBox", BoolAspect::LabelPlacement::AtCheckBox,
            "Compact", BoolAspect::LabelPlacement::Compact,
            "InExtraLabel", BoolAspect::LabelPlacement::InExtraLabel
        );
        // clang-format on

        return settings;
    });
}

} // namespace Lua::Internal
