#include "redridge_weapon_common"
#include "weapons/weapon_rr_axe"
#include "weapons/weapon_rr_crossbow"
#include "weapons/weapon_rr_9mmhandgun"
#include "weapons/weapon_rr_9mmar"
#include "weapons/weapon_rr_shotgun"
#include "weapons/weapon_rr_357"
#include "weapons/weapon_rr_handgrenade"
#include "redridge_weapon_registry"
#include "redridge_monster_registry"
#include "entities/trigger_hurt_custom"
#include "hr_redridge2_coop"
#include "hr_redridge6_coop"

namespace RedridgePort
{

    const string LETTERBOX_SPRITE = "redridge/cinematic/black.spr";

    const int BAR_SEGMENTS = 5;
    const int BAR_WIDTH = 512;
    const int BAR_HEIGHT = 135;

    const int TOP_CHANNEL_BASE = 0;
    const int BOTTOM_CHANNEL_BASE = BAR_SEGMENTS;
    const int SEAM_CHANNEL_BASE = BAR_SEGMENTS * 2;
    const int LETTERBOX_CHANNEL_COUNT = BAR_SEGMENTS * 3;

    const float BOTTOM_Y = -0.00001f;

    const float BOTTOM_SEAM_Y = 0.9995f;
    const int BOTTOM_SEAM_HEIGHT = 2;

    const float BAR_TICK = 0.05f;
    const int BAR_STEP = 5;
    const float BAR_HOLD_TIME = 360.0f;

    const bool HIDE_BASE_HUD = true;
    const bool BLOCK_WEAPONS = true;

    const string REDRIDGE_BEER_PICKUP_SOUND = "redridge/items/whiskey.wav";

    const int CINEMATIC_HIDE_MASK =
        HIDEHUD_WEAPONS |
        HIDEHUD_HEALTH |
        HIDEHUD_SUITPOWER |
        HIDEHUD_CROSSHAIR |
        HIDEHUD_AMMO;

    array<int> g_BarVisibleHeight;
    array<bool> g_BarWanted;

    CScheduledFunction@ g_LetterboxTimer = null;

    bool g_CinematicActive = false;
    array<EHandle> g_CinematicOwners;
    bool g_HooksRegistered = false;

    void EnsureRuntimeState()
    {
        const uint needed = uint(g_Engine.maxClients + 1);

        if (g_BarVisibleHeight.length() < needed)
            g_BarVisibleHeight.resize(needed);

        if (g_BarWanted.length() < needed)
            g_BarWanted.resize(needed);

    }

    void ResetPlayerRuntimeState(CBasePlayer@ player)
    {
        EnsureRuntimeState();

        if (player is null)
            return;

        const int index = player.entindex();
        if (index <= 0 || uint(index) >= g_BarVisibleHeight.length())
            return;

        g_BarVisibleHeight[index] = 0;
        g_BarWanted[index] = false;

        player.GetCustomKeyvalues().SetKeyvalue("$i_rr_hud_saved", 0);
    }

    void ResetRuntimeState()
    {
        EnsureRuntimeState();

        if (g_LetterboxTimer !is null)
        {
            g_Scheduler.RemoveTimer(g_LetterboxTimer);
            @g_LetterboxTimer = null;
        }

        for (uint i = 0; i < g_BarVisibleHeight.length(); ++i)
        {
            g_BarVisibleHeight[i] = 0;
            g_BarWanted[i] = false;
        }

        g_CinematicActive = false;
        g_CinematicOwners.resize(0);
    }

    void MapInit()
    {
        RedridgeMonsters::Register();
        RedridgeTriggerHurtCustom::Register();
        RedridgeWeapons::Register();
        RedridgeWeapons::ApplyMappings();
        RedridgeWeaponCommon::RegisterNoGibHook();

        g_Game.PrecacheGeneric("sprites/" + LETTERBOX_SPRITE);
        g_SoundSystem.PrecacheSound(REDRIDGE_BEER_PICKUP_SOUND);
        ResetRuntimeState();
        Redridge2Coop::MapInit();
        Redridge6Coop::MapInit();

        g_SurvivalMode.EnableMapSupport();
        g_SurvivalMode.SetStartOn(false);
        g_Scheduler.SetTimeout("RedridgeActivateSurvival", 30.0f);

        g_Scheduler.SetTimeout(
            "RedridgeApplyStockMonsterFactionSweep",
            0.20f);

        if (!g_HooksRegistered)
        {
            g_Hooks.RegisterHook(
                Hooks::Player::ClientPutInServer,
                @ClientPutInServer);

            g_Hooks.RegisterHook(
                Hooks::Player::PlayerSpawn,
                @PlayerSpawn);

            g_Hooks.RegisterHook(
                Hooks::Player::PlayerPreThink,
                @RedridgeTeddy::PlayerPreThink);

            g_Hooks.RegisterHook(
                Hooks::Player::PlayerPreThink,
                @CinematicPlayerPreThink);

            g_Hooks.RegisterHook(
                Hooks::Player::ClientDisconnect,
                @ClientDisconnect);

            g_Hooks.RegisterHook(
                Hooks::Game::EntityCreated,
                @EntityCreated);

            g_Hooks.RegisterHook(
                Hooks::PickupObject::Collected,
                @PickupCollected);

            g_HooksRegistered = true;
        }
    }

