#include "npcstats.hpp"

#include <iomanip>

#include <components/esm/loadclas.hpp>
#include <components/esm/loadgmst.hpp>
#include <components/esm/loadfact.hpp>
#include <components/esm/npcstats.hpp>

#include "../mwworld/esmstore.hpp"

#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwbase/windowmanager.hpp"

MWMechanics::NpcStats::NpcStats()
    : mDisposition (0)
, mReputation(0)
, mCrimeId(-1)
, mBounty(0)
, mWerewolfKills (0)
, mLevelProgress(0)
, mTimeToStartDrowning(-1.0) // set breath to special value, it will be replaced during actor update
    , mIsWerewolf(false)
{
    mSkillIncreases.resize (ESM::Attribute::Length, 0);
    mSpecIncreases.resize(3, 0);
}

int MWMechanics::NpcStats::getBaseDisposition() const
{
    return mDisposition;
}

void MWMechanics::NpcStats::setBaseDisposition(int disposition)
{
    mDisposition = disposition;
}

const MWMechanics::SkillValue& MWMechanics::NpcStats::getSkill (int index) const
{
    if (index<0 || index>=ESM::Skill::Length)
        throw std::runtime_error ("skill index out of range");

    return mSkill[index];
}

MWMechanics::SkillValue& MWMechanics::NpcStats::getSkill (int index)
{
    if (index<0 || index>=ESM::Skill::Length)
        throw std::runtime_error ("skill index out of range");

    return mSkill[index];
}

void MWMechanics::NpcStats::setSkill(int index, const MWMechanics::SkillValue &value)
{
    if (index<0 || index>=ESM::Skill::Length)
        throw std::runtime_error ("skill index out of range");

    mSkill[index] = value;
}

const std::map<std::string, int>& MWMechanics::NpcStats::getFactionRanks() const
{
    return mFactionRank;
}

int MWMechanics::NpcStats::getFactionRank(const std::string &faction) const
{
    const std::string lower = Misc::StringUtils::lowerCase(faction);
    std::map<std::string, int>::const_iterator it = mFactionRank.find(lower);
    if (it != mFactionRank.end())
        return it->second;

    return -1;
}

void MWMechanics::NpcStats::raiseRank(const std::string &faction)
{
    const std::string lower = Misc::StringUtils::lowerCase(faction);
    std::map<std::string, int>::iterator it = mFactionRank.find(lower);
    if (it != mFactionRank.end())
    {
        // Does the next rank exist?
        const ESM::Faction* factionPtr = MWBase::Environment::get().getWorld()->getStore().get<ESM::Faction>().find(lower);
        if (it->second+1 < 10 && !factionPtr->mRanks[it->second+1].empty())
            it->second += 1;
    }
}

void MWMechanics::NpcStats::lowerRank(const std::string &faction)
{
    const std::string lower = Misc::StringUtils::lowerCase(faction);
    std::map<std::string, int>::iterator it = mFactionRank.find(lower);
    if (it != mFactionRank.end())
    {
        it->second = it->second-1;
        if (it->second < 0)
        {
            mFactionRank.erase(it);
            mExpelled.erase(lower);
        }
    }
}

void MWMechanics::NpcStats::joinFaction(const std::string& faction)
{
    const std::string lower = Misc::StringUtils::lowerCase(faction);
    std::map<std::string, int>::iterator it = mFactionRank.find(lower);
    if (it == mFactionRank.end())
        mFactionRank[lower] = 0;
}

bool MWMechanics::NpcStats::getExpelled(const std::string& factionID) const
{
    return mExpelled.find(Misc::StringUtils::lowerCase(factionID)) != mExpelled.end();
}

void MWMechanics::NpcStats::expell(const std::string& factionID)
{
    std::string lower = Misc::StringUtils::lowerCase(factionID);
    if (mExpelled.find(lower) == mExpelled.end())
    {
        std::string message = "#{sExpelledMessage}";
        message += MWBase::Environment::get().getWorld()->getStore().get<ESM::Faction>().find(factionID)->mName;
        MWBase::Environment::get().getWindowManager()->messageBox(message);
        mExpelled.insert(lower);
    }
}

void MWMechanics::NpcStats::clearExpelled(const std::string& factionID)
{
    mExpelled.erase(Misc::StringUtils::lowerCase(factionID));
}

bool MWMechanics::NpcStats::isInFaction (const std::string& faction) const
{
    return (mFactionRank.find(Misc::StringUtils::lowerCase(faction)) != mFactionRank.end());
}

