#include "adapters/unity/chams/resources/ChamsBundleRuntime.h"

#include <algorithm>
#include <cstdint>
#include <string>
#include <system_error>

#include "adapters/il2cpp/Il2CppExports.h"
#include "adapters/il2cpp/Il2CppResolver.h"
#include "adapters/il2cpp/Il2CppObjectAccess.h"
#include "adapters/il2cpp/Il2CppString.h"
#include "adapters/il2cpp/Il2CppObjectInstance.h"
#include "foundation/diagnostics/Logger.h"
#include "application/lifecycle/RuntimeState.h"

namespace ta::adapters::unity::chams
{
    namespace
    {
        constexpr std::uint32_t kMaxLoadAttempts = 5;
        constexpr std::uint32_t kRetryDelayFrames = 300;
        std::string managed_path_string(const std::filesystem::path& path)
        {
            const auto utf8_path = path.u8string();
            return {
                reinterpret_cast<const char*>(utf8_path.data()),
                utf8_path.size()
            };
        }
    }

    ChamsBundleRuntime& ChamsBundleRuntime::instance() noexcept
    {
        static ChamsBundleRuntime runtime;
        return runtime;
    }

    bool ChamsBundleRuntime::initialize(std::filesystem::path bundle_path) noexcept
    {
        reset();
        try
        {
            std::error_code ec;
            if (!std::filesystem::is_regular_file(bundle_path, ec) || ec)
            {
                ta::foundation::logger::error(
                    "RESOURCE",
                    "chams bundle unavailable | path=" + bundle_path.string());
                return false;
            }

            bundle_path_ = std::move(bundle_path);
            initialized_ = true;
            return true;
        }
        catch (...)
        {
            ta::foundation::logger::error("RESOURCE", "chams bundle runtime initialization failed");
            reset();
            return false;
        }
    }

    const char* ChamsBundleRuntime::failure_name(const LoadFailure failure) noexcept
    {
        switch (failure)
        {
        case LoadFailure::None: return "none";
        case LoadFailure::AssetBundleImageUnavailable: return "asset-bundle image unavailable";
        case LoadFailure::CoreImageUnavailable: return "core image unavailable";
        case LoadFailure::ClassUnavailable: return "AssetBundle class unavailable";
        case LoadFailure::MethodUnavailable: return "LoadFromFileAsync method unavailable";
        case LoadFailure::StringAllocationFailed: return "managed path allocation failed";
        case LoadFailure::RequestCreationFailed: return "Unity returned null async request";
        case LoadFailure::CompletionMethodUnavailable: return "AsyncOperation completion method unavailable";
        case LoadFailure::AssetBundleAccessorUnavailable: return "AssetBundleCreateRequest accessor unavailable";
        case LoadFailure::UnityReturnedNull: return "Unity returned null bundle";
        }
        return "unknown";
    }

    Il2CppObject* ChamsBundleRuntime::start_async_load(LoadFailure& failure)
    {
        failure = LoadFailure::None;

        const Il2CppImage* asset_bundle_module = ta::adapters::il2cpp::resolve_image("UnityEngine.AssetBundleModule.dll");
        if (!asset_bundle_module)
        {
            failure = LoadFailure::AssetBundleImageUnavailable;
            return nullptr;
        }

        // The request object derives from UnityEngine.AsyncOperation. Matching
        // the known-good loader keeps startup deferred until both Unity modules
        // are present in the IL2CPP domain.
        if (!ta::adapters::il2cpp::resolve_image("UnityEngine.CoreModule.dll"))
        {
            failure = LoadFailure::CoreImageUnavailable;
            return nullptr;
        }

        const Il2CppClass* asset_bundle_class = ta::adapters::il2cpp::resolve_class(
            asset_bundle_module,
            "UnityEngine",
            "AssetBundle");
        if (!asset_bundle_class)
        {
            failure = LoadFailure::ClassUnavailable;
            return nullptr;
        }

        if (!ta::adapters::il2cpp::exports::api().il2cpp_string_new)
        {
            failure = LoadFailure::StringAllocationFailed;
            return nullptr;
        }

        const std::string path = managed_path_string(bundle_path_);
        Il2CppString* managed_path = ta::adapters::il2cpp::il2cpp_string_new(path.c_str());
        if (!managed_path)
        {
            failure = LoadFailure::StringAllocationFailed;
            return nullptr;
        }

        // Use the single supported AssetBundle loading path. If this method is
        // unavailable, report the binding error instead of trying another overload.
        const auto load_from_file_async =
            ta::adapters::il2cpp::get_method<Il2CppObject*(*)(Il2CppString*)>(
                asset_bundle_class,
                "LoadFromFileAsync",
                1);
        if (!load_from_file_async)
        {
            failure = LoadFailure::MethodUnavailable;
            return nullptr;
        }

        Il2CppObject* request = load_from_file_async(managed_path);
        if (!request)
            failure = LoadFailure::RequestCreationFailed;
        return request;
    }