    HookReturnCode ClientPutInServer(CBasePlayer@ player)
    {
        if (player is null)
            return HOOK_CONTINUE;

        ResetPlayerRuntimeState(player);
        RedridgeTeddy::ResetPlayerFreeze(player);
        ReconcilePlayerControl(player);

        if (g_CinematicActive)
            ApplyActiveCinematicToPlayer(player);

        return HOOK_CONTINUE;
    }

    HookReturnCode PlayerSpawn(CBasePlayer@ player)
    {
        if (player !is null)
        {

            RedridgeTeddy::ResetPlayerFreeze(player);
            ReconcilePlayerControl(player);
        }

        if (player !is null && g_CinematicActive)
            ApplyActiveCinematicToPlayer(player);

        return HOOK_CONTINUE;
    }

    void StopPlayerCinematicMotion(CBasePlayer@ player)
    {
        if (player is null)
            return;

        const Vector zero(0.0f, 0.0f, 0.0f);

        player.pev.velocity = zero;
        player.pev.basevelocity = zero;
        player.pev.punchangle = zero;
        player.m_flPlayerFallVelocity = 0.0f;

        player.pev.flags &= ~FL_BASEVELOCITY;
    }

    HookReturnCode CinematicPlayerPreThink(
        CBasePlayer@ player,
        uint& out flags)
    {
        if (player is null)
            return HOOK_CONTINUE;

        RedridgeWeapons::HandleImpulse101(player);

        const bool activeChanged = PruneInvalidCinematicOwners();

        if (!g_CinematicActive)
        {
            if (activeChanged)
                ReconcileAllCinematicPlayers();

            return HOOK_CONTINUE;
        }

        StopPlayerCinematicMotion(player);
        return HOOK_CONTINUE;
    }

    HookReturnCode ClientDisconnect(CBasePlayer@ player)
    {
        if (player is null)
            return HOOK_CONTINUE;

        RedridgeTeddy::ResetPlayerFreeze(player);

        const int index = player.entindex();

        if (index > 0 && uint(index) < g_BarVisibleHeight.length())
        {
            g_BarVisibleHeight[index] = 0;
            g_BarWanted[index] = false;
        }

        return HOOK_CONTINUE;
    }

    bool IsSharedRedridgeStockMonster(const string& in classname)
    {

        return
            classname == "monster_zombie" ||
            classname == "monster_zombie_barney" ||
            classname == "monster_zombie_soldier" ||
            classname == "monster_headcrab";
    }

    bool IsSharedRedridgeZombie(const string& in classname)
    {
        return
            classname == "monster_zombie" ||
            classname == "monster_zombie_barney" ||
            classname == "monster_zombie_soldier";
    }

    void ApplySharedRedridgeMonsterFaction(CBaseEntity@ entity)
    {
        if (entity is null)
            return;

        const string classname = entity.GetClassname();

        if (!IsSharedRedridgeStockMonster(classname))
            return;

        entity.SetClassification(CLASS_ALIEN_PREDATOR);

        if (IsSharedRedridgeZombie(classname))
        {
            CBaseMonster@ monster = entity.MyMonsterPointer();

            if (monster !is null)
                monster.m_bloodColor = BLOOD_COLOR_RED;
        }
    }

    void ApplySharedRedridgeMonsterFactionDelayed(EHandle hEntity)
    {
        if (!hEntity.IsValid())
            return;

        ApplySharedRedridgeMonsterFaction(hEntity.GetEntity());
    }

