#include "Transport.hpp"

#include "servers/CNShardServer.hpp"

#include "PlayerManager.hpp"
#include "Nanos.hpp"
#include "TableData.hpp"
#include "Entities.hpp"
#include "NPCManager.hpp"
#include "MobAI.hpp"

#include <unordered_map>
#include <cmath>

using namespace Transport;

std::map<int32_t, TransportRoute> Transport::Routes;
std::map<int32_t, TransportLocation> Transport::Locations;
std::vector<NPCPath> Transport::NPCPaths;
std::map<int32_t, std::queue<Vec3>> Transport::SkywayPaths;
std::unordered_map<CNSocket*, std::queue<Vec3>> Transport::SkywayQueues;
std::unordered_map<int32_t, std::queue<Vec3>> Transport::NPCQueues;

static void transportRegisterLocationHandler(CNSocket* sock, CNPacketData* data) {
    auto transport = (sP_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION*)data->buf;
    Player* plr = PlayerManager::getPlayer(sock);

    bool newReg = false; // this is a new registration
    //std::cout << "request to register transport, eTT " << transport->eTT << ", locID " << transport->iLocationID << ", npc " << transport->iNPC_ID << std::endl;
    if (transport->eTT == 1) { // S.C.A.M.P.E.R.
        if (transport->iLocationID < 1 || transport->iLocationID > 31) { // sanity check
            std::cout << "[WARN] S.C.A.M.P.E.R. location ID " << transport->iLocationID << " is out of bounds" << std::endl;
            INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp);

            failResp.eTT = transport->eTT;
            failResp.iErrorCode = 0; // TODO: review what error code to use here
            failResp.iLocationID = transport->iLocationID;

            sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
            return;
        }

        // update registration bitfield using bitmask
        uint32_t newScamperFlag = plr->iWarpLocationFlag | (1UL << (transport->iLocationID - 1));
        if (newScamperFlag != plr->iWarpLocationFlag) {
            plr->iWarpLocationFlag = newScamperFlag;
            newReg = true;
        }
    } else if (transport->eTT == 2) { // Monkey Skyway System
        if (transport->iLocationID < 1 || transport->iLocationID > 127) { // sanity check
            std::cout << "[WARN] Skyway location ID " << transport->iLocationID << " is out of bounds" << std::endl;
            INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp);

            failResp.eTT = transport->eTT;
            failResp.iErrorCode = 0; // TODO: review what error code to use here
            failResp.iLocationID = transport->iLocationID;

            sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
            return;
        }

        /*
         * assuming the two bitfields are just stuck together to make a longer one, do a similar operation
         */
        int index = transport->iLocationID > 64 ? 1 : 0;
        uint64_t newMonkeyFlag = plr->aSkywayLocationFlag[index] | (1ULL << (index ? transport->iLocationID - 65 : transport->iLocationID - 1));
        if (newMonkeyFlag != plr->aSkywayLocationFlag[index]) {
            plr->aSkywayLocationFlag[index] = newMonkeyFlag;
            newReg = true;
        }
    } else {
        std::cout << "[WARN] Unknown mode of transport; eTT = " << transport->eTT << std::endl;
        INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL, failResp);

        failResp.eTT = transport->eTT;
        failResp.iErrorCode = 0; // TODO: review what error code to use here
        failResp.iLocationID = transport->iLocationID;

        sock->sendPacket(failResp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_FAIL);
        return;
    }

    if (!newReg)
        return; // don't send new registration message

    INITSTRUCT(sP_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC, resp);
    // response parameters
    resp.eTT = transport->eTT;
    resp.iLocationID = transport->iLocationID;
    resp.iWarpLocationFlag = plr->iWarpLocationFlag;
    resp.aWyvernLocationFlag[0] = plr->aSkywayLocationFlag[0];
    resp.aWyvernLocationFlag[1] = plr->aSkywayLocationFlag[1];

    sock->sendPacket(resp, P_FE2CL_REP_PC_REGIST_TRANSPORTATION_LOCATION_SUCC);
}

