#include "Nanos.hpp"

#include "servers/CNShardServer.hpp"

#include "PlayerManager.hpp"
#include "Missions.hpp"
#include "Abilities.hpp"
#include "NPCManager.hpp"

#include <cmath>

using namespace Nanos;

std::map<int32_t, NanoData> Nanos::NanoTable;
std::map<int32_t, NanoTuning> Nanos::NanoTunings;

#pragma region Helper methods
void Nanos::addNano(CNSocket* sock, int16_t nanoID, int16_t slot, bool spendfm) {
    if (nanoID <= 0 || nanoID >= NANO_COUNT)
        return;

    Player *plr = PlayerManager::getPlayer(sock);

    int level = plr->level;

#ifndef ACADEMY
    level = nanoID < plr->level ? plr->level : nanoID;

    /*
     * Spend the necessary Fusion Matter.
     * Note the use of the not-yet-incremented plr->level as opposed to level.
     * Doing it the other way always leaves the FM at 0. Jade totally called it.
     */
    plr->level = level;

    if (spendfm) {
        plr->subtractCapped(CappedValueType::FUSIONMATTER, (int)Missions::AvatarGrowth[plr->level-1]["m_iReqBlob_NanoCreate"]);
        Missions::updateFusionMatter(sock);
    }
#endif

    // Send to client
    INITSTRUCT(sP_FE2CL_REP_PC_NANO_CREATE_SUCC, resp);
    resp.Nano.iID = nanoID;
    resp.Nano.iStamina = 150;
    resp.iQuestItemSlotNum = slot;
    resp.iPC_Level = level;
    resp.iPC_FusionMatter = plr->fusionmatter;

    if (plr->activeNano > 0 && plr->activeNano == nanoID)
        summonNano(sock, -1); // just unsummon the nano to prevent infinite buffs

    // Update player
    plr->Nanos[nanoID] = resp.Nano;

    sock->sendPacket(resp, P_FE2CL_REP_PC_NANO_CREATE_SUCC);

    /*
     * iPC_Level in NANO_CREATE_SUCC sets the player's level.
     * Other players must be notified of the change as well. Both P_FE2CL_REP_PC_NANO_CREATE and
     * P_FE2CL_REP_PC_CHANGE_LEVEL appear to play the same animation, but only the latter affects
     * the other player's displayed level.
     */

    INITSTRUCT(sP_FE2CL_REP_PC_CHANGE_LEVEL, resp2);

    resp2.iPC_ID = plr->iID;
    resp2.iPC_Level = level;

    // Update other players' perception of the player's level
    PlayerManager::sendToViewable(sock, resp2, P_FE2CL_REP_PC_CHANGE_LEVEL);
}

void Nanos::summonNano(CNSocket *sock, int slot, bool silent) {
    INITSTRUCT(sP_FE2CL_REP_NANO_ACTIVE_SUCC, resp);
    resp.iActiveNanoSlotNum = slot;
    Player *plr = PlayerManager::getPlayer(sock);

    if (slot > 2 || slot < -1)
        return; // sanity check

    int16_t nanoID = slot == -1 ? 0 : plr->equippedNanos[slot];

    if (slot != -1 && plr->Nanos[nanoID].iSkillID == 0)
        return; // prevent powerless nanos from summoning

    if (nanoID >= NANO_COUNT || nanoID < 0)
        return; // sanity check

    plr->activeNano = nanoID;
    sNano& nano = plr->Nanos[nanoID];

    SkillData* skill = Abilities::SkillTable.count(nano.iSkillID) > 0
                        ? &Abilities::SkillTable[nano.iSkillID] : nullptr;
    if (slot != -1 && skill != nullptr && skill->drainType == SkillDrainType::PASSIVE) {
        // passive buff effect
        resp.eCSTB___Add = 1;
        ICombatant* src = dynamic_cast<ICombatant*>(plr);
        int32_t targets[] = { plr->iID };
        std::vector<ICombatant*> affectedCombatants = Abilities::matchTargets(src, skill, 1, targets);
        Abilities::useNanoSkill(sock, skill, nano, affectedCombatants);
    }

    if (!silent) // silent nano death but only for the summoning player
        sock->sendPacket(resp, P_FE2CL_REP_NANO_ACTIVE_SUCC);

    // Send to other players, these players can't handle silent nano deaths so this packet needs to be sent.
    INITSTRUCT(sP_FE2CL_NANO_ACTIVE, pkt1);
    pkt1.iPC_ID = plr->iID;
    pkt1.Nano = nano;
    PlayerManager::sendToViewable(sock, pkt1, P_FE2CL_NANO_ACTIVE);
}