int MWMechanics::NpcStats::getFactionReputation (const std::string& faction) const
{
    std::map<std::string, int>::const_iterator iter = mFactionReputation.find (Misc::StringUtils::lowerCase(faction));

    if (iter==mFactionReputation.end())
        return 0;

    return iter->second;
}

void MWMechanics::NpcStats::setFactionReputation (const std::string& faction, int value)
{
    mFactionReputation[Misc::StringUtils::lowerCase(faction)] = value;
}

float MWMechanics::NpcStats::getSkillProgressRequirement (int skillIndex, const ESM::Class& class_) const
{
    float progressRequirement = static_cast<float>(1 + getSkill(skillIndex).getBase());

    const MWWorld::Store<ESM::GameSetting> &gmst =
        MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();

    float typeFactor = gmst.find ("fMiscSkillBonus")->mValue.getFloat();

    for (int i=0; i<5; ++i)
    {
        if (class_.mData.mSkills[i][0]==skillIndex)
        {
            typeFactor = gmst.find ("fMinorSkillBonus")->mValue.getFloat();
            break;
        }
        else if (class_.mData.mSkills[i][1]==skillIndex)
        {
            typeFactor = gmst.find ("fMajorSkillBonus")->mValue.getFloat();
            break;
        }
    }

    progressRequirement *= typeFactor;

    if (typeFactor<=0)
        throw std::runtime_error ("invalid skill type factor");

    float specialisationFactor = 1;

    const ESM::Skill *skill =
        MWBase::Environment::get().getWorld()->getStore().get<ESM::Skill>().find (skillIndex);
    if (skill->mData.mSpecialization==class_.mData.mSpecialization)
    {
        specialisationFactor = gmst.find ("fSpecialSkillBonus")->mValue.getFloat();

        if (specialisationFactor<=0)
            throw std::runtime_error ("invalid skill specialisation factor");
    }
    progressRequirement *= specialisationFactor;

    return progressRequirement;
}

void MWMechanics::NpcStats::useSkill (int skillIndex, const ESM::Class& class_, int usageType, float extraFactor)
{
    const ESM::Skill *skill =
        MWBase::Environment::get().getWorld()->getStore().get<ESM::Skill>().find (skillIndex);
    float skillGain = 1;
    if (usageType>=4)
        throw std::runtime_error ("skill usage type out of range");
    if (usageType>=0)
    {
        skillGain = skill->mData.mUseValue[usageType];
        if (skillGain<0)
            throw std::runtime_error ("invalid skill gain factor");
    }
    skillGain *= extraFactor;

    MWMechanics::SkillValue& value = getSkill (skillIndex);

    value.setProgress(value.getProgress() + skillGain);

    if (int(value.getProgress())>=int(getSkillProgressRequirement(skillIndex, class_)))
    {
        // skill levelled up
        increaseSkill(skillIndex, class_, false);
    }
}

void MWMechanics::NpcStats::increaseSkill(int skillIndex, const ESM::Class &class_, bool preserveProgress, bool readBook)
{
    int base = getSkill (skillIndex).getBase();

    if (base >= 100)
        return;

    base += 1;

    const MWWorld::Store<ESM::GameSetting> &gmst =
        MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();

    // is this a minor or major skill?
    int increase = gmst.find("iLevelupMiscMultAttriubte")->mValue.getInteger(); // Note: GMST has a typo
    for (int k=0; k<5; ++k)
    {
        if (class_.mData.mSkills[k][0] == skillIndex)
        {
            mLevelProgress += gmst.find("iLevelUpMinorMult")->mValue.getInteger();
            increase = gmst.find("iLevelUpMinorMultAttribute")->mValue.getInteger();
            break;
        }
        else if (class_.mData.mSkills[k][1] == skillIndex)
        {
            mLevelProgress += gmst.find("iLevelUpMajorMult")->mValue.getInteger();
            increase = gmst.find("iLevelUpMajorMultAttribute")->mValue.getInteger();
            break;
        }
    }

    const ESM::Skill* skill =
        MWBase::Environment::get().getWorld ()->getStore ().get<ESM::Skill>().find(skillIndex);
    mSkillIncreases[skill->mData.mAttribute] += increase;

    mSpecIncreases[skill->mData.mSpecialization] += gmst.find("iLevelupSpecialization")->mValue.getInteger();

    // Play sound & skill progress notification
    /// \todo check if character is the player, if levelling is ever implemented for NPCs
    MWBase::Environment::get().getWindowManager()->playSound("skillraise");

    std::string message = MWBase::Environment::get().getWindowManager ()->getGameSettingString ("sNotifyMessage39", "");
    message = Misc::StringUtils::format(message, ("#{" + ESM::Skill::sSkillNameIds[skillIndex] + "}"), base);

    if (readBook)
        message = "#{sBookSkillMessage}\n" + message;
    
    MWBase::Environment::get().getWindowManager ()->messageBox(message, MWGui::ShowInDialogueMode_Never);

    if (mLevelProgress >= gmst.find("iLevelUpTotal")->mValue.getInteger())
    {
        // levelup is possible now
        MWBase::Environment::get().getWindowManager ()->messageBox ("#{sLevelUpMsg}", MWGui::ShowInDialogueMode_Never);
    }

    getSkill(skillIndex).setBase (base);
    if (!preserveProgress)
        getSkill(skillIndex).setProgress(0);
}