static void transportWarpHandler(CNSocket* sock, CNPacketData* data) {
    auto req = (sP_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION*)data->buf;
    Player* plr = PlayerManager::getPlayer(sock);

    /*
     * req:
     * eIL -- inventory type
     * iNPC_ID -- the ID of the NPC who is warping you
     * iTransporationID -- iVehicleID
     * iSlotNum -- inventory slot number
     */

    if (Routes.find(req->iTransporationID) == Routes.end() || Routes[req->iTransporationID].cost > plr->money) { // sanity check
        INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL, failResp);

        failResp.iErrorCode = 0; // TODO: error code
        failResp.iTransportationID = req->iTransporationID;

        sock->sendPacket(failResp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_FAIL);
        return;
    }

    TransportRoute route = Routes[req->iTransporationID];
    plr->subtractCapped(CappedValueType::TAROS, route.cost);

    TransportLocation* target = nullptr;
    switch (route.type) {
    case 1: // S.C.A.M.P.E.R.
        target = &Locations[route.end];
        break;
    case 2: // Monkey Skyway
        // set last safe coords
        plr->lastX = plr->x;
        plr->lastY = plr->y;
        plr->lastZ = plr->z;
        if (SkywayPaths.find(route.mssRouteNum) != SkywayPaths.end()) { // check if route exists
            Nanos::summonNano(sock, -1); // make sure that no nano is active during the ride
            SkywayQueues[sock] = SkywayPaths[route.mssRouteNum]; // set socket point queue to route
            plr->onMonkey = true;
            break;
        } else if (TableData::RunningSkywayRoutes.find(route.mssRouteNum) != TableData::RunningSkywayRoutes.end()) {
            std::vector<Vec3>* _route = &TableData::RunningSkywayRoutes[route.mssRouteNum];
            Nanos::summonNano(sock, -1);
            testMssRoute(sock, _route);
            plr->onMonkey = true;
            break;
        }

        // refund and send alert packet
        plr->addCapped(CappedValueType::TAROS, route.cost);
        INITSTRUCT(sP_FE2CL_ANNOUNCE_MSG, alert);
        alert.iAnnounceType = 0; // don't think this lets us make a confirm dialog
        alert.iDuringTime = 3;
        U8toU16("Skyway route " + std::to_string(route.mssRouteNum) + " isn't pathed yet. You will not be charged any taros.", (char16_t*)alert.szAnnounceMsg, sizeof(alert.szAnnounceMsg));
        sock->sendPacket(alert, P_FE2CL_ANNOUNCE_MSG);

        std::cout << "[WARN] MSS route " << route.mssRouteNum << " not pathed" << std::endl;
        break;
    default:
        std::cout << "[WARN] Unknown tranportation type " << route.type << std::endl;
        break;
    }

    INITSTRUCT(sP_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC, resp);
    // response parameters
    resp.eTT = route.type;
    resp.iCandy = plr->money;
    resp.iX = (target == nullptr) ? plr->x : target->x;
    resp.iY = (target == nullptr) ? plr->y : target->y;
    resp.iZ = (target == nullptr) ? plr->z : target->z;
    sock->sendPacket(resp, P_FE2CL_REP_PC_WARP_USE_TRANSPORTATION_SUCC);

    if (target == nullptr)
        return;

    // we warped; update position and chunks
    PlayerManager::updatePlayerPositionForWarp(sock, target->x, target->y, target->z, INSTANCE_OVERWORLD);
}

void Transport::testMssRoute(CNSocket *sock, std::vector<Vec3>* route) {
    int speed = 1500; // TODO: make this adjustable
    std::queue<Vec3> path;
    Vec3 last = route->front(); // start pos

    for (int i = 1; i < route->size(); i++) {
        Vec3 coords = route->at(i);
        Transport::lerp(&path, last, coords, speed);
        path.push(coords); // add keyframe to the queue
        last = coords; // update start pos
    }

    SkywayQueues[sock] = path;
}

/*
 * Go through every socket that has broomstick points queued up, and advance to the next point.
 * If the player has disconnected or finished the route, clean up and remove them from the queue.
 */
