
#include "npcstats.hpp"

#include <cmath>
#include <stdexcept>
#include <vector>
#include <algorithm>

#include <boost/format.hpp>

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

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

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

MWMechanics::NpcStats::NpcStats()
: mMovementFlags (0)
, mDrawState (DrawState_Nothing)
, mBounty (0)
, mLevelProgress(0)
, mDisposition(0)
, mVampire (0)
, mReputation(0)
, mWerewolf (false)
, mWerewolfKills (0)
, mProfit(0)
, mAttackStrength(0.0f)
{
    mSkillIncreases.resize (ESM::Attribute::Length);
    for (int i=0; i<ESM::Attribute::Length; ++i)
        mSkillIncreases[i] = 0;
}

MWMechanics::DrawState_ MWMechanics::NpcStats::getDrawState() const
{
    return mDrawState;
}

void MWMechanics::NpcStats::setDrawState (DrawState_ state)
{
    mDrawState = state;
}

float MWMechanics::NpcStats::getAttackStrength() const
{
    return mAttackStrength;
}

void MWMechanics::NpcStats::setAttackStrength(float value)
{
    mAttackStrength = value;
}

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

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

bool MWMechanics::NpcStats::getMovementFlag (Flag flag) const
{
    return mMovementFlags & flag;
}

void MWMechanics::NpcStats::setMovementFlag (Flag flag, bool state)
{
    if (state)
        mMovementFlags |= flag;
    else
        mMovementFlags &= ~flag;
}

const MWMechanics::Stat<float>& MWMechanics::NpcStats::getSkill (int index) const
{
    if (index<0 || index>=27)
        throw std::runtime_error ("skill index out of range");

    return mSkill[index];
}

MWMechanics::Stat<float>& MWMechanics::NpcStats::getSkill (int index)
{
    if (index<0 || index>=27)
        throw std::runtime_error ("skill index out of range");

    return mSkill[index];
}

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

std::set<std::string>& MWMechanics::NpcStats::getExpelled()
{
    return mExpelled;
}

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

bool MWMechanics::NpcStats::isSameFaction (const NpcStats& npcStats) const
{
    for (std::map<std::string, int>::const_iterator iter (mFactionRank.begin()); iter!=mFactionRank.end();
        ++iter)
        if (npcStats.mFactionRank.find (iter->first)!=npcStats.mFactionRank.end())
            return true;

    return false;
}

float MWMechanics::NpcStats::getSkillGain (int skillIndex, const ESM::Class& class_, int usageType,
    int level) const
{
    if (level<0)
        level = static_cast<int> (getSkill (skillIndex).getBase());

    const ESM::Skill *skill =
        MWBase::Environment::get().getWorld()->getStore().get<ESM::Skill>().find (skillIndex);

    float skillFactor = 1;

    if (usageType>=4)
        throw std::runtime_error ("skill usage type out of range");

    if (usageType>=0)
    {
        skillFactor = skill->mData.mUseValue[usageType];

        if (skillFactor<0)
            throw std::runtime_error ("invalid skill gain factor");

        if (skillFactor==0)
            return 0;
    }

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

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

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

            break;
        }

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

            break;
        }

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

    float specialisationFactor = 1;

    if (skill->mData.mSpecialization==class_.mData.mSpecialization)
    {
        specialisationFactor = gmst.find ("fSpecialSkillBonus")->getFloat();

        if (specialisationFactor<=0)
            throw std::runtime_error ("invalid skill specialisation factor");
    }
    return 1.0 / ((level+1) * (1.0/skillFactor) * typeFactor * specialisationFactor);
}

void MWMechanics::NpcStats::useSkill (int skillIndex, const ESM::Class& class_, int usageType)
{
    float base = getSkill (skillIndex).getBase();

    int level = static_cast<int> (base);

    base += getSkillGain (skillIndex, class_, usageType);

    if (static_cast<int> (base)!=level)
    {
        // skill leveled up
        increaseSkill(skillIndex, class_, false);
    }
    else
        getSkill (skillIndex).setBase (base);
}

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

    int level = static_cast<int> (base);

    if (level >= 100)
        return;

    if (preserveProgress)
        base += 1;
    else
        base = level+1;

    // if this is a major or minor skill of the class, increase level progress
    bool levelProgress = false;
    for (int i=0; i<2; ++i)
        for (int j=0; j<5; ++j)
        {
            int skill = class_.mData.mSkills[j][i];
            if (skill == skillIndex)
                levelProgress = true;
        }

    mLevelProgress += levelProgress;

    // check the attribute this skill belongs to
    const ESM::Skill* skill =
        MWBase::Environment::get().getWorld ()->getStore ().get<ESM::Skill>().find(skillIndex);
    ++mSkillIncreases[skill->mData.mAttribute];

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

    std::stringstream message;
    message << boost::format(MWBase::Environment::get().getWindowManager ()->getGameSettingString ("sNotifyMessage39", ""))
               % std::string("#{" + ESM::Skill::sSkillNameIds[skillIndex] + "}")
               % static_cast<int> (base);
    MWBase::Environment::get().getWindowManager ()->messageBox(message.str());

    if (mLevelProgress >= 10)
    {
        // levelup is possible now
        MWBase::Environment::get().getWindowManager ()->messageBox ("#{sLevelUpMsg}");
    }

    getSkill (skillIndex).setBase (base);
}

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

void MWMechanics::NpcStats::levelUp()
{
    mLevelProgress -= 10;
    for (int i=0; i<ESM::Attribute::Length; ++i)
        mSkillIncreases[i] = 0;
}

int MWMechanics::NpcStats::getLevelupAttributeMultiplier(int attribute) const
{
    // Source: http://www.uesp.net/wiki/Morrowind:Level#How_to_Level_Up
    int num = mSkillIncreases[attribute];
    if (num <= 1)
        return 1;
    else if (num <= 4)
        return 2;
    else if (num <= 7)
        return 3;
    else if (num <= 9)
        return 4;
    else
        return 5;
}

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::getFactionReputation (const std::string& faction) const
{
    std::map<std::string, int>::const_iterator iter = mFactionReputation.find (faction);

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

    return iter->second;
}

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

bool MWMechanics::NpcStats::isVampire() const
{
    return mVampire;
}

void MWMechanics::NpcStats::setVampire (bool set)
{
    mVampire = set;
}

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

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

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<6; ++i)
        skills.push_back (static_cast<int> (getSkill (faction.mData.mSkills[i]).getModified()));

    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;

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

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

void MWMechanics::NpcStats::setWerewolf (bool set)
{
    mWerewolf = set;
}

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

int MWMechanics::NpcStats::getProfit() const
{
    return mProfit;
}

void MWMechanics::NpcStats::modifyProfit(int diff)
{
    mProfit += diff;
}
