#include "aiwander.hpp"

#include <OgreVector3.h>
#include <OgreSceneNode.h>

#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/dialoguemanager.hpp"

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

#include "creaturestats.hpp"
#include "steering.hpp"
#include "movement.hpp"

namespace MWMechanics
{
    static const int COUNT_BEFORE_RESET = 200; // TODO: maybe no longer needed
    static const float DOOR_CHECK_INTERVAL = 1.5f;

    AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<int>& idle, bool repeat):
        mDistance(distance), mDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat)
      , mCellX(std::numeric_limits<int>::max())
      , mCellY(std::numeric_limits<int>::max())
      , mXCell(0)
      , mYCell(0)
      , mCell(NULL)
      , mStuckCount(0) // TODO: maybe no longer needed
      , mDoorCheckDuration(0)
      , mTrimCurrentNode(false)
      , mReaction(0)
      , mGreetDistanceMultiplier(0)
      , mGreetDistanceReset(0)
      , mChance(0)
      , mRotate(false)
      , mTargetAngle(0)
      , mSaidGreeting(false)
      , mHasReturnPosition(false)
      , mReturnPosition(0,0,0)
    {
        for(unsigned short counter = 0; counter < mIdle.size(); counter++)
        {
            if(mIdle[counter] >= 127 || mIdle[counter] < 0)
                mIdle[counter] = 0;
        }

        if(mDistance < 0)
            mDistance = 0;
        if(mDuration < 0)
            mDuration = 0;
        if(mDuration == 0)
            mTimeOfDay = 0;

        mStartTime = MWBase::Environment::get().getWorld()->getTimeStamp();
        mPlayedIdle = 0;
        const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
        mIdleChanceMultiplier =
            store.get<ESM::GameSetting>().find("fIdleChanceMultiplier")->getFloat();

        mGreetDistanceMultiplier =
            store.get<ESM::GameSetting>().find("iGreetDistanceMultiplier")->getInt();
        mGreetDistanceReset =
            store.get<ESM::GameSetting>().find("fGreetDistanceReset")->getFloat();
        mChance = store.get<ESM::GameSetting>().find("fVoiceIdleOdds")->getFloat();

        mStoredAvailableNodes = false;
        mChooseAction = true;
        mIdleNow = false;
        mMoveNow = false;
        mWalking = false;
    }

    AiPackage * MWMechanics::AiWander::clone() const
    {
        return new AiWander(*this);
    }

    /*
     * AiWander high level states (0.29.0). Not entirely accurate in some cases
     * e.g. non-NPC actors do not greet and some creatures may be moving even in
     * the IdleNow state.
     *
     *                          [select node,
     *                           build path]
     *                 +---------->MoveNow----------->Walking
     *                 |                                 |
     * [allowed        |                                 |
     *  nodes]         |        [hello if near]          |
     *  start--->ChooseAction----->IdleNow               |
     *                ^ ^           |                    |
     *                | |           |                    |
     *                | +-----------+                    |
     *                |                                  |
     *                +----------------------------------+
     *
     *
     * New high level states.  Not exactly as per vanilla (e.g. door stuff)
     * but the differences are required because our physics does not work like
     * vanilla and therefore have to compensate/work around.
     *
     *                         [select node,     [if stuck evade
     *                          build path]       or remove nodes if near door]
     *                 +---------->MoveNow<---------->Walking
     *                 |              ^                | |
     *                 |              |(near door)     | |
     * [allowed        |              |                | |
     *  nodes]         |        [hello if near]        | |
     *  start--->ChooseAction----->IdleNow             | |
     *                ^ ^           |  ^               | |
     *                | |           |  | (stuck near   | |
     *                | +-----------+  +---------------+ |
     *                |                    player)       |
     *                +----------------------------------+
     *
     * NOTE: non-time critical operations are run once every 250ms or so.
     *
     * TODO: It would be great if door opening/closing can be detected and pathgrid
     * links dynamically updated.  Currently (0.29.0) AiWander allows choosing a
     * destination beyond closed doors which sometimes makes the actors stuck at the
     * door and impossible for the player to open the door.
     *
     * For now detect being stuck at the door and simply delete the nodes from the
     * allowed set.  The issue is when the door opens the allowed set is not
     * re-calculated.  However this would not be an issue in most cases since hostile
     * actors will enter combat (i.e. no longer wandering) and different pathfinding
     * will kick in.
     */
    bool AiWander::execute (const MWWorld::Ptr& actor,float duration)
    {
        MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor);
        if(cStats.isDead() || cStats.getHealth().getCurrent() <= 0)
            return true; // Don't bother with dead actors

        bool cellChange = mCell && (actor.getCell() != mCell);
        if(!mCell || cellChange)
        {
            mCell = actor.getCell();
            mStoredAvailableNodes = false; // prob. not needed since mDistance = 0
        }
        const ESM::Cell *cell = mCell->getCell();

        cStats.setDrawState(DrawState_Nothing);
        cStats.setMovementFlag(CreatureStats::Flag_Run, false);

        ESM::Position pos = actor.getRefData().getPosition();

        // Check if an idle actor is  too close to a door - if so start walking
        mDoorCheckDuration += duration;
        if(mDoorCheckDuration >= DOOR_CHECK_INTERVAL)
        {
            mDoorCheckDuration = 0;    // restart timer
            if(mDistance &&            // actor is not intended to be stationary
               mIdleNow &&             // but is in idle
               !mWalking &&            // FIXME: some actors are idle while walking
               proximityToDoor(actor, MIN_DIST_TO_DOOR_SQUARED*1.6*1.6)) // NOTE: checks interior cells only
            {
                mIdleNow = false;
                mMoveNow = true;
                mTrimCurrentNode = false; // just in case
            }
        }

        if(mWalking) // have not yet reached the destination
        {
            // turn towards the next point in mPath
            zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1])));
            actor.getClass().getMovementSettings(actor).mPosition[1] = 1;

            // Returns true if evasive action needs to be taken
            if(mObstacleCheck.check(actor, duration))
            {
                // first check if we're walking into a door
                if(proximityToDoor(actor)) // NOTE: checks interior cells only
                {
                    // remove allowed points then select another random destination
                    mTrimCurrentNode = true;
                    trimAllowedNodes(mAllowedNodes, mPathFinder);
                    mObstacleCheck.clear();
                    mPathFinder.clearPath();
                    mWalking = false;
                    mMoveNow = true;
                }
                else // probably walking into another NPC
                {
                    // TODO: diagonal should have same animation as walk forward
                    //       but doesn't seem to do that?
                    actor.getClass().getMovementSettings(actor).mPosition[0] = 1;
                    actor.getClass().getMovementSettings(actor).mPosition[1] = 0.1f;
                    // change the angle a bit, too
                    zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1])));
                }
                mStuckCount++;  // TODO: maybe no longer needed
            }