static void stepSkywaySystem() {

    // using an unordered map so we can remove finished players in one iteration
    std::unordered_map<CNSocket*, std::queue<Vec3>>::iterator it = SkywayQueues.begin();
    while (it != SkywayQueues.end()) {

        std::queue<Vec3>* queue = &it->second;

        if (PlayerManager::players.find(it->first) == PlayerManager::players.end()) {
            // pluck out dead socket + update iterator
            it = SkywayQueues.erase(it);
            continue;
        }

        Player* plr = PlayerManager::getPlayer(it->first);

        if (queue->empty()) {
            // send dismount packet
            INITSTRUCT(sP_FE2CL_REP_PC_RIDING_SUCC, rideSucc);
            INITSTRUCT(sP_FE2CL_PC_RIDING, rideBroadcast);
            rideSucc.iPC_ID = plr->iID;
            rideSucc.eRT = 0;
            rideBroadcast.iPC_ID = plr->iID;
            rideBroadcast.eRT = 0;
            it->first->sendPacket(rideSucc, P_FE2CL_REP_PC_RIDING_SUCC);
            // send packet to players in view
            PlayerManager::sendToViewable(it->first, rideBroadcast, P_FE2CL_PC_RIDING);
            it = SkywayQueues.erase(it); // remove player from tracking map + update iterator
            plr->onMonkey = false;
        } else {
            Vec3 point = queue->front(); // get point
            queue->pop(); // remove point from front of queue

            INITSTRUCT(sP_FE2CL_PC_BROOMSTICK_MOVE, bmstk);
            bmstk.iPC_ID = plr->iID;
            bmstk.iToX = point.x;
            bmstk.iToY = point.y;
            bmstk.iToZ = point.z;
            it->first->sendPacket(bmstk, P_FE2CL_PC_BROOMSTICK_MOVE);
            // set player location to point to update viewables
            PlayerManager::updatePlayerPosition(it->first, point.x, point.y, point.z, plr->instanceID, plr->angle);
            // send packet to players in view
            PlayerManager::sendToViewable(it->first, bmstk, P_FE2CL_PC_BROOMSTICK_MOVE);

            it++; // go to next entry in map
        }
    }
}

static void stepNPCPathing() {

    // all NPC pathing queues
    std::unordered_map<int32_t, std::queue<Vec3>>::iterator it = NPCQueues.begin();
    while (it != NPCQueues.end()) {

        std::queue<Vec3>* queue = &it->second;

        BaseNPC* npc = nullptr;
        if (NPCManager::NPCs.find(it->first) != NPCManager::NPCs.end())
            npc = NPCManager::NPCs[it->first];

        if (npc == nullptr || queue->empty()) {
            // pluck out dead path + update iterator
            it = NPCQueues.erase(it);
            continue;
        }

        // skip if not simulating mobs
        if (npc->kind == EntityKind::MOB && !MobAI::simulateMobs) {
            it++;
            continue;
        }

        // do not roam if not roaming
        if (npc->kind == EntityKind::MOB && ((Mob*)npc)->state != AIState::ROAMING) {
            it++;
            continue;
        }

        Vec3 point = queue->front(); // get point
        queue->pop(); // remove point from front of queue

        // calculate displacement
        int dXY = hypot(point.x - npc->x, point.y - npc->y); // XY plane distance
        int distanceBetween = hypot(dXY, point.z - npc->z); // total distance

        // update NPC location to update viewables
        NPCManager::updateNPCPosition(npc->id, point.x, point.y, point.z, npc->instanceID, npc->angle);

        // TODO: move walking logic into Entity stack
        switch (npc->kind) {
        case EntityKind::BUS:
            INITSTRUCT(sP_FE2CL_TRANSPORTATION_MOVE, busMove);

            busMove.eTT = 3;
            busMove.iT_ID = npc->id;
            busMove.iMoveStyle = 0; // ???
            busMove.iToX = point.x;
            busMove.iToY = point.y;
            busMove.iToZ = point.z;
            busMove.iSpeed = distanceBetween; // set to distance to match how monkeys work

            NPCManager::sendToViewable(npc, &busMove, P_FE2CL_TRANSPORTATION_MOVE, sizeof(sP_FE2CL_TRANSPORTATION_MOVE));
            break;
        case EntityKind::MOB:
            MobAI::incNextMovement((Mob*)npc);
            /* fallthrough */
        default:
            INITSTRUCT(sP_FE2CL_NPC_MOVE, move);
            move.iNPC_ID = npc->id;
            move.iMoveStyle = 0; // ???
            move.iToX = point.x;
            move.iToY = point.y;
            move.iToZ = point.z;
            move.iSpeed = distanceBetween;

            NPCManager::sendToViewable(npc, &move, P_FE2CL_NPC_MOVE, sizeof(sP_FE2CL_NPC_MOVE));
            break;
        }

        /*
         * If this path should be repeated, move processed point to the back to maintain cycle.
         */
        if (npc->loopingPath)
            queue->push(point);

        it++; // go to next entry in map
    }
}

