#include <trx/game/game_flow/inventory.h>

#include <trx/config.h>
#include <trx/game/gun.h>
#include <trx/game/inventory.h>
#include <trx/game/objects.h>
#include <trx/game/overlay.h>
#include <trx/game/savegame.h>
#include <trx/game/stats.h>

#include <string.h>

static int8_t m_SecretInvItems[O_NUMBER_OF] = {};
static int8_t m_Add2InvItems[O_NUMBER_OF] = {};
static bool m_RemoveWeapons = false;
static bool m_RemoveAmmo = false;
static bool m_RemoveFlares = false;
static bool m_RemoveMedipacks = false;
static bool m_RemoveScions = false;
static bool m_RemoveBinoculars = false;

// What the scion modifier takes away. The object ids are not consecutive, so
// they are named one by one.
static const OBJECT_ID m_ScionObjects[] = {
    O_SCION_ITEM_1, O_QUEST_ITEM_1, O_QUEST_ITEM_2,
    O_QUEST_ITEM_3, O_QUEST_ITEM_4, NO_OBJECT,
};

static bool M_CanHaveItem(const OBJECT_ID object_id)
{
    if (Object_IsType(object_id, g_GunObjects) && object_id != O_PISTOL_ITEM
        && g_Config.gameplay.disable_extra_guns) {
        return false;
    }
    if ((object_id == O_SMALL_MEDIPACK_ITEM
         || object_id == O_LARGE_MEDIPACK_ITEM)
        && g_Config.gameplay.disable_medpacks) {
        return false;
    }
    return true;
}

static void M_ResumeInfo_AddItem(
    RESUME_INFO *const resume, const OBJECT_ID object_id, const int32_t qty)
{
    // The binoculars are held rather than counted, so any number of them is
    // the one pair.
    if (object_id == O_BINOCULARS_ITEM) {
        if (qty > 0) {
            Inv_State_SetCount(&resume->inv, object_id, 1);
        }
        return;
    }
    Inv_State_AddCount(&resume->inv, object_id, qty);
}

static void M_ModifyResumeInfo_GunOrAmmo(
    RESUME_INFO *const resume, const LARA_GUN_TYPE gun_type)
{
    const OBJECT_ID gun_object_id = Gun_GetGunObject(gun_type);
    const OBJECT_ID ammo_object_id = Gun_GetAmmoObject(gun_type);
    const int32_t ammo_pickup_qty = Gun_GetRoundsPerBox(gun_type);
    const int32_t ammo_initial_qty = Gun_GetInitialRounds(gun_type);

    if (!M_CanHaveItem(gun_object_id) || !M_CanHaveItem(ammo_object_id)) {
        return;
    }

    Inv_State_AddAmmo(
        &resume->inv, gun_type,
        ammo_pickup_qty * m_Add2InvItems[ammo_object_id]);
    if (!Inv_State_Has(&resume->inv, gun_object_id)
        && m_Add2InvItems[gun_object_id] > 0) {
        Inv_State_SetCount(&resume->inv, gun_object_id, 1);
        Inv_State_AddAmmo(&resume->inv, gun_type, ammo_initial_qty);
    }
}

static void M_ModifyResumeInfo_Item(
    RESUME_INFO *const resume, const OBJECT_ID object_id)
{
    if (!M_CanHaveItem(object_id)) {
        return;
    }

    M_ResumeInfo_AddItem(resume, object_id, m_Add2InvItems[object_id]);
}

static void M_CollectNewPickup(const OBJECT_ID object_id)
{
    Overlay_AddDisplayPickup(object_id);
    Stats_AddPickup();
}