    void ApplySharedRedridgeMonsterFactionSweep()
    {
        const array<string> classnames =
        {
            "monster_zombie",
            "monster_zombie_barney",
            "monster_zombie_soldier",
            "monster_headcrab"
        };

        for (uint i = 0; i < classnames.length(); ++i)
        {
            CBaseEntity@ entity = null;

            while ((@entity = g_EntityFuncs.FindEntityByClassname(
                entity,
                classnames[i])) !is null)
            {
                ApplySharedRedridgeMonsterFaction(entity);
            }
        }
    }

    HookReturnCode EntityCreated(CBaseEntity@ entity)
    {
        if (entity is null)
            return HOOK_CONTINUE;

        const string classname = entity.GetClassname();

        if (IsSharedRedridgeStockMonster(classname))
        {

            g_Scheduler.SetTimeout(
                "RedridgeApplyStockMonsterFactionDelayed",
                0.05f,
                EHandle(entity));
        }

        return HOOK_CONTINUE;
    }

    HookReturnCode PickupCollected(CBaseEntity@ pickup, CBaseEntity@ other)
    {
        if (pickup is null || other is null)
            return HOOK_CONTINUE;

        if (pickup.GetClassname() != "item_battery" || !other.IsPlayer())
            return HOOK_CONTINUE;

        g_SoundSystem.StopSound(
            other.edict(),
            CHAN_ITEM,
            "items/gunpickup2.wav",
            true);

        g_SoundSystem.PlaySound(
            other.edict(),
            CHAN_ITEM,
            REDRIDGE_BEER_PICKUP_SOUND,
            1.0f,
            ATTN_NORM);

        return HOOK_CONTINUE;
    }

    bool PruneInvalidCinematicOwners()
    {
        const bool wasActive = g_CinematicActive;

        for (int i = int(g_CinematicOwners.length()) - 1; i >= 0; --i)
        {
            if (!g_CinematicOwners[uint(i)].IsValid())
                g_CinematicOwners.removeAt(uint(i));
        }

        g_CinematicActive = g_CinematicOwners.length() > 0;
        return wasActive != g_CinematicActive;
    }

    int FindCinematicOwner(CBaseEntity@ owner)
    {
        if (owner is null)
            return -1;

        for (uint i = 0; i < g_CinematicOwners.length(); ++i)
        {
            if (!g_CinematicOwners[i].IsValid())
                continue;

            CBaseEntity@ activeOwner = g_CinematicOwners[i].GetEntity();

            if (activeOwner is owner)
                return int(i);
        }

        return -1;
    }

    bool AddCinematicOwner(CBaseEntity@ owner)
    {
        PruneInvalidCinematicOwners();

        if (owner is null || FindCinematicOwner(owner) >= 0)
            return false;

        EHandle handle = EHandle(owner);
        g_CinematicOwners.insertLast(handle);
        g_CinematicActive = true;
        return true;
    }

    bool RemoveCinematicOwner(CBaseEntity@ owner)
    {
        PruneInvalidCinematicOwners();

        const int index = FindCinematicOwner(owner);

        if (index < 0)
            return false;

        g_CinematicOwners.removeAt(uint(index));
        g_CinematicActive = g_CinematicOwners.length() > 0;
        return true;
    }

    void TogglePlayerControl(
        CBaseEntity@ pActivator,
        CBaseEntity@ pCaller,
        USE_TYPE useType,
        float flValue)
    {
        if (pCaller is null)
            return;

        EnsureRuntimeState();

        CustomKeyvalues@ keys = pCaller.GetCustomKeyvalues();

        const bool frozen =
            keys.HasKeyvalue("$i_rr_frozen") &&
            keys.GetKeyvalue("$i_rr_frozen").GetInteger() != 0;

        bool newFrozen = frozen;

        if (useType == USE_ON)
            newFrozen = true;
        else if (useType == USE_OFF)
            newFrozen = false;
        else if (useType == USE_SET)
            newFrozen = flValue != 0.0f;
        else
            newFrozen = !frozen;

        bool ownerAdded = false;
        bool ownerRemoved = false;

        if (newFrozen)
            ownerAdded = AddCinematicOwner(pCaller);
        else
            ownerRemoved = RemoveCinematicOwner(pCaller);

        g_CinematicActive = g_CinematicOwners.length() > 0;

        for (int i = 1; i <= g_Engine.maxClients; ++i)
        {
            CBasePlayer@ player = g_PlayerFuncs.FindPlayerByIndex(i);

            if (player is null || !player.IsConnected())
                continue;

            if (BLOCK_WEAPONS && ownerAdded)
                player.BlockWeapons(pCaller);

            if (BLOCK_WEAPONS && (ownerRemoved || (!newFrozen && frozen)))
                player.UnblockWeapons(pCaller);

            ApplyCinematicAggregateState(player);
        }

        keys.SetKeyvalue("$i_rr_frozen", newFrozen ? 1 : 0);
    }