static void tickTransportationSystem(CNServer* serv, time_t currTime) {
    stepNPCPathing();
    stepSkywaySystem();
}

/*
 * Linearly interpolate between two points and insert the results into a queue.
 */
void Transport::lerp(std::queue<Vec3>* queue, Vec3 start, Vec3 end, int gapSize, float curve) {
    int dXY = hypot(end.x - start.x, end.y - start.y); // XY plane distance
    int distanceBetween = hypot(dXY, end.z - start.z); // total distance
    int lerps = distanceBetween / gapSize; // number of intermediate points to add
    for (int i = 1; i <= lerps; i++) {
        Vec3 lerp;
        // lerp math
        //float frac = i / (lerps + 1);
        float frac = powf(i, curve) / powf(lerps + 1, curve);
        lerp.x = (start.x * (1.0f - frac)) + (end.x * frac);
        lerp.y = (start.y * (1.0f - frac)) + (end.y * frac);
        lerp.z = (start.z * (1.0f - frac)) + (end.z * frac);
        queue->push(lerp); // add lerp'd point
    }
}
void Transport::lerp(std::queue<Vec3>* queue, Vec3 start, Vec3 end, int gapSize) {
    lerp(queue, start, end, gapSize, 1);
}

/*
 * Find and return the first path that targets either the type or the ID.
 * If no matches are found, return nullptr
 */
NPCPath* Transport::findApplicablePath(int32_t id, int32_t type, int taskID) {
    NPCPath* match = nullptr;
    for (auto _path = Transport::NPCPaths.begin(); _path != Transport::NPCPaths.end(); _path++) {

        // task ID for the path must match so escorts don't start early
        if (_path->escortTaskID != taskID)
            continue;

        // search target IDs
        for (int32_t pID : _path->targetIDs) {
            if (id == pID) {
                match = &(*_path);
                break;
            }
        }

        if (match != nullptr)
            break; // early break for ID matches, since ID has higher priority than type

        // search target types
        for (int32_t pType : _path->targetTypes) {
            if (type == pType) {
                match = &(*_path);
                break;
            }
        }

        if (match != nullptr)
            break;
    }
    return match;
}

void Transport::constructPathNPC(int32_t id, NPCPath* path) {
    BaseNPC* npc = NPCManager::NPCs[id];

    if (npc->kind == EntityKind::MOB) {
        auto mob = (Mob*)npc;
        mob->staticPath = true;

        Vec3 firstPoint = path->points.front();

        // Ensure that the first point coincides with the mob's spawn point.
        if (mob->spawnX != firstPoint.x || mob->spawnY != firstPoint.y) {
            std::cout << "[FATAL] The first point of the route for mob " << mob->id << " (type " << mob->type
                << ") does not correspond with its spawn point." << std::endl;
            exit(1);
        }
    }

    npc->loopingPath = path->isLoop;

    // Interpolate
    std::vector<Vec3> pathPoints = path->points;
    std::queue<Vec3> points;

    auto _point = pathPoints.begin();
    Vec3 from = *_point; // point A coords
    for (_point++; _point != pathPoints.end(); _point++) { // loop through all point Bs
        Vec3 to = *_point; // point B coords
        // add point A to the queue
        if (path->isRelative) {
            // relative; the NPCs current position is assumed to be its spawn point
            Vec3 fromReal = { from.x + npc->x, from.y + npc->y, from.z + npc->z };
            Vec3 toReal = { to.x + npc->x, to.y + npc->y, to.z + npc->z };
            points.push(fromReal);
            Transport::lerp(&points, fromReal, toReal, path->speed); // lerp from A to B
        }
        else {
            // absolute
            points.push(from);
            Transport::lerp(&points, from, to, path->speed); // lerp from A to B
        }

        from = to; // update point A
    }

    Transport::NPCQueues[id] = points;
}

void Transport::init() {
    REGISTER_SHARD_TIMER(tickTransportationSystem, 1000);

    REGISTER_SHARD_PACKET(P_CL2FE_REQ_REGIST_TRANSPORTATION_LOCATION, transportRegisterLocationHandler);
    REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_WARP_USE_TRANSPORTATION, transportWarpHandler);
}
