#include "db/internal.hpp"

// Loading and saving players to/from the DB

void Database::getPlayer(Player* plr, int id) {
    std::lock_guard<std::mutex> lock(dbCrit);

    const char* sql = R"(
        SELECT
            p.AccountID, p.Slot, p.FirstName, p.LastName,
            p.Level, p.Nano1, p.Nano2, p.Nano3,
            p.AppearanceFlag, p.TutorialFlag, p.PayZoneFlag,
            p.XCoordinate, p.YCoordinate, p.ZCoordinate, p.NameCheck,
            p.Angle, p.HP, acc.AccountLevel, p.FusionMatter, p.Taros, p.Quests,
            p.BatteryW, p.BatteryN, p.Mentor, p.WarpLocationFlag,
            p.SkywayLocationFlag, p.CurrentMissionID, p.FirstUseFlag,
            a.Body, a.EyeColor, a.FaceStyle, a.Gender, a.HairColor, a.HairStyle, a.Height, a.SkinColor
        FROM Players as p
        INNER JOIN Appearances as a ON p.PlayerID = a.PlayerID
        INNER JOIN Accounts as acc ON p.AccountID = acc.AccountID
        WHERE p.PlayerID = ?;
        )";
    sqlite3_stmt* stmt;

    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, id);
    if (sqlite3_step(stmt) != SQLITE_ROW) {
        sqlite3_finalize(stmt);
        std::cout << "[WARN] Database: Failed to load character [" << id << "]: " << sqlite3_errmsg(db) << std::endl;
        return;
    }

    plr->iID = id;
    plr->PCStyle.iPC_UID = id;

    plr->accountId = sqlite3_column_int(stmt, 0);
    plr->slot = sqlite3_column_int(stmt, 1);

    // parsing const unsigned char* to char16_t
    std::string placeHolder = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2)));
    U8toU16(placeHolder, plr->PCStyle.szFirstName, sizeof(plr->PCStyle.szFirstName));
    placeHolder = std::string(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3)));
    U8toU16(placeHolder, plr->PCStyle.szLastName, sizeof(plr->PCStyle.szLastName));

    plr->level = sqlite3_column_int(stmt, 4);
    plr->equippedNanos[0] = sqlite3_column_int(stmt, 5);
    plr->equippedNanos[1] = sqlite3_column_int(stmt, 6);
    plr->equippedNanos[2] = sqlite3_column_int(stmt, 7);

    plr->PCStyle2.iAppearanceFlag = sqlite3_column_int(stmt, 8);
    plr->PCStyle2.iTutorialFlag = sqlite3_column_int(stmt, 9);
    plr->PCStyle2.iPayzoneFlag = sqlite3_column_int(stmt, 10);

    plr->x = sqlite3_column_int(stmt, 11);
    plr->y = sqlite3_column_int(stmt, 12);
    plr->z = sqlite3_column_int(stmt, 13);
    plr->PCStyle.iNameCheck = sqlite3_column_int(stmt, 14);

    plr->angle = sqlite3_column_int(stmt, 15);
    plr->HP = sqlite3_column_int(stmt, 16);
    plr->accountLevel = sqlite3_column_int(stmt, 17);
    plr->setCapped(CappedValueType::FUSIONMATTER, sqlite3_column_int(stmt, 18));
    plr->setCapped(CappedValueType::TAROS, sqlite3_column_int(stmt, 19));

    memcpy(plr->aQuestFlag, sqlite3_column_blob(stmt, 20), sizeof(plr->aQuestFlag));

    plr->setCapped(CappedValueType::BATTERY_W, sqlite3_column_int(stmt, 21));
    plr->setCapped(CappedValueType::BATTERY_N, sqlite3_column_int(stmt, 22));
    plr->mentor = sqlite3_column_int(stmt, 23);
    plr->iWarpLocationFlag = sqlite3_column_int(stmt, 24);

    memcpy(plr->aSkywayLocationFlag, sqlite3_column_blob(stmt, 25), sizeof(plr->aSkywayLocationFlag));

    plr->CurrentMissionID = sqlite3_column_int(stmt, 26);

    memcpy(plr->iFirstUseFlag, sqlite3_column_blob(stmt, 27), sizeof(plr->iFirstUseFlag));

    plr->PCStyle.iBody = sqlite3_column_int(stmt, 28);
    plr->PCStyle.iEyeColor = sqlite3_column_int(stmt, 29);
    plr->PCStyle.iFaceStyle = sqlite3_column_int(stmt, 30);
    plr->PCStyle.iGender = sqlite3_column_int(stmt, 31);
    plr->PCStyle.iHairColor = sqlite3_column_int(stmt, 32);
    plr->PCStyle.iHairStyle = sqlite3_column_int(stmt, 33);
    plr->PCStyle.iHeight = sqlite3_column_int(stmt, 34);
    plr->PCStyle.iSkinColor = sqlite3_column_int(stmt, 35);

    sqlite3_finalize(stmt);

    // get inventory
    sql = R"(
        SELECT Slot, Type, ID, Opt, TimeLimit
        FROM Inventory
        WHERE PlayerID = ?;
        )";

    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);

    sqlite3_bind_int(stmt, 1, id);

    while (sqlite3_step(stmt) == SQLITE_ROW) {
        int slot = sqlite3_column_int(stmt, 0);

        // for extra safety
        if (slot < 0 || slot > AEQUIP_COUNT + AINVEN_COUNT + ABANK_COUNT) {
            std::cout << "[WARN] Database: Invalid item slot in db?! " << std::endl;
            continue;
        }

        sItemBase* item;
        if (slot < AEQUIP_COUNT) {
            // equipment
            item = &plr->Equip[slot];
        } else if (slot < (AEQUIP_COUNT + AINVEN_COUNT)) {
            // inventory
            item = &plr->Inven[slot - AEQUIP_COUNT];
        } else {
            // bank
            item = &plr->Bank[slot - AEQUIP_COUNT - AINVEN_COUNT];
        }

        item->iType = sqlite3_column_int(stmt, 1);
        item->iID = sqlite3_column_int(stmt, 2);
        item->iOpt = sqlite3_column_int(stmt, 3);
        item->iTimeLimit = sqlite3_column_int(stmt, 4);
    }

    sqlite3_finalize(stmt);

    // get quest inventory
    sql = R"(
        SELECT Slot, ID, Opt
        FROM QuestItems
        WHERE PlayerID = ?;
        )";

    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);

    sqlite3_bind_int(stmt, 1, id);

    while (sqlite3_step(stmt) == SQLITE_ROW) {
        int slot = sqlite3_column_int(stmt, 0);

        // for extra safety
        if (slot < 0)
            continue;

        sItemBase* item = &plr->QInven[slot];
        item->iType = 8;
        item->iID = sqlite3_column_int(stmt, 1);
        item->iOpt = sqlite3_column_int(stmt, 2);
    }

    sqlite3_finalize(stmt);

    // get nanos
    sql = R"(
        SELECT ID, Skill, Stamina
        FROM Nanos
        WHERE PlayerID = ?;
        )";

    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, id);

    while (sqlite3_step(stmt) == SQLITE_ROW) {
        int id = sqlite3_column_int(stmt, 0);

        // for extra safety
        if (id < 0 || id > NANO_COUNT)
            continue;

        sNano* nano = &plr->Nanos[id];
        nano->iID = id;
        nano->iSkillID = sqlite3_column_int(stmt, 1);
        nano->iStamina = sqlite3_column_int(stmt, 2);
    }

    sqlite3_finalize(stmt);

    // get active quests
    sql = R"(
        SELECT
            TaskID,
            RemainingNPCCount1,
            RemainingNPCCount2,
            RemainingNPCCount3
        FROM RunningQuests
        WHERE PlayerID = ?;
        )";

    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, id);

    std::set<int> tasksSet; // used to prevent duplicate tasks from loading in
    for (int i = 0; sqlite3_step(stmt) == SQLITE_ROW && i < ACTIVE_MISSION_COUNT; i++) {

        int taskID = sqlite3_column_int(stmt, 0);
        if (tasksSet.find(taskID) != tasksSet.end())
            continue;

        plr->tasks[i] = taskID;
        tasksSet.insert(taskID);
        plr->RemainingNPCCount[i][0] = sqlite3_column_int(stmt, 1);
        plr->RemainingNPCCount[i][1] = sqlite3_column_int(stmt, 2);
        plr->RemainingNPCCount[i][2] = sqlite3_column_int(stmt, 3);
    }

    sqlite3_finalize(stmt);

    // get buddies
    sql = R"(
        SELECT PlayerAID, PlayerBID
        FROM Buddyships
        WHERE PlayerAID = ? OR PlayerBID = ?;
        )";

    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, id);
    sqlite3_bind_int(stmt, 2, id);

    int i = 0;
    while (sqlite3_step(stmt) == SQLITE_ROW && i < 50) {
        int PlayerAId = sqlite3_column_int(stmt, 0);
        int PlayerBId = sqlite3_column_int(stmt, 1);

        plr->buddyIDs[i] = id == PlayerAId ? PlayerBId : PlayerAId;
        plr->isBuddyBlocked[i] = false;
        i++;
    }

    sqlite3_finalize(stmt);

    // get blocked players
    sql = R"(
        SELECT BlockedPlayerID FROM Blocks
        WHERE PlayerID = ?;
        )";

    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, id);

    // i retains its value from after the loop over Buddyships
    while (sqlite3_step(stmt) == SQLITE_ROW && i < 50) {
        plr->buddyIDs[i] = sqlite3_column_int(stmt, 0);
        plr->isBuddyBlocked[i] = true;
        i++;
    }

    sqlite3_finalize(stmt);
}