    void ApplyActiveCinematicToPlayer(CBasePlayer@ player)
    {
        const bool activeChanged = PruneInvalidCinematicOwners();

        if (activeChanged)
            ReconcileAllCinematicPlayers();

        if (!g_CinematicActive || player is null || !player.IsConnected())
            return;

        if (BLOCK_WEAPONS)
        {

            for (uint i = 0; i < g_CinematicOwners.length(); ++i)
            {
                if (!g_CinematicOwners[i].IsValid())
                    continue;

                CBaseEntity@ owner = g_CinematicOwners[i].GetEntity();

                if (owner !is null)
                    player.BlockWeapons(owner);
            }
        }

        ApplyCinematicAggregateState(player);
    }

    void ReconcilePlayerControl(CBasePlayer@ player)
    {
        if (player is null)
            return;

        player.EnableControl(!g_CinematicActive);
    }

    void ApplyCinematicAggregateState(CBasePlayer@ player)
    {
        if (player is null)
            return;

        ReconcilePlayerControl(player);

        if (g_CinematicActive)
        {

            StopPlayerCinematicMotion(player);

            if (HIDE_BASE_HUD)
                HidePlayerHud(player);

            SetLetterboxTarget(player, true);
            return;
        }

        SetLetterboxTarget(player, false);

        if (HIDE_BASE_HUD)
            RestorePlayerHud(player);
    }

    void ReconcileAllCinematicPlayers()
    {
        for (int i = 1; i <= g_Engine.maxClients; ++i)
        {
            CBasePlayer@ player = g_PlayerFuncs.FindPlayerByIndex(i);

            if (player !is null && player.IsConnected())
                ApplyCinematicAggregateState(player);
        }
    }

    void HidePlayerHud(CBasePlayer@ player)
    {
        CustomKeyvalues@ keys = player.GetCustomKeyvalues();

        const bool saved =
            keys.HasKeyvalue("$i_rr_hud_saved") &&
            keys.GetKeyvalue("$i_rr_hud_saved").GetInteger() != 0;

        if (!saved)
        {
            keys.SetKeyvalue("$i_rr_prev_hidehud", player.m_iHideHUD);
            keys.SetKeyvalue("$i_rr_hud_saved", 1);
        }

        player.m_iHideHUD |= CINEMATIC_HIDE_MASK;
    }

    void RestorePlayerHud(CBasePlayer@ player)
    {
        CustomKeyvalues@ keys = player.GetCustomKeyvalues();

        if (!keys.HasKeyvalue("$i_rr_hud_saved") ||
            keys.GetKeyvalue("$i_rr_hud_saved").GetInteger() == 0)
        {
            return;
        }

        if (keys.HasKeyvalue("$i_rr_prev_hidehud"))
            player.m_iHideHUD =
                keys.GetKeyvalue("$i_rr_prev_hidehud").GetInteger();
        else
            player.m_iHideHUD &= ~CINEMATIC_HIDE_MASK;

        keys.SetKeyvalue("$i_rr_hud_saved", 0);
    }

    void FillBarParams(
        HUDSpriteParams& out params,
        uint8 channel,
        float x,
        float y,
        int height)
    {
        params.channel = channel;
        params.flags =
            HUD_ELEM_ABSOLUTE_X |
            HUD_ELEM_NO_BORDER |
            HUD_SPR_OPAQUE;

        params.spritename = LETTERBOX_SPRITE;

        params.left = 0;
        params.top = 0;
        params.width = BAR_WIDTH;
        params.height = height;

        params.x = x;
        params.y = y;

        params.color1 = RGBA(255, 255, 255, 255);
        params.color2 = RGBA(255, 255, 255, 255);

        params.frame = 0;
        params.numframes = 1;
        params.framerate = 0.0f;

        params.fadeinTime = 0.0f;
        params.fadeoutTime = 0.0f;
        params.holdTime = BAR_HOLD_TIME;
        params.fxTime = 0.0f;
        params.effect = HUD_EFFECT_NONE;
    }