static void setNanoSkill(CNSocket* sock, sP_CL2FE_REQ_NANO_TUNE* skill) {
    if (skill == nullptr || skill->iNanoID >= NANO_COUNT || skill->iNanoID < 0)
        return;

    Player *plr = PlayerManager::getPlayer(sock);

    if (plr->activeNano > 0 && plr->activeNano == skill->iNanoID)
        summonNano(sock, -1); // just unsummon the nano to prevent infinite buffs

    sNano nano = plr->Nanos[skill->iNanoID];
    nano.iSkillID = skill->iTuneID;
    plr->Nanos[skill->iNanoID] = nano;

    // Send to client
    INITSTRUCT(sP_FE2CL_REP_NANO_TUNE_SUCC, resp);
    resp.iNanoID = skill->iNanoID;
    resp.iSkillID = skill->iTuneID;
    resp.iPC_FusionMatter = plr->fusionmatter;
    resp.aItem[9] = plr->Inven[0]; // quick fix to make sure item in slot 0 doesn't get yeeted by default


    // check if there's any garbage in the item slot array (this'll happen when a nano station isn't used)
    for (int i = 0; i < 10; i++) {
        if (skill->aiNeedItemSlotNum[i] < 0 || skill->aiNeedItemSlotNum[i] >= AINVEN_COUNT) {
            sock->sendPacket(resp, P_FE2CL_REP_NANO_TUNE_SUCC);
            return; // stop execution, don't run consumption logic
        }
    }

#ifndef ACADEMY
    if (plr->fusionmatter < (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"]) // sanity check
        return;
#endif

    plr->subtractCapped(CappedValueType::FUSIONMATTER, (int)Missions::AvatarGrowth[plr->level]["m_iReqBlob_NanoTune"]);

    int reqItemCount = NanoTunings[skill->iTuneID].reqItemCount;
    int reqItemID = NanoTunings[skill->iTuneID].reqItems;
    int i = 0;
    while (reqItemCount > 0 && i < 10) {

        sItemBase& item = plr->Inven[skill->aiNeedItemSlotNum[i]];
        if (item.iType == 7 && item.iID == reqItemID) {
            if (item.iOpt > reqItemCount) {
                item.iOpt -= reqItemCount;
                reqItemCount = 0;
            }
            else {
                reqItemCount -= item.iOpt;
                item.iID = 0;
                item.iType = 0;
                item.iOpt = 0;
            }
        }
        i++; // next slot
    }

    resp.iPC_FusionMatter = plr->fusionmatter; // update fusion matter in packet
    // update items clientside
    for (int i = 0; i < 10; i++) {
        if (skill->aiNeedItemSlotNum[i]) { // non-zero check
            resp.aItem[i] = plr->Inven[skill->aiNeedItemSlotNum[i]];
            resp.aiItemSlotNum[i] = skill->aiNeedItemSlotNum[i];
        }
    }

    sock->sendPacket(resp, P_FE2CL_REP_NANO_TUNE_SUCC);

    DEBUGLOG(
        std::cout << PlayerManager::getPlayerName(plr) << " set skill id " << skill->iTuneID << " for nano: " << skill->iNanoID << std::endl;
    )
}

// 0=A 1=B 2=C -1=Not found
int Nanos::nanoStyle(int nanoID) {
    if (nanoID < 1 || nanoID >= (int)NanoTable.size())
        return -1;
    return NanoTable[nanoID].style;
}

bool Nanos::getNanoBoost(Player* plr) {
    for (int i = 0; i < 3; i++)
        if (plr->equippedNanos[i] == plr->activeNano)
            if (plr->hasBuff(ECSB_STIMPAKSLOT1 + i))
                return true;
    return false;
}
#pragma endregion

static void nanoEquipHandler(CNSocket* sock, CNPacketData* data) {
    auto nano = (sP_CL2FE_REQ_NANO_EQUIP*)data->buf;
    INITSTRUCT(sP_FE2CL_REP_NANO_EQUIP_SUCC, resp);
    Player *plr = PlayerManager::getPlayer(sock);

    // sanity checks
    if (nano->iNanoSlotNum > 2 || nano->iNanoSlotNum < 0)
        return;

    if (nano->iNanoID < 0 || nano->iNanoID >= NANO_COUNT)
        return;

    resp.iNanoID = nano->iNanoID;
    resp.iNanoSlotNum = nano->iNanoSlotNum;

    // Update player
    plr->equippedNanos[nano->iNanoSlotNum] = nano->iNanoID;

    // unsummon nano if replaced
    if (plr->activeNano == plr->equippedNanos[nano->iNanoSlotNum])
        summonNano(sock, -1);

    sock->sendPacket(resp, P_FE2CL_REP_NANO_EQUIP_SUCC);
}

static void nanoUnEquipHandler(CNSocket* sock, CNPacketData* data) {
    auto nano = (sP_CL2FE_REQ_NANO_UNEQUIP*)data->buf;
    INITSTRUCT(sP_FE2CL_REP_NANO_UNEQUIP_SUCC, resp);
    Player *plr = PlayerManager::getPlayer(sock);

    // sanity check
    if (nano->iNanoSlotNum > 2 || nano->iNanoSlotNum < 0)
        return;

    resp.iNanoSlotNum = nano->iNanoSlotNum;

    // unsummon nano if removed
    if (plr->equippedNanos[nano->iNanoSlotNum] == plr->activeNano)
        summonNano(sock, -1);

    // update player
    plr->equippedNanos[nano->iNanoSlotNum] = 0;

    sock->sendPacket(resp, P_FE2CL_REP_NANO_UNEQUIP_SUCC);
}

static void nanoSummonHandler(CNSocket* sock, CNPacketData* data) {
    auto pkt = (sP_CL2FE_REQ_NANO_ACTIVE*)data->buf;
    Player *plr = PlayerManager::getPlayer(sock);

    summonNano(sock, pkt->iNanoSlotNum);

    DEBUGLOG(
        std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano slot: " << pkt->iNanoSlotNum << std::endl;
    )
}

static void nanoSkillUseHandler(CNSocket* sock, CNPacketData* data) {
    Player *plr = PlayerManager::getPlayer(sock);

    // validate request check
    sP_CL2FE_REQ_NANO_SKILL_USE* pkt = (sP_CL2FE_REQ_NANO_SKILL_USE*)data->buf;
    if (!validInVarPacket(sizeof(sP_CL2FE_REQ_NANO_SKILL_USE), pkt->iTargetCnt, sizeof(int32_t), data->size)) {
        std::cout << "[WARN] bad sP_CL2FE_REQ_NANO_SKILL_USE packet size" << std::endl;
        return;
    }

    sNano& nano = plr->Nanos[plr->activeNano];
    int16_t skillID = nano.iSkillID;
    SkillData* skillData = &Abilities::SkillTable[skillID];

    DEBUGLOG(
        std::cout << PlayerManager::getPlayerName(plr) << " requested to summon nano skill " << std::endl;
    )

    ICombatant* plrCombatant = dynamic_cast<ICombatant*>(plr);
    std::vector<ICombatant*> targetData = Abilities::matchTargets(plrCombatant, skillData, pkt->iTargetCnt, (int32_t*)(pkt + 1));
    Abilities::useNanoSkill(sock, skillData, nano, targetData);

    if (plr->Nanos[plr->activeNano].iStamina <= 0)
        summonNano(sock, -1);
}

static void nanoSkillSetHandler(CNSocket* sock, CNPacketData* data) {
    auto skill = (sP_CL2FE_REQ_NANO_TUNE*)data->buf;
    setNanoSkill(sock, skill);
}

static void nanoSkillSetGMHandler(CNSocket* sock, CNPacketData* data) {
    auto skillGM = (sP_CL2FE_REQ_NANO_TUNE*)data->buf;
    setNanoSkill(sock, skillGM);
}

static void nanoRecallRegisterHandler(CNSocket* sock, CNPacketData* data) {
    auto recallData = (sP_CL2FE_REQ_REGIST_RXCOM*)data->buf;

    if (NPCManager::NPCs.find(recallData->iNPCID) == NPCManager::NPCs.end())
        return;

    Player* plr = PlayerManager::getPlayer(sock);

    BaseNPC *npc = NPCManager::NPCs[recallData->iNPCID];

    INITSTRUCT(sP_FE2CL_REP_REGIST_RXCOM, response);
    response.iMapNum = plr->recallInstance = (int32_t)npc->instanceID; // Never going to recall into a Fusion Lair
    response.iX = plr->recallX = npc->x;
    response.iY = plr->recallY = npc->y;
    response.iZ = plr->recallZ = npc->z;
    sock->sendPacket(response, P_FE2CL_REP_REGIST_RXCOM);
}

static void nanoRecallHandler(CNSocket* sock, CNPacketData* data) {
    auto recallData = (sP_CL2FE_REQ_WARP_USE_RECALL*)data->buf;

    Player* plr = PlayerManager::getPlayer(sock);
    Player* otherPlr = PlayerManager::getPlayerFromID(recallData->iGroupMemberID);
    if (otherPlr == nullptr)
        return;

    // ensure the group member is still in the same IZ
    if (otherPlr->instanceID != plr->instanceID)
        return;

    // do not allow hypothetical recall points in lairs to mess with the respawn logic
    if (PLAYERID(plr->instanceID) != 0)
        return;

    if ((int32_t)plr->instanceID == otherPlr->recallInstance)
        PlayerManager::sendPlayerTo(sock, otherPlr->recallX, otherPlr->recallY, otherPlr->recallZ, otherPlr->recallInstance);
    else {
        INITSTRUCT(sP_FE2CL_REP_WARP_USE_RECALL_FAIL, response)
        sock->sendPacket(response, P_FE2CL_REP_WARP_USE_RECALL_FAIL);
    }
}

static void nanoPotionHandler(CNSocket* sock, CNPacketData* data) {
    Player* player = PlayerManager::getPlayer(sock);

    // sanity checks
    if (player->activeNano == -1 || player->batteryN == 0)
        return;

    sNano nano = player->Nanos[player->activeNano];
    int difference = 150 - nano.iStamina;
    if (player->batteryN < difference)
        difference = player->batteryN;

    if (difference == 0)
        return;

    INITSTRUCT(sP_FE2CL_REP_CHARGE_NANO_STAMINA, response);
    response.iNanoID = nano.iID;
    response.iNanoStamina = nano.iStamina + difference;
    response.iBatteryN = player->batteryN - difference;

    sock->sendPacket(response, P_FE2CL_REP_CHARGE_NANO_STAMINA);
    // now update serverside
    player->subtractCapped(CappedValueType::BATTERY_N, difference);
    player->Nanos[nano.iID].iStamina += difference;

}

void Nanos::init() {
    REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_ACTIVE, nanoSummonHandler);
    REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_EQUIP, nanoEquipHandler);
    REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_UNEQUIP, nanoUnEquipHandler);
    REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_TUNE, nanoSkillSetHandler);
    REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_GIVE_NANO_SKILL, nanoSkillSetGMHandler);
    REGISTER_SHARD_PACKET(P_CL2FE_REQ_NANO_SKILL_USE, nanoSkillUseHandler);
    REGISTER_SHARD_PACKET(P_CL2FE_REQ_REGIST_RXCOM, nanoRecallRegisterHandler);
    REGISTER_SHARD_PACKET(P_CL2FE_REQ_WARP_USE_RECALL, nanoRecallHandler);
    REGISTER_SHARD_PACKET(P_CL2FE_REQ_CHARGE_NANO_STAMINA, nanoPotionHandler);
}
