#ifndef OPENMW_COMPONENTS_SETTINGS_SETTINGVALUE_H
#define OPENMW_COMPONENTS_SETTINGS_SETTINGVALUE_H

#include "gyroscopeaxis.hpp"
#include "navmeshrendermode.hpp"
#include "sanitizer.hpp"
#include "settings.hpp"

#include "components/debug/debuglog.hpp"
#include "components/detournavigator/collisionshapetype.hpp"

#include <osg/io_utils>

#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace Settings
{
    enum class SettingValueType
    {
        Bool,
        Int,
        UnsignedInt,
        Long,
        UnsignedLong,
        LongLong,
        UnsignedLongLong,
        Float,
        Double,
        String,
        Vec2f,
        Vec3f,
        CollisionShapeType,
        StringArray,
        MyGuiColour,
        GyroscopeAxis,
        NavMeshRenderMode,
        LightingMethod,
        HrtfMode,
        WindowMode,
        VSyncMode,
        ScreenshotSettings,
    };

    template <class T>
    constexpr SettingValueType getSettingValueType() = delete;

    template <>
    inline constexpr SettingValueType getSettingValueType<bool>()
    {
        return SettingValueType::Bool;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<int>()
    {
        return SettingValueType::Int;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<unsigned int>()
    {
        return SettingValueType::UnsignedInt;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<long>()
    {
        return SettingValueType::Long;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<unsigned long>()
    {
        return SettingValueType::UnsignedLong;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<long long>()
    {
        return SettingValueType::LongLong;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<unsigned long long>()
    {
        return SettingValueType::UnsignedLongLong;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<float>()
    {
        return SettingValueType::Float;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<double>()
    {
        return SettingValueType::Double;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<std::string>()
    {
        return SettingValueType::String;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<osg::Vec2f>()
    {
        return SettingValueType::Vec2f;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<osg::Vec3f>()
    {
        return SettingValueType::Vec3f;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<DetourNavigator::CollisionShapeType>()
    {
        return SettingValueType::CollisionShapeType;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<std::vector<std::string>>()
    {
        return SettingValueType::StringArray;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<MyGUI::Colour>()
    {
        return SettingValueType::MyGuiColour;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<GyroscopeAxis>()
    {
        return SettingValueType::GyroscopeAxis;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<NavMeshRenderMode>()
    {
        return SettingValueType::NavMeshRenderMode;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<SceneUtil::LightingMethod>()
    {
        return SettingValueType::LightingMethod;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<HrtfMode>()
    {
        return SettingValueType::HrtfMode;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<WindowMode>()
    {
        return SettingValueType::WindowMode;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<SDLUtil::VSyncMode>()
    {
        return SettingValueType::VSyncMode;
    }

    template <>
    inline constexpr SettingValueType getSettingValueType<ScreenshotSettings>()
    {
        return SettingValueType::ScreenshotSettings;
    }

    inline constexpr std::string_view getSettingValueTypeName(SettingValueType type)
    {
        switch (type)
        {
            case SettingValueType::Bool:
                return "bool";
            case SettingValueType::Int:
                return "int";
            case SettingValueType::UnsignedInt:
                return "unsigned int";
            case SettingValueType::Long:
                return "long";
            case SettingValueType::UnsignedLong:
                return "unsigned long";
            case SettingValueType::LongLong:
                return "long long";
            case SettingValueType::UnsignedLongLong:
                return "unsigned long long";
            case SettingValueType::Float:
                return "float";
            case SettingValueType::Double:
                return "double";
            case SettingValueType::String:
                return "string";
            case SettingValueType::Vec2f:
                return "vec2f";
            case SettingValueType::Vec3f:
                return "vec3f";
            case SettingValueType::CollisionShapeType:
                return "collision shape type";
            case SettingValueType::StringArray:
                return "string array";
            case SettingValueType::MyGuiColour:
                return "colour";
            case SettingValueType::GyroscopeAxis:
                return "gyroscope axis";
            case SettingValueType::NavMeshRenderMode:
                return "navmesh render mode";
            case SettingValueType::LightingMethod:
                return "lighting method";
            case SettingValueType::HrtfMode:
                return "hrtf mode";
            case SettingValueType::WindowMode:
                return "window mode";
            case SettingValueType::VSyncMode:
                return "vsync mode";
            case SettingValueType::ScreenshotSettings:
                return "screenshot settings";
        }
        return "unsupported";
    }

    template <class T>
    constexpr std::string_view getSettingValueTypeName()
    {
        return getSettingValueTypeName(getSettingValueType<T>());
    }

    inline std::string getSettingDescription(SettingValueType type, std::string_view category, std::string_view name)
    {
        return std::string(getSettingValueTypeName(type)) + " [" + std::string(category) + "] " + std::string(name)
            + " setting";
    }

    template <class T>
    std::string getSettingDescription(std::string_view category, std::string_view name)
    {
        return getSettingDescription(getSettingValueType<T>(), category, name);
    }

    class Index;

    class BaseSettingValue
    {
    public:
        const SettingValueType mType;
        const std::string_view mCategory;
        const std::string_view mName;

        explicit BaseSettingValue(
            SettingValueType type, std::string_view category, std::string_view name, Index& index);

        BaseSettingValue(const BaseSettingValue& other) = delete;

        BaseSettingValue(BaseSettingValue&& other);

        BaseSettingValue& operator=(const BaseSettingValue& other) = delete;

        BaseSettingValue& operator=(BaseSettingValue&& other) = delete;

    private:
        Index& mIndex;
    };

    template <class T>
    class SettingValue;

    class Index
    {
    public:
        template <class T>
        SettingValue<T>* find(std::string_view category, std::string_view name) const;

        template <class T>
        SettingValue<T>& get(std::string_view category, std::string_view name) const;

        void insert(BaseSettingValue* value)
        {
            if (!mValues.emplace(std::make_pair(value->mCategory, value->mName), value).second)
                throw std::invalid_argument("Duplicated setting definition: "
                    + getSettingDescription(value->mType, value->mCategory, value->mName));
        }

        void insertOrAssign(BaseSettingValue* value)
        {
            mValues.insert_or_assign(std::make_pair(value->mCategory, value->mName), value);
        }

    private:
        std::map<std::pair<std::string_view, std::string_view>, BaseSettingValue*> mValues;
    };

    inline BaseSettingValue::BaseSettingValue(
        SettingValueType type, std::string_view category, std::string_view name, Index& index)
        : mType(type)
        , mCategory(category)
        , mName(name)
        , mIndex(index)
    {
        mIndex.insert(this);
    }

    inline BaseSettingValue::BaseSettingValue(BaseSettingValue&& other)
        : mType(other.mType)
        , mCategory(other.mCategory)
        , mName(other.mName)
        , mIndex(other.mIndex)
    {
        mIndex.insertOrAssign(this);
    }

    template <class T>
    class SettingValue final : public BaseSettingValue
    {
    public:
        explicit SettingValue(Index& index, std::string_view category, std::string_view name,
            std::unique_ptr<const Sanitizer<T>>&& sanitizer = nullptr)
            : BaseSettingValue(getSettingValueType<T>(), category, name, index)
            , mSanitizer(std::move(sanitizer))
            , mValue(sanitize(Settings::Manager::get<T>(mName, mCategory)))
        {
        }

        explicit SettingValue(Index& index, std::string_view category, std::string_view name, T&& defaultValue,
            std::unique_ptr<const Sanitizer<T>>&& sanitizer = nullptr)
            : BaseSettingValue(getSettingValueType<T>(), category, name, index)
            , mSanitizer(std::move(sanitizer))
            , mDefaultValue(sanitize(defaultValue))
            , mValue(sanitize(Settings::Manager::getOrDefault<T>(mName, mCategory, mDefaultValue)))
        {
        }

        SettingValue(SettingValue&& other)
            : BaseSettingValue(std::move(other))
            , mSanitizer(std::move(other.mSanitizer))
            , mDefaultValue(std::move(other.mValue))
            , mValue(sanitize(Settings::Manager::get<T>(mName, mCategory)))
        {
        }

        SettingValue(const SettingValue& other) = delete;

        SettingValue& operator=(const SettingValue& other) = delete;

        SettingValue& operator=(SettingValue&& other) = delete;

        const T& get() const { return mValue; }

        operator const T&() const { return mValue; }

        void set(const T& value)
        {
            if (mValue == value)
                return;
            mValue = sanitize(value);
            Settings::Manager::set(mName, mCategory, mValue);
        }

        void reset() { set(mDefaultValue); }

    private:
        std::unique_ptr<const Sanitizer<T>> mSanitizer;
        T mDefaultValue{};
        T mValue{};

        struct WriteValue
        {
            const T& mValue;

            friend std::ostream& operator<<(std::ostream& stream, const WriteValue& value)
            {
                if constexpr (std::is_enum_v<T>)
                    return stream << static_cast<std::underlying_type_t<T>>(value.mValue);
                else if constexpr (std::is_same_v<T, std::vector<std::string>>)
                {
                    bool first = true;
                    for (const std::string& v : value.mValue)
                    {
                        if (std::exchange(first, false))
                            stream << v;
                        else
                            stream << "," << v;
                    }
                    return stream;
                }
                else if constexpr (std::is_same_v<T, ScreenshotSettings>)
                {
                    stream << "ScreenshotSettings{ .mType = " << static_cast<int>(value.mValue.mType);
                    if (value.mValue.mWidth.has_value())
                        stream << ", .mWidth = " << *value.mValue.mWidth;
                    if (value.mValue.mHeight.has_value())
                        stream << ", .mHeight = " << *value.mValue.mHeight;
                    if (value.mValue.mCubeSize.has_value())
                        stream << ", .mCubeSize = " << *value.mValue.mCubeSize;
                    return stream << " }";
                }
                else
                    return stream << value.mValue;
            }
        };

        T sanitize(const T& value) const
        {
            if (mSanitizer == nullptr)
                return value;
            try
            {
                T sanitizedValue = mSanitizer->apply(value);
                if (sanitizedValue != value)
                    Log(Debug::Warning) << getSettingDescription<T>(mCategory, mName)
                                        << " value is out of allowed values set: " << WriteValue{ value }
                                        << ", sanitized to " << WriteValue{ sanitizedValue };
                return sanitizedValue;
            }
            catch (const std::exception& e)
            {
                throw std::runtime_error(
                    "Invalid " + getSettingDescription<T>(mCategory, mName) + " value: " + std::string(e.what()));
            }
        }
    };

    template <class T>
    SettingValue<T>* Index::find(std::string_view category, std::string_view name) const
    {
        const auto it = mValues.find({ category, name });
        if (it == mValues.end())
            return nullptr;
        BaseSettingValue* const value = it->second;
        if (value->mType != getSettingValueType<T>())
            throw std::invalid_argument(getSettingDescription(value->mType, category, name)
                + " does not match requested type: " + std::string(getSettingValueTypeName<T>()));
        return static_cast<SettingValue<T>*>(value);
    }

    template <class T>
    SettingValue<T>& Index::get(std::string_view category, std::string_view name) const
    {
        SettingValue<T>* const result = find<T>(category, name);
        if (result == nullptr)
            throw std::invalid_argument(getSettingDescription<T>(category, name) + " is not found");
        return *result;
    }

    class WithIndex
    {
    public:
        explicit WithIndex(Index& index)
            : mIndex(index)
        {
        }

    protected:
        Index& mIndex;
    };
}

#endif