/*
 * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program. If not, see .
 */
#include "QuestPools.h"
#include "Containers.h"
#include "DatabaseEnv.h"
#include "Log.h"
#include "ObjectMgr.h"
#include "QuestDef.h"
#include "Timer.h"
#include 
#include 
/*static*/ QuestPoolMgr* QuestPoolMgr::instance()
{
    static QuestPoolMgr instance;
    return &instance;
}
static void RegeneratePool(QuestPool& pool)
{
    ASSERT(!pool.members.empty(), "Quest pool %u is empty", pool.poolId);
    uint32 const n = pool.members.size()-1;
    ASSERT(pool.numActive <= pool.members.size(), "Quest Pool %u requests %u spawns, but has only %u members.", pool.poolId, pool.numActive, uint32(pool.members.size()));
    pool.activeQuests.clear();
    for (uint32 i = 0; i < pool.numActive; ++i)
    {
        uint32 j = urand(i,n);
        if (i != j)
            std::swap(pool.members[i], pool.members[j]);
        for (uint32 quest : pool.members[i])
            pool.activeQuests.insert(quest);
    }
}
static void SaveToDB(QuestPool const& pool, CharacterDatabaseTransaction trans)
{
    CharacterDatabasePreparedStatement* delStmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_POOL_QUEST_SAVE);
    delStmt->setUInt32(0, pool.poolId);
    trans->Append(delStmt);
    for (uint32 questId : pool.activeQuests)
    {
        CharacterDatabasePreparedStatement* insStmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_POOL_QUEST_SAVE);
        insStmt->setUInt32(0, pool.poolId);
        insStmt->setUInt32(1, questId);
        trans->Append(insStmt);
    }
}
void QuestPoolMgr::LoadFromDB()
{
    uint32 oldMSTime = getMSTime();
    std::unordered_map*, uint32>> lookup; // poolId -> (vector, index)
    _poolLookup.clear();
    _dailyPools.clear();
    _weeklyPools.clear();
    _monthlyPools.clear();
    // load template data from world DB
    {
        QueryResult result = WorldDatabase.Query("SELECT qpm.questId, qpm.poolId, qpm.poolIndex, qpt.numActive FROM quest_pool_members qpm LEFT JOIN quest_pool_template qpt ON qpm.poolId = qpt.poolId");
        if (!result)
        {
            TC_LOG_INFO("server.loading", ">> Loaded 0 quest pools. DB table `quest_pool_members` is empty.");
            return;
        }
        do
        {
            Field* fields = result->Fetch();
            if (fields[2].IsNull())
            {
                TC_LOG_ERROR("sql.sql", "Table `quest_pool_members` contains reference to non-existing pool {}. Skipped.", fields[1].GetUInt32());
                continue;
            }
            uint32 questId = fields[0].GetUInt32();
            uint32 poolId = fields[1].GetUInt32();
            uint32 poolIndex = fields[2].GetUInt8();
            uint32 numActive = fields[3].GetUInt32();
            Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
            if (!quest)
            {
                TC_LOG_ERROR("sql.sql", "Table `quest_pool_members` contains reference to non-existing quest {}. Skipped.", questId);
                continue;
            }
            if (!quest->IsDailyOrWeekly() && !quest->IsMonthly())
            {
                TC_LOG_ERROR("sql.sql", "Table `quest_pool_members` contains reference to quest {}, which is neither daily, weekly nor monthly. Skipped.", questId);
                continue;
            }
            auto& pair = lookup[poolId];
            if (!pair.first)
            {
                pair.first = (quest->IsDaily() ? &_dailyPools : quest->IsWeekly() ? &_weeklyPools : &_monthlyPools);
                pair.first->emplace_back();
                pair.second = (pair.first->size()-1);
                pair.first->back().poolId = poolId;
                pair.first->back().numActive = numActive;
            }
            QuestPool::Members& members = (*pair.first)[pair.second].members;
            if (!(poolIndex < members.size()))
                members.resize(poolIndex+1);
            members[poolIndex].push_back(questId);
        } while (result->NextRow());
    }
    // load saved spawns from character DB
    {
        QueryResult result = CharacterDatabase.Query("SELECT pool_id, quest_id FROM pool_quest_save");
        if (result)
        {
            std::unordered_set unknownPoolIds;
            do
            {
                Field* fields = result->Fetch();
                uint32 poolId = fields[0].GetUInt32();
                uint32 questId = fields[1].GetUInt32();
                auto it = lookup.find(poolId);
                if (it == lookup.end() || !it->second.first)
                {
                    TC_LOG_ERROR("sql.sql", "Table `pool_quest_save` contains reference to non-existant quest pool {}. Deleted.", poolId);
                    unknownPoolIds.insert(poolId);
                    continue;
                }
                (*it->second.first)[it->second.second].activeQuests.insert(questId);
            } while (result->NextRow());
            CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
            for (uint32 poolId : unknownPoolIds)
            {
                CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_POOL_QUEST_SAVE);
                stmt->setUInt32(0, poolId);
                trans->Append(stmt);
            }
            CharacterDatabase.CommitTransaction(trans);
        }
    }
    // post-processing and sanity checks
    CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
    for (auto const& pair : lookup)
    {
        if (!pair.second.first)
            continue;
        QuestPool& pool = (*pair.second.first)[pair.second.second];
        if (pool.members.size() < pool.numActive)
        {
            TC_LOG_ERROR("sql.sql", "Table `quest_pool_template` contains quest pool {} requesting {} spawns, but only has {} members. Requested spawns reduced.", pool.poolId, pool.numActive, pool.members.size());
            pool.numActive = pool.members.size();
        }
        bool doRegenerate = pool.activeQuests.empty();
        if (!doRegenerate)
        {
            std::unordered_set accountedFor;
            uint32 activeCount = 0;
            for (size_t i = pool.members.size(); (i--);)
            {
                QuestPool::Member& member = pool.members[i];
                if (member.empty())
                {
                    TC_LOG_ERROR("sql.sql", "Table `quest_pool_members` contains no entries at index {} for quest pool {}. Index removed.", i, pool.poolId);
                    std::swap(pool.members[i], pool.members.back());
                    pool.members.pop_back();
                    continue;
                }
                // check if the first member is active
                auto itFirst = pool.activeQuests.find(member[0]);
                bool status = (itFirst != pool.activeQuests.end());
                // temporarily remove any spawns that are accounted for
                if (status)
                {
                    accountedFor.insert(member[0]);
                    pool.activeQuests.erase(itFirst);
                }
                // now check if all other members also have the same status, and warn if not
                for (auto it = member.begin(); (++it) != member.end();)
                {
                    auto itOther = pool.activeQuests.find(*it);
                    bool otherStatus = (itOther != pool.activeQuests.end());
                    if (status != otherStatus)
                        TC_LOG_WARN("sql.sql", "Table `pool_quest_save` {} quest {} (in pool {}, index {}) saved, but its index is{} active (because quest {} is{} in the table). Set quest {} to {}active.", (status ? "does not have" : "has"), *it, pool.poolId, i, (status ? "" : " not"), member[0], (status ? "" : " not"), *it, (status ? "" : "in"));
                    if (otherStatus)
                        pool.activeQuests.erase(itOther);
                    if (status)
                        accountedFor.insert(*it);
                }
                if (status)
                    ++activeCount;
            }
            // warn for any remaining active spawns (not part of the pool)
            for (uint32 quest : pool.activeQuests)
                TC_LOG_WARN("sql.sql", "Table `pool_quest_save` has saved quest {} for pool {}, but that quest is not part of the pool. Skipped.", quest, pool.poolId);
            // only the previously-found spawns should actually be active
            std::swap(pool.activeQuests, accountedFor);
            if (activeCount != pool.numActive)
            {
                doRegenerate = true;
                TC_LOG_ERROR("sql.sql", "Table `pool_quest_save` has {} active members saved for pool {}, which requests {} active members. Pool spawns re-generated.", activeCount, pool.poolId, pool.numActive);
            }
        }
        if (doRegenerate)
        {
            RegeneratePool(pool);
            SaveToDB(pool, trans);
        }
        for (QuestPool::Member const& member : pool.members)
        {
            for (uint32 quest : member)
            {
                QuestPool*& ref = _poolLookup[quest];
                if (ref)
                {
                    TC_LOG_ERROR("sql.sql", "Table `quest_pool_members` lists quest {} as member of pool {}, but it is already a member of pool {}. Skipped.", quest, pool.poolId, ref->poolId);
                    continue;
                }
                ref = &pool;
            }
        }
    }
    CharacterDatabase.CommitTransaction(trans);
    TC_LOG_INFO("server.loading", ">> Loaded {} daily, {} weekly and {} monthly quest pools in {} ms", _dailyPools.size(), _weeklyPools.size(), _monthlyPools.size(), GetMSTimeDiffToNow(oldMSTime));
}
void QuestPoolMgr::Regenerate(std::vector& pools)
{
    CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
    for (QuestPool& pool : pools)
    {
        RegeneratePool(pool);
        SaveToDB(pool, trans);
    }
    CharacterDatabase.CommitTransaction(trans);
}
// the storage structure ends up making this kind of inefficient
// we don't use it in practice (only in debug commands), so that's fine
QuestPool const* QuestPoolMgr::FindQuestPool(uint32 poolId) const
{
    auto lambda = [poolId](QuestPool const& p) { return (p.poolId == poolId); };
    auto it = std::find_if(_dailyPools.begin(), _dailyPools.end(), lambda);
    if (it != _dailyPools.end())
        return &*it;
    it = std::find_if(_weeklyPools.begin(), _weeklyPools.end(), lambda);
    if (it != _weeklyPools.end())
        return &*it;
    it = std::find_if(_monthlyPools.begin(), _monthlyPools.end(), lambda);
    if (it != _monthlyPools.end())
        return &*it;
    return nullptr;
}
bool QuestPoolMgr::IsQuestActive(uint32 questId) const
{
    auto it = _poolLookup.find(questId);
    if (it == _poolLookup.end()) // not pooled
        return true;
    return (it->second->activeQuests.find(questId) != it->second->activeQuests.end());
}