#pragma once

#include <cstdint>
#include <string>
#include <string_view>
#include <utility>
#include <variant>

#include "adapters/il2cpp/Il2CppApi.h"
#include "external/il2cpp/il2cpp-api-types.h"

namespace ta::adapters::il2cpp
{
    enum class LookupPolicy
    {
        ExactClass,
        IncludeBaseClasses
    };

    enum class InteropErrorCode
    {
        InvalidObject,
        FieldNotFound,
        MethodNotFound,
        NullMethodPointer
    };

    struct InteropError
    {
        InteropErrorCode code = InteropErrorCode::InvalidObject;
        std::string type;
        std::string member;
    };

    template <typename TValue>
    class InteropResult
    {
    public:
        static InteropResult success(TValue value)
        {
            return InteropResult(std::move(value));
        }

        static InteropResult failure(InteropError error)
        {
            return InteropResult(std::move(error));
        }

        [[nodiscard]] bool has_value() const noexcept
        {
            return std::holds_alternative<TValue>(storage_);
        }

        [[nodiscard]] explicit operator bool() const noexcept
        {
            return has_value();
        }

        [[nodiscard]] TValue& value() & { return std::get<TValue>(storage_); }
        [[nodiscard]] const TValue& value() const& { return std::get<TValue>(storage_); }
        [[nodiscard]] TValue&& value() && { return std::get<TValue>(std::move(storage_)); }
        [[nodiscard]] const InteropError& error() const { return std::get<InteropError>(storage_); }

    private:
        explicit InteropResult(TValue value) : storage_(std::move(value)) {}
        explicit InteropResult(InteropError error) : storage_(std::move(error)) {}

        std::variant<TValue, InteropError> storage_;
    };

    class ManagedRuntime final
    {
    public:
        [[nodiscard]] static InteropResult<const FieldInfo*> field(
            const Il2CppClass* klass,
            const std::string_view name,
            const LookupPolicy policy = LookupPolicy::IncludeBaseClasses)
        {
            if (!klass || name.empty())
                return InteropResult<const FieldInfo*>::failure(make_error(
                    InteropErrorCode::InvalidObject, klass, name));

            const std::string member(name);
            for (const Il2CppClass* current = klass; current; current =
                    policy == LookupPolicy::IncludeBaseClasses ? current->parent : nullptr)
            {
                if (FieldInfo* resolved = il2cpp_class_get_field_from_name(
                        const_cast<Il2CppClass*>(current), member.c_str()))
                {
                    return InteropResult<const FieldInfo*>::success(resolved);
                }
            }
            return InteropResult<const FieldInfo*>::failure(make_error(
                InteropErrorCode::FieldNotFound, klass, name));
        }

        [[nodiscard]] static InteropResult<const MethodInfo*> method(
            const Il2CppClass* klass,
            const std::string_view name,
            const int argument_count,
            const LookupPolicy policy = LookupPolicy::IncludeBaseClasses)
        {
            if (!klass || name.empty())
                return InteropResult<const MethodInfo*>::failure(make_error(
                    InteropErrorCode::InvalidObject, klass, name));

            const std::string member(name);
            for (const Il2CppClass* current = klass; current; current =
                    policy == LookupPolicy::IncludeBaseClasses ? current->parent : nullptr)
            {
                const MethodInfo* resolved = il2cpp_class_get_method_from_name(
                    const_cast<Il2CppClass*>(current), member.c_str(), argument_count);
                if (!resolved)
                    continue;
                if (!resolved->methodPointer)
                    return InteropResult<const MethodInfo*>::failure(make_error(
                        InteropErrorCode::NullMethodPointer, klass, name));
                return InteropResult<const MethodInfo*>::success(resolved);
            }
            return InteropResult<const MethodInfo*>::failure(make_error(
                InteropErrorCode::MethodNotFound, klass, name));
        }

        [[nodiscard]] static const FieldInfo* field_or_null(
            const Il2CppClass* klass,
            const std::string_view name,
            const LookupPolicy policy = LookupPolicy::IncludeBaseClasses)
        {
            const auto result = field(klass, name, policy);
            return result ? result.value() : nullptr;
        }