int MWMechanics::NpcStats::getLevelProgress () const
{
    return mLevelProgress;
}

void MWMechanics::NpcStats::levelUp()
{
    const MWWorld::Store<ESM::GameSetting> &gmst =
        MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();

    mLevelProgress -= gmst.find("iLevelUpTotal")->mValue.getInteger();
    mLevelProgress = std::max(0, mLevelProgress); // might be necessary when levelup was invoked via console

    for (int i=0; i<ESM::Attribute::Length; ++i)
        mSkillIncreases[i] = 0;

    const int endurance = getAttribute(ESM::Attribute::Endurance).getBase();

    // "When you gain a level, in addition to increasing three primary attributes, your Health
    // will automatically increase by 10% of your Endurance attribute. If you increased Endurance this level,
    // the Health increase is calculated from the increased Endurance"
    // Note: we should add bonus Health points to current level too.
    float healthGain = endurance * gmst.find("fLevelUpHealthEndMult")->mValue.getFloat();
    MWMechanics::DynamicStat<float> health(getHealth());
    health.setBase(getHealth().getBase() + healthGain);
    health.setCurrent(std::max(1.f, getHealth().getCurrent() + healthGain));
    setHealth(health);

    setLevel(getLevel()+1);
}

void MWMechanics::NpcStats::updateHealth()
{
    const int endurance = getAttribute(ESM::Attribute::Endurance).getBase();
    const int strength = getAttribute(ESM::Attribute::Strength).getBase();

    setHealth(floor(0.5f * (strength + endurance)));
}

int MWMechanics::NpcStats::getLevelupAttributeMultiplier(int attribute) const
{
    int num = mSkillIncreases[attribute];

    if (num == 0)
        return 1;

    num = std::min(10, num);

    // iLevelUp01Mult - iLevelUp10Mult
    std::stringstream gmst;
    gmst << "iLevelUp" << std::setfill('0') << std::setw(2) << num << "Mult";

    return MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(gmst.str())->mValue.getInteger();
}

int MWMechanics::NpcStats::getSkillIncreasesForSpecialization(int spec) const
{
    return mSpecIncreases[spec];
}

void MWMechanics::NpcStats::flagAsUsed (const std::string& id)
{
    mUsedIds.insert (id);
}

bool MWMechanics::NpcStats::hasBeenUsed (const std::string& id) const
{
    return mUsedIds.find (id)!=mUsedIds.end();
}

int MWMechanics::NpcStats::getBounty() const
{
    return mBounty;
}

void MWMechanics::NpcStats::setBounty (int bounty)
{
    mBounty = bounty;
}

int MWMechanics::NpcStats::getReputation() const
{
    return mReputation;
}

void MWMechanics::NpcStats::setReputation(int reputation)
{
    mReputation = reputation;
}

int MWMechanics::NpcStats::getCrimeId() const
{
    return mCrimeId;
}

void MWMechanics::NpcStats::setCrimeId(int id)
{
    mCrimeId = id;
}