/*
 * Low-level function to save a player to DB.
 * Must be run in a SQL transaction and with dbCrit locked.
 * The caller manages the transacstion, so if this function returns false,
 * the caller must roll it back.
 */
bool Database::_updatePlayer(Player *player) {
    const char* sql = R"(
        UPDATE Players
        SET
            Level = ? , Nano1 = ?, Nano2 = ?, Nano3 = ?,
            XCoordinate = ?, YCoordinate = ?, ZCoordinate = ?,
            Angle = ?, HP = ?, FusionMatter = ?, Taros = ?, Quests = ?,
            BatteryW = ?, BatteryN = ?, WarplocationFlag = ?,
            SkywayLocationFlag = ?, CurrentMissionID = ?,
            PayZoneFlag = ?, FirstUseFlag = ?, Mentor = ?
        WHERE PlayerID = ?;
        )";
    sqlite3_stmt* stmt;
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, player->level);
    sqlite3_bind_int(stmt, 2, player->equippedNanos[0]);
    sqlite3_bind_int(stmt, 3, player->equippedNanos[1]);
    sqlite3_bind_int(stmt, 4, player->equippedNanos[2]);

    if (player->instanceID == 0 && !player->onMonkey) {
        sqlite3_bind_int(stmt, 5, player->x);
        sqlite3_bind_int(stmt, 6, player->y);
        sqlite3_bind_int(stmt, 7, player->z);
        sqlite3_bind_int(stmt, 8, player->angle);
    }
    else {
        sqlite3_bind_int(stmt, 5, player->lastX);
        sqlite3_bind_int(stmt, 6, player->lastY);
        sqlite3_bind_int(stmt, 7, player->lastZ);
        sqlite3_bind_int(stmt, 8, player->lastAngle);
    }

    sqlite3_bind_int(stmt, 9, player->HP);
    sqlite3_bind_int(stmt, 10, player->fusionmatter);
    sqlite3_bind_int(stmt, 11, player->money);
    sqlite3_bind_blob(stmt, 12, player->aQuestFlag, sizeof(player->aQuestFlag), NULL);
    sqlite3_bind_int(stmt, 13, player->batteryW);
    sqlite3_bind_int(stmt, 14, player->batteryN);
    sqlite3_bind_int(stmt, 15, player->iWarpLocationFlag);
    sqlite3_bind_blob(stmt, 16, player->aSkywayLocationFlag, sizeof(player->aSkywayLocationFlag), NULL);
    sqlite3_bind_int(stmt, 17, player->CurrentMissionID);
    sqlite3_bind_int(stmt, 18, player->PCStyle2.iPayzoneFlag);
    sqlite3_bind_blob(stmt, 19, player->iFirstUseFlag, sizeof(player->iFirstUseFlag), NULL);
    sqlite3_bind_int(stmt, 20, player->mentor);
    sqlite3_bind_int(stmt, 21, player->iID);

    if (sqlite3_step(stmt) != SQLITE_DONE) {
        sqlite3_finalize(stmt);
        return false;
    }

    sqlite3_finalize(stmt);

    // update inventory
    sql = R"(
        DELETE FROM Inventory WHERE PlayerID = ?;
        )";
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, player->iID);
    int rc = sqlite3_step(stmt);

    sqlite3_finalize(stmt);

    sql = R"(
        INSERT INTO Inventory
            (PlayerID, Slot, Type, Opt, ID, Timelimit)
        VALUES (?, ?, ?, ?, ?, ?);
        )";
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);

    for (int i = 0; i < AEQUIP_COUNT; i++) {
        if (player->Equip[i].iID == 0)
            continue;

        sqlite3_bind_int(stmt, 1, player->iID);
        sqlite3_bind_int(stmt, 2, i);
        sqlite3_bind_int(stmt, 3, player->Equip[i].iType);
        sqlite3_bind_int(stmt, 4, player->Equip[i].iOpt);
        sqlite3_bind_int(stmt, 5, player->Equip[i].iID);
        sqlite3_bind_int(stmt, 6, player->Equip[i].iTimeLimit);

        rc = sqlite3_step(stmt);

        if (rc != SQLITE_DONE) {
            sqlite3_finalize(stmt);
            return false;
        }
        sqlite3_reset(stmt);
    }

    for (int i = 0; i < AINVEN_COUNT; i++) {
        if (player->Inven[i].iID == 0)
            continue;

        sqlite3_bind_int(stmt, 1, player->iID);
        sqlite3_bind_int(stmt, 2, i + AEQUIP_COUNT);
        sqlite3_bind_int(stmt, 3, player->Inven[i].iType);
        sqlite3_bind_int(stmt, 4, player->Inven[i].iOpt);
        sqlite3_bind_int(stmt, 5, player->Inven[i].iID);
        sqlite3_bind_int(stmt, 6, player->Inven[i].iTimeLimit);

        if (sqlite3_step(stmt) != SQLITE_DONE) {
            sqlite3_finalize(stmt);
            return false;
        }
        sqlite3_reset(stmt);
    }

    for (int i = 0; i < ABANK_COUNT; i++) {
        if (player->Bank[i].iID == 0)
            continue;

        sqlite3_bind_int(stmt, 1, player->iID);
        sqlite3_bind_int(stmt, 2, i + AEQUIP_COUNT + AINVEN_COUNT);
        sqlite3_bind_int(stmt, 3, player->Bank[i].iType);
        sqlite3_bind_int(stmt, 4, player->Bank[i].iOpt);
        sqlite3_bind_int(stmt, 5, player->Bank[i].iID);
        sqlite3_bind_int(stmt, 6, player->Bank[i].iTimeLimit);

        if (sqlite3_step(stmt) != SQLITE_DONE) {
            sqlite3_finalize(stmt);
            return false;
        }
        sqlite3_reset(stmt);
    }

    sqlite3_finalize(stmt);

    // Update Quest Inventory
    sql = R"(
        DELETE FROM QuestItems WHERE PlayerID = ?;
        )";
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, player->iID);
    sqlite3_step(stmt);

    sqlite3_finalize(stmt);

    sql = R"(
        INSERT INTO QuestItems (PlayerID, Slot, Opt, ID)
        VALUES (?, ?, ?, ?);
        )";
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);

    for (int i = 0; i < AQINVEN_COUNT; i++) {
        if (player->QInven[i].iID == 0)
            continue;

        sqlite3_bind_int(stmt, 1, player->iID);
        sqlite3_bind_int(stmt, 2, i);
        sqlite3_bind_int(stmt, 3, player->QInven[i].iOpt);
        sqlite3_bind_int(stmt, 4, player->QInven[i].iID);

        if (sqlite3_step(stmt) != SQLITE_DONE) {
            sqlite3_finalize(stmt);
            return false;
        }
        sqlite3_reset(stmt);
    }

    sqlite3_finalize(stmt);

    // Update Nanos
    sql = R"(
        DELETE FROM Nanos WHERE PlayerID = ?;
        )";
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, player->iID);
    sqlite3_step(stmt);

    sqlite3_finalize(stmt);

    sql = R"(
        INSERT INTO Nanos (PlayerID, ID, SKill, Stamina)
        VALUES (?, ?, ?, ?);
        )";
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);

    for (int i = 0; i < NANO_COUNT; i++) {
        if (player->Nanos[i].iID == 0)
            continue;

        sqlite3_bind_int(stmt, 1, player->iID);
        sqlite3_bind_int(stmt, 2, player->Nanos[i].iID);
        sqlite3_bind_int(stmt, 3, player->Nanos[i].iSkillID);
        sqlite3_bind_int(stmt, 4, player->Nanos[i].iStamina);

        if (sqlite3_step(stmt) != SQLITE_DONE) {
            sqlite3_finalize(stmt);
            return false;
        }
        sqlite3_reset(stmt);
    }

    sqlite3_finalize(stmt);

    // Update Running Quests
    sql = R"(
        DELETE FROM RunningQuests WHERE PlayerID = ?;
        )";
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, player->iID);
    sqlite3_step(stmt);

    sqlite3_finalize(stmt);

    sql = R"(
        INSERT INTO RunningQuests
            (PlayerID, TaskID, RemainingNPCCount1, RemainingNPCCount2, RemainingNPCCount3)
        VALUES (?, ?, ?, ?, ?);
        )";
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);

    for (int i = 0; i < ACTIVE_MISSION_COUNT; i++) {
        if (player->tasks[i] == 0)
            continue;
        sqlite3_bind_int(stmt, 1, player->iID);
        sqlite3_bind_int(stmt, 2, player->tasks[i]);
        sqlite3_bind_int(stmt, 3, player->RemainingNPCCount[i][0]);
        sqlite3_bind_int(stmt, 4, player->RemainingNPCCount[i][1]);
        sqlite3_bind_int(stmt, 5, player->RemainingNPCCount[i][2]);

        if (sqlite3_step(stmt) != SQLITE_DONE) {
            sqlite3_finalize(stmt);
            return false;
        }
        sqlite3_reset(stmt);
    }

    sqlite3_finalize(stmt);

    return true;
}

void Database::updatePlayer(Player *player) {
    std::lock_guard<std::mutex> lock(dbCrit);

    sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);

    if (!_updatePlayer(player)) {
        std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
        sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
        return;
    }

    sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
}

void Database::commitTrade(Player *plr1, Player *plr2) {
    std::lock_guard<std::mutex> lock(dbCrit);

    sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);

    if (!_updatePlayer(plr1)) {
        std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
        sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
        return;
    }

    if (!_updatePlayer(plr2)) {
        std::cout << "[WARN] Database: Failed to save player to database: " << sqlite3_errmsg(db) << std::endl;
        sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
        return;
    }

    sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
}