static void M_ModifyInventory_GunOrAmmo(
    const GF_INV_TYPE type, const LARA_GUN_TYPE gun_type)
{
    const OBJECT_ID gun_object_id = Gun_GetGunObject(gun_type);
    const OBJECT_ID ammo_object_id = Gun_GetAmmoObject(gun_type);
    const int32_t ammo_pickup_qty = Gun_GetRoundsPerBox(gun_type);
    const int32_t ammo_initial_qty = Gun_GetInitialRounds(gun_type);

    if (!M_CanHaveItem(gun_object_id) || !M_CanHaveItem(ammo_object_id)) {
        return;
    }

    if (Inv_HasItem(gun_object_id)) {
        if (type == GF_INV_SECRET) {
            // Convert already collected guns into ammo to maintain stats
            // accuracy.
            Inv_AddAmmo(
                gun_type, ammo_pickup_qty * m_SecretInvItems[ammo_object_id]);
            Inv_AddAmmo(
                gun_type, ammo_initial_qty * m_SecretInvItems[gun_object_id]);
            for (int32_t i = 0; i < m_SecretInvItems[ammo_object_id]; i++) {
                M_CollectNewPickup(ammo_object_id);
            }
            for (int32_t i = 0; i < m_SecretInvItems[gun_object_id]; i++) {
                M_CollectNewPickup(ammo_object_id);
            }
        } else if (type == GF_INV_REGULAR) {
            Inv_AddAmmo(
                gun_type, ammo_pickup_qty * m_Add2InvItems[ammo_object_id]);
        }
    } else if (
        (type == GF_INV_REGULAR && m_Add2InvItems[gun_object_id] > 0)
        || (type == GF_INV_SECRET && m_SecretInvItems[gun_object_id] > 0)) {

        Inv_AddItem(gun_object_id);

        if (type == GF_INV_SECRET) {
            Inv_AddAmmo(
                gun_type, ammo_pickup_qty * m_SecretInvItems[ammo_object_id]);
            M_CollectNewPickup(gun_object_id);
            for (int32_t i = 0; i < m_SecretInvItems[ammo_object_id]; i++) {
                M_CollectNewPickup(ammo_object_id);
            }
        } else if (type == GF_INV_REGULAR) {
            Inv_AddAmmo(
                gun_type, ammo_pickup_qty * m_Add2InvItems[ammo_object_id]);
        }
    } else if (type == GF_INV_SECRET) {
        for (int32_t i = 0; i < m_SecretInvItems[ammo_object_id]; i++) {
            Inv_AddItem(ammo_object_id);
            M_CollectNewPickup(ammo_object_id);
        }
    } else if (type == GF_INV_REGULAR) {
        for (int32_t i = 0; i < m_Add2InvItems[ammo_object_id]; i++) {
            Inv_AddItem(ammo_object_id);
        }
    }
}

static void M_ModifyInventory_Item(
    const GF_INV_TYPE type, const OBJECT_ID object_id)
{
    int32_t qty = 0;
    if (type == GF_INV_SECRET) {
        qty = m_SecretInvItems[object_id];
    } else if (type == GF_INV_REGULAR) {
        qty = m_Add2InvItems[object_id];
    }

    // Check for gameplay mods from secret rewards
    if (!M_CanHaveItem(object_id)) {
        qty = 0;
    }

    for (int32_t i = 0; i < qty; i++) {
        if (Inv_AddItem(object_id) && type == GF_INV_SECRET) {
            M_CollectNewPickup(object_id);
        }
    }
}

void GF_InventoryModifier_Scan(const GF_LEVEL *const level)
{
    for (int32_t i = O_FIRST; i < O_NUMBER_OF; i++) {
        m_SecretInvItems[i] = 0;
        m_Add2InvItems[i] = 0;
    }
    m_RemoveWeapons = false;
    m_RemoveAmmo = false;
    m_RemoveFlares = false;
    m_RemoveMedipacks = false;
    m_RemoveScions = false;
    m_RemoveBinoculars = false;

    if (level == nullptr) {
        return;
    }
    for (int32_t i = 0; i < level->sequence.length; i++) {
        const GF_SEQUENCE_EVENT *const event = &level->sequence.events[i];
        if (event->type == GFS_ADD_ITEM
            || event->type == GFS_ADD_SECRET_REWARD) {
            const GF_ADD_ITEM_DATA *const data = event->data;
            if (data->object_id < O_FIRST || data->object_id >= O_NUMBER_OF) {
                continue;
            }
            if (data->inv_type == GF_INV_SECRET) {
                m_SecretInvItems[data->object_id] += data->quantity;
            } else if (data->inv_type == GF_INV_REGULAR) {
                m_Add2InvItems[data->object_id] += data->quantity;
            }
        } else if (event->type == GFS_REMOVE_WEAPONS) {
            m_RemoveWeapons = true;
        } else if (event->type == GFS_REMOVE_AMMO) {
            m_RemoveAmmo = true;
        } else if (event->type == GFS_REMOVE_FLARES) {
            m_RemoveFlares = true;
        } else if (event->type == GFS_REMOVE_MEDIPACKS) {
            m_RemoveMedipacks = true;
        } else if (event->type == GFS_REMOVE_SCIONS) {
            m_RemoveScions = true;
        } else if (event->type == GFS_REMOVE_BINOCULARS) {
            m_RemoveBinoculars = true;
        }
    }
}

int32_t GF_GetSecretRewardCount(const GF_LEVEL *const level)
{
    int32_t sum = 0;
    if (level == nullptr) {
        return sum;
    }
    for (int32_t i = 0; i < level->sequence.length; i++) {
        const GF_SEQUENCE_EVENT *const event = &level->sequence.events[i];
        if (event->type == GFS_ADD_SECRET_REWARD) {
            const GF_ADD_ITEM_DATA *const data = event->data;
            if (data->inv_type == GF_INV_SECRET) {
                sum += data->quantity;
            }
        }
    }
    return sum;
}