bool MWMechanics::NpcStats::hasSkillsForRank (const std::string& factionId, int rank) const
{
    if (rank<0 || rank>=10)
        throw std::runtime_error ("rank index out of range");

    const ESM::Faction& faction =
        *MWBase::Environment::get().getWorld()->getStore().get<ESM::Faction>().find (factionId);

    std::vector<int> skills;

    for (int i=0; i<7; ++i)
    {
        if (faction.mData.mSkills[i] != -1)
            skills.push_back (static_cast<int> (getSkill (faction.mData.mSkills[i]).getBase()));
    }

    if (skills.empty())
        return true;

    std::sort (skills.begin(), skills.end());

    std::vector<int>::const_reverse_iterator iter = skills.rbegin();

    const ESM::RankData& rankData = faction.mData.mRankData[rank];

    if (*iter<rankData.mSkill1)
        return false;

    if (skills.size() < 2)
        return true;

    return *++iter>=rankData.mSkill2;
}

bool MWMechanics::NpcStats::isWerewolf() const
{
    return mIsWerewolf;
}

void MWMechanics::NpcStats::setWerewolf (bool set)
{
    if (mIsWerewolf == set)
        return;

    if(set != false)
    {
        mWerewolfKills = 0;
    }
    mIsWerewolf = set;
}

int MWMechanics::NpcStats::getWerewolfKills() const
{
    return mWerewolfKills;
}

void MWMechanics::NpcStats::addWerewolfKill()
{
    ++mWerewolfKills;
}

float MWMechanics::NpcStats::getTimeToStartDrowning() const
{
    return mTimeToStartDrowning;
}

void MWMechanics::NpcStats::setTimeToStartDrowning(float time)
{
    mTimeToStartDrowning=time;
}

void MWMechanics::NpcStats::writeState (ESM::NpcStats& state) const
{
    for (std::map<std::string, int>::const_iterator iter (mFactionRank.begin());
        iter!=mFactionRank.end(); ++iter)
        state.mFactions[iter->first].mRank = iter->second;

    state.mDisposition = mDisposition;

    for (int i=0; i<ESM::Skill::Length; ++i)
        mSkill[i].writeState (state.mSkills[i]);

    state.mIsWerewolf = mIsWerewolf;

    state.mCrimeId = mCrimeId;

    state.mBounty = mBounty;

    for (std::set<std::string>::const_iterator iter (mExpelled.begin());
        iter!=mExpelled.end(); ++iter)
        state.mFactions[*iter].mExpelled = true;

    for (std::map<std::string, int>::const_iterator iter (mFactionReputation.begin());
        iter!=mFactionReputation.end(); ++iter)
        state.mFactions[iter->first].mReputation = iter->second;

    state.mReputation = mReputation;
    state.mWerewolfKills = mWerewolfKills;
    state.mLevelProgress = mLevelProgress;

    for (int i=0; i<ESM::Attribute::Length; ++i)
        state.mSkillIncrease[i] = mSkillIncreases[i];

    for (int i=0; i<3; ++i)
        state.mSpecIncreases[i] = mSpecIncreases[i];

    std::copy (mUsedIds.begin(), mUsedIds.end(), std::back_inserter (state.mUsedIds));

    state.mTimeToStartDrowning = mTimeToStartDrowning;
}

void MWMechanics::NpcStats::readState (const ESM::NpcStats& state)
{
    const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();

    for (std::map<std::string, ESM::NpcStats::Faction>::const_iterator iter (state.mFactions.begin());
        iter!=state.mFactions.end(); ++iter)
        if (store.get<ESM::Faction>().search (iter->first))
        {
            if (iter->second.mExpelled)
                mExpelled.insert (iter->first);

            if (iter->second.mRank >= 0)
                mFactionRank[iter->first] = iter->second.mRank;

            if (iter->second.mReputation)
                mFactionReputation[Misc::StringUtils::lowerCase(iter->first)] = iter->second.mReputation;
        }

    mDisposition = state.mDisposition;

    for (int i=0; i<ESM::Skill::Length; ++i)
        mSkill[i].readState (state.mSkills[i]);

    mIsWerewolf = state.mIsWerewolf;

    mCrimeId = state.mCrimeId;
    mBounty = state.mBounty;
    mReputation = state.mReputation;
    mWerewolfKills = state.mWerewolfKills;
    mLevelProgress = state.mLevelProgress;

    for (int i=0; i<ESM::Attribute::Length; ++i)
        mSkillIncreases[i] = state.mSkillIncrease[i];

    for (int i=0; i<3; ++i)
        mSpecIncreases[i] = state.mSpecIncreases[i];

    for (std::vector<std::string>::const_iterator iter (state.mUsedIds.begin());
        iter!=state.mUsedIds.end(); ++iter)
        if (store.find (*iter))
            mUsedIds.insert (*iter);

    mTimeToStartDrowning = state.mTimeToStartDrowning;
}