        [[nodiscard]] static const MethodInfo* method_or_null(
            const Il2CppClass* klass,
            const std::string_view name,
            const int argument_count,
            const LookupPolicy policy = LookupPolicy::IncludeBaseClasses)
        {
            const auto result = method(klass, name, argument_count, policy);
            return result ? result.value() : nullptr;
        }

        template <typename TValue>
        [[nodiscard]] static InteropResult<TValue> read_field(
            Il2CppObject* object,
            const std::string_view name,
            const LookupPolicy policy = LookupPolicy::IncludeBaseClasses)
        {
            if (!object || !object->klass)
                return InteropResult<TValue>::failure(make_error(
                    InteropErrorCode::InvalidObject, object ? object->klass : nullptr, name));

            const auto resolved = field(object->klass, name, policy);
            if (!resolved)
                return InteropResult<TValue>::failure(resolved.error());

            return InteropResult<TValue>::success(*reinterpret_cast<TValue*>(
                reinterpret_cast<std::uintptr_t>(object) + resolved.value()->offset));
        }

        template <typename TValue>
        [[nodiscard]] static bool write_field(
            Il2CppObject* object,
            const std::string_view name,
            TValue value,
            const LookupPolicy policy = LookupPolicy::IncludeBaseClasses)
        {
            if (!object || !object->klass)
                return false;
            const auto resolved = field(object->klass, name, policy);
            if (!resolved)
                return false;
            *reinterpret_cast<TValue*>(
                reinterpret_cast<std::uintptr_t>(object) + resolved.value()->offset) = std::move(value);
            return true;
        }

        template <typename TResult, typename... TArgs>
        [[nodiscard]] static InteropResult<TResult> invoke(
            Il2CppObject* object,
            const std::string_view name,
            const LookupPolicy policy,
            TArgs... args)
        {
            if (!object || !object->klass)
                return InteropResult<TResult>::failure(make_error(
                    InteropErrorCode::InvalidObject, object ? object->klass : nullptr, name));

            const auto resolved = method(object->klass, name, sizeof...(TArgs), policy);
            if (!resolved)
                return InteropResult<TResult>::failure(resolved.error());

            using Function = TResult(*)(Il2CppObject*, TArgs...);
            return InteropResult<TResult>::success(
                reinterpret_cast<Function>(resolved.value()->methodPointer)(object, args...));
        }

        template <typename TResult, typename... TArgs>
        [[nodiscard]] static InteropResult<TResult> invoke_with_method_info(
            Il2CppObject* object,
            const std::string_view name,
            const LookupPolicy policy,
            TArgs... args)
        {
            if (!object || !object->klass)
                return InteropResult<TResult>::failure(make_error(
                    InteropErrorCode::InvalidObject, object ? object->klass : nullptr, name));

            const auto resolved = method(object->klass, name, sizeof...(TArgs), policy);
            if (!resolved)
                return InteropResult<TResult>::failure(resolved.error());

            using Function = TResult(*)(Il2CppObject*, TArgs..., const MethodInfo*);
            return InteropResult<TResult>::success(
                reinterpret_cast<Function>(resolved.value()->methodPointer)(
                    object, args..., resolved.value()));
        }

        template <typename TFunction>
        [[nodiscard]] static TFunction function(
            const Il2CppClass* klass,
            const std::string_view name,
            const int argument_count,
            const LookupPolicy policy = LookupPolicy::IncludeBaseClasses)
        {
            const auto resolved = method(klass, name, argument_count, policy);
            return resolved ? reinterpret_cast<TFunction>(resolved.value()->methodPointer) : TFunction{};
        }

        [[nodiscard]] static std::string class_chain_name(const Il2CppClass* klass)
        {
            std::string result;
            for (const Il2CppClass* current = klass; current; current = current->parent)
            {
                if (!current->name)
                    continue;
                if (!result.empty())
                    result.push_back(' ');
                result.append(current->name);
            }
            return result;
        }

    private:
        [[nodiscard]] static InteropError make_error(
            const InteropErrorCode code,
            const Il2CppClass* klass,
            const std::string_view member)
        {
            return {
                code,
                klass && klass->name ? klass->name : "?",
                std::string(member)
            };
        }
    };
}