//#if 0
            // TODO: maybe no longer needed
            if(mStuckCount >= COUNT_BEFORE_RESET) // something has gone wrong, reset
            {
                //std::cout << "Reset \""<< cls.getName(actor) << "\"" << std::endl;
                mObstacleCheck.clear();

                stopWalking(actor);
                mMoveNow = false;
                mWalking = false;
                mChooseAction = true;
            }
//#endif
        }

        if (mRotate)
        {
            // Reduce the turning animation glitch by using a *HUGE* value of
            // epsilon...  TODO: a proper fix might be in either the physics or the
            // animation subsystem
            if (zTurn(actor, Ogre::Degree(mTargetAngle), Ogre::Degree(5)))
                mRotate = false;
        }

        mReaction += duration;
        if(mReaction > 0.25f) // FIXME: hard coded constant
        {
            mReaction = 0;
            return false;
        }

        // NOTE: everything below get updated every 0.25 seconds

        MWBase::World *world = MWBase::Environment::get().getWorld();
        if(mDuration)
        {
            // End package if duration is complete or mid-night hits:
            MWWorld::TimeStamp currentTime = world->getTimeStamp();
            if(currentTime.getHour() >= mStartTime.getHour() + mDuration)
            {
                if(!mRepeat)
                {
                    stopWalking(actor);
                    return true;
                }
                else
                    mStartTime = currentTime;
            }
            else if(int(currentTime.getHour()) == 0 && currentTime.getDay() != mStartTime.getDay())
            {
                if(!mRepeat)
                {
                    stopWalking(actor);
                    return true;
                }
                else
                    mStartTime = currentTime;
            }
        }

        // Initialization to discover & store allowed node points for this actor.
        if(!mStoredAvailableNodes)
        {
            // infrequently used, therefore no benefit in caching it as a member
            const ESM::Pathgrid *
                pathgrid = world->getStore().get<ESM::Pathgrid>().search(*cell);

            // cache the current cell location
            mCellX = cell->mData.mX;
            mCellY = cell->mData.mY;

            // If there is no path this actor doesn't go anywhere. See:
            // https://forum.openmw.org/viewtopic.php?t=1556
            // http://www.fliggerty.com/phpBB3/viewtopic.php?f=30&t=5833
            if(!pathgrid || pathgrid->mPoints.empty())
                mDistance = 0;

            // A distance value passed into the constructor indicates how far the
            // actor can  wander from the spawn position.  AiWander assumes that
            // pathgrid points are available, and uses them to randomly select wander
            // destinations within the allowed set of pathgrid points (nodes).
            if(mDistance)
            {
                mXCell = 0;
                mYCell = 0;
                if(cell->isExterior())
                {
                    mXCell = mCellX * ESM::Land::REAL_SIZE;
                    mYCell = mCellY * ESM::Land::REAL_SIZE;
                }

                // FIXME: There might be a bug here.  The allowed node points are
                // based on the actor's current position rather than the actor's
                // spawn point.  As a result the allowed nodes for wander can change
                // between saves, for example.
                //
                // convert npcPos to local (i.e. cell) co-ordinates
                Ogre::Vector3 npcPos(pos.pos);
                npcPos[0] = npcPos[0] - mXCell;
                npcPos[1] = npcPos[1] - mYCell;

                // mAllowedNodes for this actor with pathgrid point indexes based on mDistance
                // NOTE: mPoints and mAllowedNodes are in local co-ordinates
                for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++)
                {
                    Ogre::Vector3 nodePos(pathgrid->mPoints[counter].mX, pathgrid->mPoints[counter].mY,
                        pathgrid->mPoints[counter].mZ);
                    if(npcPos.squaredDistance(nodePos) <= mDistance * mDistance)
                        mAllowedNodes.push_back(pathgrid->mPoints[counter]);
                }
                if(!mAllowedNodes.empty())
                {
                    Ogre::Vector3 firstNodePos(mAllowedNodes[0].mX, mAllowedNodes[0].mY, mAllowedNodes[0].mZ);
                    float closestNode = npcPos.squaredDistance(firstNodePos);
                    unsigned int index = 0;
                    for(unsigned int counterThree = 1; counterThree < mAllowedNodes.size(); counterThree++)
                    {
                        Ogre::Vector3 nodePos(mAllowedNodes[counterThree].mX, mAllowedNodes[counterThree].mY,
                            mAllowedNodes[counterThree].mZ);
                        float tempDist = npcPos.squaredDistance(nodePos);
                        if(tempDist < closestNode)
                            index = counterThree;
                    }
                    mCurrentNode = mAllowedNodes[index];
                    mAllowedNodes.erase(mAllowedNodes.begin() + index);

                    mStoredAvailableNodes = true; // set only if successful in finding allowed nodes
                }
            }
        }

        // Actor becomes stationary - see above URL's for previous research
        if(mAllowedNodes.empty())
            mDistance = 0;

        // Don't try to move if you are in a new cell (ie: positioncell command called) but still play idles.
        if(mDistance && cellChange)
            mDistance = 0;

        // For stationary NPCs, move back to the starting location if another AiPackage moved us elsewhere
        if (cellChange)
            mHasReturnPosition = false;
        if (mDistance == 0 && mHasReturnPosition && Ogre::Vector3(pos.pos).squaredDistance(mReturnPosition) > 20*20)
        {
            mChooseAction = false;
            mIdleNow = false;

            if (!mPathFinder.isPathConstructed())
            {
                Ogre::Vector3 destNodePos = mReturnPosition;

                ESM::Pathgrid::Point dest;
                dest.mX = destNodePos[0];
                dest.mY = destNodePos[1];
                dest.mZ = destNodePos[2];

                // actor position is already in world co-ordinates
                ESM::Pathgrid::Point start;
                start.mX = pos.pos[0];
                start.mY = pos.pos[1];
                start.mZ = pos.pos[2];

                // don't take shortcuts for wandering
                mPathFinder.buildPath(start, dest, actor.getCell(), false);

                if(mPathFinder.isPathConstructed())
                {
                    mMoveNow = false;
                    mWalking = true;
                }
            }
        }

        if(mChooseAction)
        {
            mPlayedIdle = 0;
            getRandomIdle(); // NOTE: sets mPlayedIdle with a random selection

            if(!mPlayedIdle && mDistance)
            {
                mChooseAction = false;
                mMoveNow = true;
            }
            else
            {
                // Play idle animation and recreate vanilla (broken?) behavior of resetting start time of AIWander:
                MWWorld::TimeStamp currentTime = world->getTimeStamp();
                mStartTime = currentTime;
                playIdle(actor, mPlayedIdle);
                mChooseAction = false;
                mIdleNow = true;

                // Play idle voiced dialogue entries randomly
                int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified();
                if (hello > 0)
                {
                    int roll = std::rand()/ (static_cast<double> (RAND_MAX) + 1) * 100; // [0, 99]
                    MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr();

                    // Don't bother if the player is out of hearing range
                    if (roll < mChance && Ogre::Vector3(player.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(pos.pos)) < 1500*1500)
                        MWBase::Environment::get().getDialogueManager()->say(actor, "idle");
                }
            }
        }

        // Allow interrupting a walking actor to trigger a greeting
        if(mIdleNow || (mWalking && !mObstacleCheck.isNormalState() && mDistance))
        {
            // Play a random voice greeting if the player gets too close
            int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified();
            float helloDistance = hello;
            helloDistance *= mGreetDistanceMultiplier;

            MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr();
            Ogre::Vector3 playerPos(player.getRefData().getPosition().pos);
            Ogre::Vector3 actorPos(actor.getRefData().getPosition().pos);
            float playerDistSqr = playerPos.squaredDistance(actorPos);

            if(playerDistSqr <= helloDistance*helloDistance)
            {
                if(mWalking)
                {
                    stopWalking(actor);
                    mMoveNow = false;
                    mWalking = false;
                    mObstacleCheck.clear();
                    mIdleNow = true;
                    getRandomIdle();
                }

                if(!mRotate)
                {
                    Ogre::Vector3 dir = playerPos - actorPos;
                    float length = dir.length();

                    float faceAngle = Ogre::Radian(Ogre::Math::ACos(dir.y / length) *
                            ((Ogre::Math::ASin(dir.x / length).valueRadians()>0)?1.0:-1.0)).valueDegrees();
                    float actorAngle = actor.getRefData().getBaseNode()->getOrientation().getRoll().valueDegrees();
                    // an attempt at reducing the turning animation glitch
                    if(abs(abs(faceAngle) - abs(actorAngle)) >= 5) // TODO: is there a better way?
                    {
                        mTargetAngle = faceAngle;
                        mRotate = true;
                    }
                }
            }

            if (!mSaidGreeting)
            {
                // TODO: check if actor is aware / has line of sight
                if (playerDistSqr <= helloDistance*helloDistance
                        // Only play a greeting if the player is not moving
                        && Ogre::Vector3(player.getClass().getMovementSettings(player).mPosition).squaredLength() == 0)
                {
                    mSaidGreeting = true;
                    MWBase::Environment::get().getDialogueManager()->say(actor, "hello");
                }
            }
            else
            {
                if (playerDistSqr >= mGreetDistanceReset*mGreetDistanceReset * mGreetDistanceMultiplier*mGreetDistanceMultiplier)
                    mSaidGreeting = false;
            }

            // Check if idle animation finished
            // FIXME: don't stay forever
            if(!checkIdle(actor, mPlayedIdle) && playerDistSqr > helloDistance*helloDistance)
            {
                mPlayedIdle = 0;
                mIdleNow = false;
                mChooseAction = true;
            }
        }

        if(mMoveNow && mDistance)
        {
            // Construct a new path if there isn't one
            if(!mPathFinder.isPathConstructed())
            {
                assert(mAllowedNodes.size());
                unsigned int randNode = (int)(rand() / ((double)RAND_MAX + 1) * mAllowedNodes.size());
                // NOTE: initially constructed with local (i.e. cell) co-ordinates
                Ogre::Vector3 destNodePos(mAllowedNodes[randNode].mX,
                                          mAllowedNodes[randNode].mY,
                                          mAllowedNodes[randNode].mZ);

                // convert dest to use world co-ordinates
                ESM::Pathgrid::Point dest;
                dest.mX = destNodePos[0] + mXCell;
                dest.mY = destNodePos[1] + mYCell;
                dest.mZ = destNodePos[2];

                // actor position is already in world co-ordinates
                ESM::Pathgrid::Point start;
                start.mX = pos.pos[0];
                start.mY = pos.pos[1];
                start.mZ = pos.pos[2];

                // don't take shortcuts for wandering
                mPathFinder.buildPath(start, dest, actor.getCell(), false);

                if(mPathFinder.isPathConstructed())
                {
                    // buildPath inserts dest in case it is not a pathgraph point
                    // index which is a duplicate for AiWander.  However below code
                    // does not work since getPath() returns a copy of path not a
                    // reference
                    //if(mPathFinder.getPathSize() > 1)
                        //mPathFinder.getPath().pop_back();

                    // Remove this node as an option and add back the previously used node (stops NPC from picking the same node):
                    ESM::Pathgrid::Point temp = mAllowedNodes[randNode];
                    mAllowedNodes.erase(mAllowedNodes.begin() + randNode);
                    // check if mCurrentNode was taken out of mAllowedNodes
                    if(mTrimCurrentNode && mAllowedNodes.size() > 1)
                        mTrimCurrentNode = false;
                    else
                        mAllowedNodes.push_back(mCurrentNode);
                    mCurrentNode = temp;

                    mMoveNow = false;
                    mWalking = true;
                }
                // Choose a different node and delete this one from possible nodes because it is uncreachable:
                else
                    mAllowedNodes.erase(mAllowedNodes.begin() + randNode);
            }
        }

        // Are we there yet?
        if(mWalking &&
           mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2]))
        {
            stopWalking(actor);
            mMoveNow = false;
            mWalking = false;
            mChooseAction = true;
            mHasReturnPosition = false;
        }

        return false; // AiWander package not yet completed
    }

    void AiWander::trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes,
                                    const PathFinder& pathfinder)
    {
        // TODO: how to add these back in once the door opens?
        // Idea: keep a list of detected closed doors (see aicombat.cpp)
        // Every now and then check whether one of the doors is opened. (maybe
        // at the end of playing idle?) If the door is opened then re-calculate
        // allowed nodes starting from the spawn point.
        std::list<ESM::Pathgrid::Point> paths = pathfinder.getPath();
        while(paths.size() >= 2)
        {
            ESM::Pathgrid::Point pt = paths.back();
            for(unsigned int j = 0; j < nodes.size(); j++)
            {
                // FIXME: doesn't hadle a door with the same X/Y
                //        co-ordinates but with a different Z
                if(nodes[j].mX == pt.mX && nodes[j].mY == pt.mY)
                {
                    nodes.erase(nodes.begin() + j);
                    break;
                }
            }
            paths.pop_back();
        }
    }

    int AiWander::getTypeId() const
    {
        return TypeIdWander;
    }

    void AiWander::stopWalking(const MWWorld::Ptr& actor)
    {
        mPathFinder.clearPath();
        MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 0;
    }

    void AiWander::playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect)
    {
        if(idleSelect == 2)
            MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle2", 0, 1);
        else if(idleSelect == 3)
            MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle3", 0, 1);
        else if(idleSelect == 4)
            MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle4", 0, 1);
        else if(idleSelect == 5)
            MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle5", 0, 1);
        else if(idleSelect == 6)
            MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle6", 0, 1);
        else if(idleSelect == 7)
            MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle7", 0, 1);
        else if(idleSelect == 8)
            MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle8", 0, 1);
        else if(idleSelect == 9)
            MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle9", 0, 1);
    }

    bool AiWander::checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect)
    {
        if(idleSelect == 2)
            return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle2");
        else if(idleSelect == 3)
            return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle3");
        else if(idleSelect == 4)
            return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle4");
        else if(idleSelect == 5)
            return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle5");
        else if(idleSelect == 6)
            return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle6");
        else if(idleSelect == 7)
            return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle7");
        else if(idleSelect == 8)
            return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle8");
        else if(idleSelect == 9)
            return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle9");
        else
            return false;
    }

    void AiWander::setReturnPosition(const Ogre::Vector3& position)
    {
        if (!mHasReturnPosition)
        {
            mHasReturnPosition = true;
            mReturnPosition = position;
        }
    }

    void AiWander::getRandomIdle()
    {
        unsigned short idleRoll = 0;

        for(unsigned int counter = 0; counter < mIdle.size(); counter++)
        {
            unsigned short idleChance = mIdleChanceMultiplier * mIdle[counter];
            unsigned short randSelect = (int)(rand() / ((double)RAND_MAX + 1) * int(100 / mIdleChanceMultiplier));
            if(randSelect < idleChance && randSelect > idleRoll)
            {
                mPlayedIdle = counter+2;
                idleRoll = randSelect;
            }
        }
    }
}