#include "adapters/eft/esp/EftEspGateway.h"

#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>

#include <chrono>
#include <utility>

#include "application/lifecycle/RaidContext.h"

namespace ta::adapters::eft::esp
{

namespace
{
    bool refresh_due(const unsigned long long now,
                     const unsigned long long last_refresh,
                     const int interval_ms) noexcept
    {
        return last_refresh == 0 || now - last_refresh >= static_cast<unsigned long long>(interval_ms);
    }

    void adjust_interval_deadline(const unsigned long long now,
                                  const int previous_interval_ms,
                                  const int next_interval_ms,
                                  unsigned long long& last_refresh) noexcept
    {
        if (previous_interval_ms == next_interval_ms)
            return;

        // A shorter period should be observable immediately. For a longer period,
        // restart the deadline from the settings change instead of retaining the
        // previous shorter cadence.
        last_refresh = next_interval_ms < previous_interval_ms ? 0 : now;
    }

    bool player_scan_inputs_changed(const ta::features::esp::EspSettingsSnapshot& previous,
                                    const ta::features::esp::EspSettingsSnapshot& next) noexcept
    {
        return previous.pmc_enabled != next.pmc_enabled ||
               previous.scav_enabled != next.scav_enabled;
    }

    bool dynamic_loot_inputs_changed(const ta::features::esp::EspSettingsSnapshot& previous,
                                     const ta::features::esp::EspSettingsSnapshot& next) noexcept
    {
        return previous.normal_items_enabled != next.normal_items_enabled ||
               previous.normal_item_distance != next.normal_item_distance ||
               previous.loot_min_distance != next.loot_min_distance ||
               previous.wishlist_items_enabled != next.wishlist_items_enabled ||
               previous.wishlist_item_distance != next.wishlist_item_distance ||
               previous.quest_items_enabled != next.quest_items_enabled ||
               previous.quest_item_distance != next.quest_item_distance ||
               previous.corpses_enabled != next.corpses_enabled ||
               previous.corpse_distance != next.corpse_distance;
    }