void GF_InventoryModifier_ApplyToResumeInfo(const GF_LEVEL *const level)
{
    RESUME_INFO *const resume = SG_Resume_GetEntry(level);

    if (m_RemoveWeapons) {
        for (LARA_GUN_TYPE gun_type = LGT_UNARMED + 1; gun_type < NUM_WEAPONS;
             gun_type++) {
            Inv_State_SetCount(&resume->inv, Gun_GetGunObject(gun_type), 0);
        }
        resume->holsters_gun_type = LGT_UNARMED;
        resume->back_gun_type = LGT_UNARMED;
        resume->equipped_gun_type = LGT_UNARMED;
        resume->gun_status = LGS_ARMLESS;
    }

    const bool pistols_given = !Inv_State_Has(&resume->inv, O_PISTOL_ITEM)
        && m_Add2InvItems[O_PISTOL_ITEM] != 0;
    if (pistols_given) {
        Inv_State_SetCount(&resume->inv, O_PISTOL_ITEM, 1);
        if (resume->equipped_gun_type == LGT_UNARMED) {
            resume->equipped_gun_type = LGT_PISTOLS;
        }
    }

    if (m_RemoveAmmo) {
        memset(resume->inv.ammo, 0, sizeof(resume->inv.ammo));
    }

    // Pistols the game flow hands over come loaded, whatever else the level
    // took away. An endless supply needs no such rounds: the count follows the
    // gun, and Inv_State_SetCount has already written it.
    if (pistols_given && !Gun_HasInfiniteAmmo(LGT_PISTOLS)) {
        Inv_State_AddAmmo(
            &resume->inv, LGT_PISTOLS, Gun_GetInitialRounds(LGT_PISTOLS));
    }

    if (m_RemoveScions) {
        for (int32_t i = 0; m_ScionObjects[i] != NO_OBJECT; i++) {
            Inv_State_SetCount(&resume->inv, m_ScionObjects[i], 0);
        }
        m_RemoveScions = false;
    }

    if (m_RemoveFlares) {
        Inv_State_SetCount(&resume->inv, O_FLARE_ITEM, 0);
        m_RemoveFlares = false;
    }

    if (m_RemoveMedipacks) {
        Inv_State_SetCount(&resume->inv, O_LARGE_MEDIPACK_ITEM, 0);
        Inv_State_SetCount(&resume->inv, O_SMALL_MEDIPACK_ITEM, 0);
        m_RemoveMedipacks = false;
    }

    if (m_RemoveBinoculars) {
        Inv_State_SetCount(&resume->inv, O_BINOCULARS_ITEM, 0);
    } else {
        M_ModifyResumeInfo_Item(resume, O_BINOCULARS_ITEM);
    }

    for (LARA_GUN_TYPE gun_type = LGT_UNARMED + 1; gun_type < NUM_WEAPONS;
         gun_type++) {
        if (Gun_GetGunObject(gun_type) != NO_OBJECT) {
            M_ModifyResumeInfo_GunOrAmmo(resume, gun_type);
        }
    }

    M_ModifyResumeInfo_Item(resume, O_SMALL_MEDIPACK_ITEM);
    M_ModifyResumeInfo_Item(resume, O_LARGE_MEDIPACK_ITEM);
    M_ModifyResumeInfo_Item(resume, O_FLARE_ITEM);
}

void GF_InventoryModifier_Apply(
    const GF_LEVEL *const level, const GF_INV_TYPE type)
{
    RESUME_INFO *const resume = SG_Resume_GetEntry(level);

    // For GF_INV_REGULAR, we must ignore weapons, ammo, medpacks and flares,
    // as these are handled by RESUME_INFO and
    // GF_InventoryModifier_ApplyToResumeInfo and Lara_InitialiseInventory.

    if (type == GF_INV_SECRET) {
        if (m_Add2InvItems[O_PISTOL_ITEM]) {
            Inv_AddItem(O_PISTOL_ITEM);
            if (resume->equipped_gun_type == LGT_UNARMED) {
                resume->equipped_gun_type = LGT_PISTOLS;
            }
        }

        // The pistols are handled above: they arrive without ammunition of
        // their own to convert.
        for (LARA_GUN_TYPE gun_type = LGT_PISTOLS + 1; gun_type < NUM_WEAPONS;
             gun_type++) {
            if (Gun_GetGunObject(gun_type) != NO_OBJECT) {
                M_ModifyInventory_GunOrAmmo(type, gun_type);
            }
        }
    }

#define X_PICKUP_NUMBERED(item, option) M_ModifyInventory_Item(type, item);
#define X_PICKUP_MISC(item, option) M_ModifyInventory_Item(type, item);
#define X_PICKUP_VARIANT(item, option) M_ModifyInventory_Item(type, item);
#define X_PICKUP_SUPPLY_VARIANT(item, option)
#include <trx/game/objects/pickups.def>
#undef X_PICKUP_SUPPLY_VARIANT
#undef X_PICKUP_VARIANT
#undef X_PICKUP_MISC
#undef X_PICKUP_NUMBERED
    M_ModifyInventory_Item(type, O_SCION_ITEM_1);

    if (type == GF_INV_SECRET) {
        M_ModifyInventory_Item(type, O_SMALL_MEDIPACK_ITEM);
        M_ModifyInventory_Item(type, O_LARGE_MEDIPACK_ITEM);
        M_ModifyInventory_Item(type, O_FLARE_ITEM);
    }
}