    void DrawSeamGuard(CBasePlayer@ player)
    {

        for (int segment = 0; segment < BAR_SEGMENTS; ++segment)
        {
            const float x = float(segment * BAR_WIDTH);

            HUDSpriteParams seam;
            FillBarParams(
                seam,
                uint8(SEAM_CHANNEL_BASE + segment),
                x,
                BOTTOM_SEAM_Y,
                BOTTOM_SEAM_HEIGHT);

            g_PlayerFuncs.HudCustomSprite(player, seam);
        }
    }

    void DrawLetterbox(CBasePlayer@ player, int visibleHeight)
    {
        if (visibleHeight <= 0)
        {
            ClearLetterbox(player);
            return;
        }

        if (visibleHeight > BAR_HEIGHT)
            visibleHeight = BAR_HEIGHT;

        for (int segment = 0; segment < BAR_SEGMENTS; ++segment)
        {
            const float x = float(segment * BAR_WIDTH);

            HUDSpriteParams top;
            FillBarParams(
                top,
                uint8(TOP_CHANNEL_BASE + segment),
                x,
                0.0f,
                visibleHeight);

            g_PlayerFuncs.HudCustomSprite(player, top);

            HUDSpriteParams bottom;
            FillBarParams(
                bottom,
                uint8(BOTTOM_CHANNEL_BASE + segment),
                x,
                BOTTOM_Y,
                visibleHeight);

            g_PlayerFuncs.HudCustomSprite(player, bottom);
        }
    }

    void ClearLetterbox(CBasePlayer@ player)
    {
        for (int channel = 0;
             channel < LETTERBOX_CHANNEL_COUNT;
             ++channel)
        {
            HUDSpriteParams clear;
            clear.channel = uint8(channel);
            g_PlayerFuncs.HudCustomSprite(player, clear);
        }
    }

    void SetLetterboxTarget(CBasePlayer@ player, bool visible)
    {
        EnsureRuntimeState();

        const int index = player.entindex();

        if (index <= 0 || uint(index) >= g_BarWanted.length())
            return;

        g_BarWanted[index] = visible;

        const int target = visible ? BAR_HEIGHT : 0;

        if (visible && g_BarVisibleHeight[index] == 0)
            DrawSeamGuard(player);

        if (g_BarVisibleHeight[index] == target)
        {
            if (visible)
                DrawSeamGuard(player);

            DrawLetterbox(player, target);
            return;
        }

        StartLetterboxTimer();
    }

    void StartLetterboxTimer()
    {
        if (g_LetterboxTimer !is null)
            return;

        @g_LetterboxTimer =
            @g_Scheduler.SetInterval(
                "RedridgeLetterboxThink",
                BAR_TICK);
    }

    void LetterboxThink()
    {
        EnsureRuntimeState();

        bool stillAnimating = false;

        for (int i = 1; i <= g_Engine.maxClients; ++i)
        {
            CBasePlayer@ player =
                g_PlayerFuncs.FindPlayerByIndex(i);

            if (player is null || !player.IsConnected())
            {
                g_BarVisibleHeight[i] = 0;
                g_BarWanted[i] = false;
                continue;
            }

            const int target =
                g_BarWanted[i] ? BAR_HEIGHT : 0;

            int current = g_BarVisibleHeight[i];

            if (current < target)
            {
                current += BAR_STEP;

                if (current > target)
                    current = target;
            }
            else if (current > target)
            {
                current -= BAR_STEP;

                if (current < target)
                    current = target;
            }

            if (current != g_BarVisibleHeight[i])
            {
                g_BarVisibleHeight[i] = current;
                DrawLetterbox(player, current);
            }

            if (current != target)
                stillAnimating = true;
        }

        if (!stillAnimating && g_LetterboxTimer !is null)
        {
            g_Scheduler.RemoveTimer(g_LetterboxTimer);
            @g_LetterboxTimer = null;
        }
    }
}

void RedridgeLetterboxThink()
{
    RedridgePort::LetterboxThink();
}

void RedridgeApplyStockMonsterFactionDelayed(EHandle hEntity)
{
    RedridgePort::ApplySharedRedridgeMonsterFactionDelayed(hEntity);
}

void RedridgeApplyStockMonsterFactionSweep()
{
    RedridgePort::ApplySharedRedridgeMonsterFactionSweep();
}

void MapInit()
{
    RedridgePort::MapInit();
}

void RedridgeActivateSurvival()
{
    if (!g_SurvivalMode.IsActive())
    {
        g_SurvivalMode.Activate(true);

        g_PlayerFuncs.ClientPrintAll(HUD_PRINTCENTER, "\n");
    }
}