    bool static_scan_inputs_changed(const ta::features::esp::EspSettingsSnapshot& previous,
                                    const ta::features::esp::EspSettingsSnapshot& next) noexcept
    {
        return previous.extracts_enabled != next.extracts_enabled ||
               previous.transits_enabled != next.transits_enabled ||
               previous.containers_enabled != next.containers_enabled ||
               previous.container_distance != next.container_distance;
    }
}

EftEspGateway::EftEspGateway(ta::adapters::eft::runtime::EftGameContext& context,
                             ta::adapters::eft::items::WishlistService& wishlist_service)
    : context_(context), wishlist_service_(wishlist_service)
{
}


void EftEspGateway::application_tick()
{
    if (!ta::application::raid_context::stable_in_raid())
    {
        if (++raid_absent_ticks_ >= 30)
        {
            clear_raid_data();
            raid_absent_ticks_ = 30;
        }
    }
    else
    {
        raid_absent_ticks_ = 0;
    }
}

void EftEspGateway::gameworld_update(
    const ta::adapters::eft::runtime::EftGameContextSnapshot& context,
    const ta::features::esp::EspSettingsSnapshot& incoming_settings,
    const std::uint64_t settings_revision)
{
    if (ta::application::raid_context::teardown_in_progress() ||
        !ta::application::raid_context::stable_in_raid())
        return;

    raid_absent_ticks_ = 0;

    ta::foundation::math::CameraFrame camera_frame{};
    if (ta::adapters::unity::graphics::UnityCameraFrameSource::capture(camera_frame, ++camera_revision_))
        camera_snapshot_.publish(std::move(camera_frame));
    else
        camera_snapshot_.clear();

    ta::features::esp::EspSettingsSnapshot settings = incoming_settings;
    ta::features::esp::sanitize_esp_settings(settings.advanced);
    wishlist_service_.set_periodic_refresh_interval(
        std::chrono::milliseconds(settings.advanced.wishlist_fallback_interval_ms));

    const unsigned long long now = GetTickCount64();
    const bool settings_changed = !has_applied_settings_ || applied_settings_revision_ != settings_revision;

    bool force_player_scan = false;
    bool force_dynamic_scan = false;
    bool force_exfil_refresh = false;
    bool force_static_scan = false;

    if (!has_applied_settings_)
    {
        force_player_scan = true;
        force_dynamic_scan = true;
        force_exfil_refresh = true;
        force_static_scan = true;
    }
    else if (settings_changed)
    {
        adjust_interval_deadline(
            now,
            applied_settings_.advanced.local_position_interval_ms,
            settings.advanced.local_position_interval_ms,
            last_local_position_refresh_ms_);
        adjust_interval_deadline(
            now,
            applied_settings_.advanced.player_scan_interval_ms,
            settings.advanced.player_scan_interval_ms,
            last_player_refresh_ms_);
        adjust_interval_deadline(
            now,
            applied_settings_.advanced.dynamic_loot_interval_ms,
            settings.advanced.dynamic_loot_interval_ms,
            last_dynamic_refresh_ms_);
        adjust_interval_deadline(
            now,
            applied_settings_.advanced.exfil_status_interval_ms,
            settings.advanced.exfil_status_interval_ms,
            last_exfil_status_refresh_ms_);
        adjust_interval_deadline(
            now,
            applied_settings_.advanced.static_discovery_interval_ms,
            settings.advanced.static_discovery_interval_ms,
            last_static_refresh_ms_);

        force_player_scan = player_scan_inputs_changed(applied_settings_, settings);
        force_dynamic_scan = dynamic_loot_inputs_changed(applied_settings_, settings);
        force_exfil_refresh = applied_settings_.extracts_enabled != settings.extracts_enabled;
        force_static_scan = static_scan_inputs_changed(applied_settings_, settings);
    }

    const std::uint64_t wishlist_revision = wishlist_service_.revision();
    const bool wishlist_changed = wishlist_revision != applied_wishlist_revision_;
    if (wishlist_changed && settings.wishlist_items_enabled)
        force_dynamic_scan = true;

    bool refresh_local_position = refresh_due(
        now, last_local_position_refresh_ms_, settings.advanced.local_position_interval_ms);
    const bool refresh_players = force_player_scan || refresh_due(
        now, last_player_refresh_ms_, settings.advanced.player_scan_interval_ms);
    const bool refresh_dynamic = force_dynamic_scan || refresh_due(
        now, last_dynamic_refresh_ms_, settings.advanced.dynamic_loot_interval_ms);
    const bool refresh_exfil_status = force_exfil_refresh || refresh_due(
        now, last_exfil_status_refresh_ms_, settings.advanced.exfil_status_interval_ms);
    const bool refresh_static = force_static_scan || refresh_due(
        now, last_static_refresh_ms_, settings.advanced.static_discovery_interval_ms);

    // Scanners that use the local origin should never run against a stale position.
    if (refresh_players || refresh_dynamic || refresh_exfil_status || refresh_static)
        refresh_local_position = true;

    applied_settings_ = settings;
    has_applied_settings_ = true;
    applied_settings_revision_ = settings_revision;
    applied_wishlist_revision_ = wishlist_revision;

    if (!refresh_local_position && !refresh_players && !refresh_dynamic &&
        !refresh_exfil_status && !refresh_static)
        return;

    if (refresh_local_position) last_local_position_refresh_ms_ = now;
    if (refresh_players) last_player_refresh_ms_ = now;
    if (refresh_dynamic) last_dynamic_refresh_ms_ = now;
    if (refresh_exfil_status) last_exfil_status_refresh_ms_ = now;
    if (refresh_static) last_static_refresh_ms_ = now;

    world_scanner_.update_world(
        context.game_world_class,
        context.game_world_instance,
        context.main_player,
        settings,
        wishlist_service_.current(),
        working_snapshot_,
        refresh_local_position,
        refresh_players,
        refresh_dynamic,
        refresh_exfil_status,
        refresh_static);

    const auto published = snapshot_.latest();
    if (!published || working_snapshot_ != *published)
    {
        snapshot_.publish(working_snapshot_);
        has_published_world_ = true;
    }
}

std::shared_ptr<const ta::features::esp::EspWorldSnapshot> EftEspGateway::world() const noexcept
{
    return snapshot_.latest();
}

std::optional<ta::foundation::math::CameraFrame> EftEspGateway::camera() const
{
    return camera_snapshot_.latest();
}

void EftEspGateway::clear_raid_data()
{
    if (has_published_world_)
    {
        working_snapshot_ = ta::features::esp::EspWorldSnapshot{};
        snapshot_.publish(working_snapshot_);
        has_published_world_ = false;
    }

    camera_snapshot_.clear();
    raid_absent_ticks_ = 30;
    applied_settings_revision_ = 0;
    applied_wishlist_revision_ = 0;
    has_applied_settings_ = false;
    applied_settings_ = {};
    last_local_position_refresh_ms_ = 0;
    last_player_refresh_ms_ = 0;
    last_dynamic_refresh_ms_ = 0;
    last_exfil_status_refresh_ms_ = 0;
    last_static_refresh_ms_ = 0;
    world_scanner_.clear_caches();
}

void EftEspGateway::game_tick(const ta::features::esp::EspSettingsSnapshot& settings,
                              const std::uint64_t settings_revision)
{
    const auto context = context_.snapshot();
    gameworld_update(context, settings, settings_revision);
}

void EftEspGateway::reset_session() noexcept
{
    clear_raid_data();
}
}