    ChamsBundleRuntime::PollResult ChamsBundleRuntime::poll_async_load(LoadFailure& failure)
    {
        failure = LoadFailure::None;
        if (!request_)
        {
            failure = LoadFailure::RequestCreationFailed;
            return PollResult::Failed;
        }

        ta::adapters::il2cpp::Il2CppObjectInstance async_operation(request_);
        const auto is_done = async_operation.get_method<bool(*)(Il2CppObject*)>("get_isDone", 0);
        if (!is_done)
        {
            failure = LoadFailure::CompletionMethodUnavailable;
            return PollResult::Failed;
        }

        if (!is_done(request_))
        {
            const auto get_progress = async_operation.get_method<float(*)(Il2CppObject*)>("get_progress", 0);
            if (get_progress)
            {
                const int percent = static_cast<int>(get_progress(request_) * 100.0f);
                const int bucket = (std::max)(0, (std::min)(100, percent)) / 10 * 10;
                if (bucket != last_progress_bucket_)
                {
                    last_progress_bucket_ = bucket;
                    ta::foundation::logger::info(
                        "RESOURCE",
                        "chams bundle loading | progress=" + std::to_string(bucket) + "%");
                }
            }
            return PollResult::Pending;
        }

        ta::adapters::il2cpp::Il2CppObjectInstance request_instance(request_);
        const auto get_asset_bundle = request_instance.get_method<Il2CppObject*(*)(Il2CppObject*)>(
            "get_assetBundle",
            0);
        if (!get_asset_bundle)
        {
            failure = LoadFailure::AssetBundleAccessorUnavailable;
            return PollResult::Failed;
        }

        bundle_ = get_asset_bundle(request_);
        request_ = nullptr;
        if (!bundle_)
        {
            failure = LoadFailure::UnityReturnedNull;
            return PollResult::Failed;
        }

        return PollResult::Loaded;
    }

    void ChamsBundleRuntime::schedule_retry(const LoadFailure failure)
    {
        request_ = nullptr;
        last_progress_bucket_ = -1;

        if (load_attempts_ < kMaxLoadAttempts)
        {
            retry_frames_ = kRetryDelayFrames;
            ta::foundation::logger::warn(
                "RESOURCE",
                std::string("chams bundle async load failed | reason=") + failure_name(failure) +
                " | retry_in_frames=" + std::to_string(kRetryDelayFrames));
            return;
        }

        ta::foundation::logger::error(
            "RESOURCE",
            std::string("chams bundle load exhausted retries | reason=") + failure_name(failure) +
            " | attempts=" + std::to_string(load_attempts_));
    }

    void ChamsBundleRuntime::tick()
    {
        if (!initialized_ || bundle_)
            return;
        if (!request_ && load_attempts_ >= kMaxLoadAttempts)
            return;

        if (!ta::application::runtime::is_application_update_thread())
            return;

        if (request_)
        {
            LoadFailure failure = LoadFailure::None;
            switch (poll_async_load(failure))
            {
            case PollResult::Pending:
                return;
            case PollResult::Loaded:
                ta::foundation::logger::info("RESOURCE", "chams bundle loaded | path=" + bundle_path_.string());
                return;
            case PollResult::Failed:
                schedule_retry(failure);
                return;
            }
        }

        if (retry_frames_ > 0)
        {
            --retry_frames_;
            return;
        }

        ++load_attempts_;
        last_progress_bucket_ = -1;
        LoadFailure failure = LoadFailure::None;
        request_ = start_async_load(failure);
        if (!request_)
            schedule_retry(failure);
    }

    void ChamsBundleRuntime::reset() noexcept
    {
        // Shutdown runs outside Unity's application thread. Do not call
        // AssetBundle.Unload or mutate the in-flight request through Unity APIs.
        request_ = nullptr;
        bundle_ = nullptr;
        load_attempts_ = 0;
        retry_frames_ = 0;
        last_progress_bucket_ = -1;
        initialized_ = false;
        bundle_path_.clear();
    }

    Il2CppObject* ChamsBundleRuntime::current_bundle() const noexcept
    {
        return bundle_;
    }

    Il2CppObject* ChamsBundleRuntime::load_asset(const char* name, Il2CppObject* system_type) const
    {
        if (!bundle_ || !name || !*name || !system_type || !ta::adapters::il2cpp::exports::api().il2cpp_string_new ||
            !ta::application::runtime::is_application_update_thread())
        {
            return nullptr;
        }

        ta::adapters::il2cpp::Il2CppObjectInstance bundle_instance(bundle_);
        const auto load_asset_method = bundle_instance.get_method<Il2CppObject*(*)(
            Il2CppObject*, Il2CppString*, Il2CppObject*)>("LoadAsset", 2);
        if (!load_asset_method)
            return nullptr;
        Il2CppString* managed_name = ta::adapters::il2cpp::il2cpp_string_new(name);
        return managed_name ? load_asset_method(bundle_, managed_name, system_type) : nullptr;
    }
}
