/**

GDevelop - Pathfinding Behavior Extension
Copyright (c) 2010-2016 Florian Rival (Florian.Rival@gmail.com)
This project is released under the MIT License.
*/

#include <memory>
#include <unordered_map>
#include <iostream>
#include <set>
#include "PathfindingBehavior.h"
#include "PathfindingObstacleBehavior.h"
#include "ScenePathfindingObstaclesManager.h"
#include "GDCore/Tools/Localization.h"
#include "GDCpp/Extensions/Builtin/MathematicalTools.h"
#include "GDCpp/Runtime/Project/Layout.h"
#include "GDCpp/Runtime/Serialization/SerializerElement.h"
#include "GDCpp/Runtime/RuntimeScene.h"
#include "GDCpp/Runtime/RuntimeObject.h"
#include "GDCpp/Runtime/CommonTools.h"
#include <iostream>
#include <cmath>
#include <algorithm>
#if defined(GD_IDE_ONLY)
#include <map>
#include "GDCore/IDE/Dialogs/PropertyDescriptor.h"
#endif


/**
 * \brief Internal tool class representing the position of a node when looking for a path.
 */
class NodePosition
{
public:
    NodePosition(int x_, int y_) : x(x_), y(y_) {};

    int x;
    int y;
};

std::ostream& operator<<(std::ostream& stream, const NodePosition & nodePos)
{
    stream << nodePos.x << ";" << nodePos.y;
    return stream;
}

bool operator==(const NodePosition &a, const NodePosition &b)
{
    return ((a.x == b.x) && (a.y == b.y));
}

namespace std
{
/**
 * \brief Tool function used to store a NodePosition as key in std::unordered_set.
 */
template<>
struct hash<NodePosition>
{
    std::size_t operator()(NodePosition const & n) const
    {
        return (std::hash<int>()(n.x)) ^ (std::hash<int>()(n.y) << 1);
    }
};

}

namespace
{
/**
 * \brief Internal tool class representing a node when looking for a path
 */
class Node
{
public:
    Node() : pos(0, 0), cost(0), smallestCost(-1), estimateCost(-1), parent(NULL), open(true) {};
    Node(int x, int y) : pos(x, y), cost(0), smallestCost(-1), estimateCost(-1), parent(NULL), open(true) {};
    Node(const NodePosition & pos_) : pos(pos_), cost(0), smallestCost(-1), estimateCost(-1), parent(NULL), open(true) {};

    NodePosition pos;
    float cost; ///< The cost for traveling on this node
    float smallestCost; ///< the cost to go to this node (when considering the shortest path).
    float estimateCost; ///< the estimate cost total to go to the destination through this node (when considering the shortest path).
    const Node * parent; ///< The previous node to be visited to go to this node (when considering the shortest path).
    bool open; ///< true if the node is "open" (must be explored), false if "close" (already explored)

    /**
     * \brief Tool function used to store a Node in a priority_queue.
     */
    class NodeComparator
    {
    public:
        bool operator()(const Node * n1, const Node * n2)
        {
            return n1->estimateCost < n2->estimateCost;
        }
    };
};

bool operator==(Node const& n1, Node const& n2)
{
    return n1.pos.x == n2.pos.x && n1.pos.y == n2.pos.y;
};

typedef float (*DistanceFunPtr)(const NodePosition & , const NodePosition & );

/**
 * \brief Internal tool class containing the structures used by A* and members functions related
 * to them.
 */
class SearchContext
{
public:
    SearchContext(ScenePathfindingObstaclesManager & obstacles_, bool allowsDiagonal_ = true) :
        obstacles(obstacles_),
        finalNode(NULL),
        destination(0, 0),
        startX(0),
        startY(0),
        allowsDiagonal(allowsDiagonal_),
        maxComplexityFactor(50),
        cellWidth(20),
        cellHeight(20),
        leftBorder(0),
        rightBorder(0),
        topBorder(0),
        bottomBorder(0)
    {
        distanceFunction = allowsDiagonal ? &SearchContext::EuclideanDistance : &SearchContext::ManhattanDistance;
    }

    /**
     * \brief Set the start position.
     * \param x The coordinate on X axis of the start position, in "world" coordinates.
     * \param y The coordinate on Y axis of the start position, in "world" coordinates.
     */
    SearchContext & SetStartPosition(float x, float y)
    {
        startX = x;
        startY = y;
        return *this;
    }

    /**
     * \brief Set the size to be considered for the object for which the path will be planned.
     */
    SearchContext & SetObjectSize(float leftBorder_, float topBorder_, float rightBorder_, float bottomBorder_)
    {
        leftBorder = leftBorder_;
        rightBorder = rightBorder_;
        topBorder = topBorder_;
        bottomBorder = bottomBorder_;
        return *this;
    }

    /**
     * \brief Change the size of a virtual cell, in pixels.
     */
    SearchContext & SetCellSize(unsigned int cellWidth_, unsigned int cellHeight_)
    {
        cellWidth = cellWidth_;
        cellHeight = cellHeight_;
        return *this;
    }

    /**
     * \brief Compute a path to the specified position, considering the obstacles
     * and the start position passed in the constructor.
     * \return true if computation found a path, in which case you can call GetFinalNode method
     * to construct the path.
     * \param x The coordinate on X axis of the target position, in "world" coordinates.
     * \param y The coordinate on Y axis of the target position, in "world" coordinates.
     */
    bool ComputePathTo(float targetX, float targetY)
    {
        destination = NodePosition(GDRound(targetX/cellWidth), GDRound(targetY/cellHeight));
        NodePosition start(GDRound(startX/cellWidth), GDRound(startY/cellHeight));

        //Initialize the algorithm
        allNodes.clear();
        Node & startNode = GetNode(start);
        startNode.smallestCost = 0;
        startNode.estimateCost = 0 + distanceFunction(start, destination);
        openNodes.clear();
        openNodes.insert(&startNode);

        //A* algorithm main loop
        std::size_t iterationCount = 0;
        std::size_t maxIterationCount = startNode.estimateCost*maxComplexityFactor;
        while (!openNodes.empty())
        {
            if (iterationCount++ > maxIterationCount) return false; //Make sure we do not search forever.

            Node * n = *openNodes.begin(); //Get the most promising node...
            n->open = false;                 //...and flag it as explored
            openNodes.erase(openNodes.begin()); //Be sure to remove ONLY the first element!

            //Check if we reached destination?
            if ( n->pos.x == destination.x && n->pos.y == destination.y )
            {
                finalNode = n;
                return true;
            }

            //No, so add neighbors to the nodes to explore.
            InsertNeighbors(*n);
        }

        return false;
    }

    /**
     * @return The final node of the computed path.
     * Iterate on the parent member to create the path. Beware, the coordinates of the node
     * must be multiplied by the cell size to get the "world" coordinates of the path.
     */
    Node * GetFinalNode() const
    {
        return finalNode;
    }

private:
    /**
     * Insert the neighbors of the current node in the open list
     * (Only if they are not closed, and if the cost is better than the already existing smallest cost).
     */
    void InsertNeighbors(const Node & currentNode)
    {
        AddOrUpdateNode(NodePosition(currentNode.pos.x+1, currentNode.pos.y), currentNode, 1);
        AddOrUpdateNode(NodePosition(currentNode.pos.x-1, currentNode.pos.y), currentNode, 1);
        AddOrUpdateNode(NodePosition(currentNode.pos.x, currentNode.pos.y+1), currentNode, 1);
        AddOrUpdateNode(NodePosition(currentNode.pos.x, currentNode.pos.y-1), currentNode, 1);
        if ( allowsDiagonal )
        {
            AddOrUpdateNode(NodePosition(currentNode.pos.x+1, currentNode.pos.y+1), currentNode, sqrt2);
            AddOrUpdateNode(NodePosition(currentNode.pos.x+1, currentNode.pos.y-1), currentNode, sqrt2);
            AddOrUpdateNode(NodePosition(currentNode.pos.x-1, currentNode.pos.y-1), currentNode, sqrt2);
            AddOrUpdateNode(NodePosition(currentNode.pos.x-1, currentNode.pos.y+1), currentNode, sqrt2);
        }
    }

    /**
     * \brief Get (or dynamically construct) a node.
     *
     * *All* nodes should be created using this method: The cost of the node is computed thanks
     * to the objects flagged as obstacles.
     */
    Node & GetNode(const NodePosition & pos)
    {
        if (allNodes.find(pos) != allNodes.end())
            return allNodes.find(pos)->second;

        Node newNode(pos);

        bool objectsOnCell = false;
        const std::set<PathfindingObstacleBehavior*> & allObstacles = obstacles.GetAllObstacles();
        for (std::set<PathfindingObstacleBehavior*>::const_iterator it = allObstacles.begin();
             it != allObstacles.end();
             ++it)
        {
            RuntimeObject * obj = (*it)->GetObject();
            int topLeftCellX = floor((obj->GetDrawableX()-rightBorder)/(float)cellWidth);
            int topLeftCellY = floor((obj->GetDrawableY()-bottomBorder)/(float)cellHeight);
            int bottomRightCellX = ceil((obj->GetDrawableX()+obj->GetWidth()+leftBorder)/(float)cellWidth);
            int bottomRightCellY = ceil((obj->GetDrawableY()+obj->GetHeight()+topBorder)/(float)cellHeight);
            if ( topLeftCellX < pos.x && pos.x < bottomRightCellX
                && topLeftCellY < pos.y && pos.y < bottomRightCellY)
            {
                objectsOnCell = true;
                if ( (*it)->IsImpassable() )
                {
                    newNode.cost = -1;
                    break; //The cell is impassable, stop here.
                }
                else //Superimpose obstacles
                    newNode.cost += (*it)->GetCost();
            }
        }

        if (!objectsOnCell) newNode.cost = 1; //Default cost when no objects put on the cell.

        allNodes[pos] = newNode;
        return allNodes[pos];
    }

    /**
     * Compute the euclidean distance between two positions.
     */
    static float EuclideanDistance(const NodePosition & a, const NodePosition & b)
    {
        return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
    }

    /**
     * Compute the taxi distance between two positions.
     */
    static float ManhattanDistance(const NodePosition & a, const NodePosition & b)
    {
        return abs(a.x-b.x)+abs(a.y-b.y);
    }

    /**
     * Add a node to the openNodes (only if the cost to reach it is less than the existing cost, if any).
     */
    void AddOrUpdateNode(const NodePosition & newNodePosition, const Node & currentNode, float factor)
    {
        Node & neighbor = GetNode(newNodePosition);
        if (!neighbor.open || neighbor.cost < 0 ) //cost < 0 means impassable obstacle
            return;

        //Update the node costs and parent if the path coming from currentNode is better:
        if (neighbor.smallestCost == -1
            || neighbor.smallestCost > currentNode.smallestCost + (currentNode.cost+neighbor.cost)/2.0*factor)
        {
            if (neighbor.smallestCost != -1) //The node is already in the open list:
            {
                //remove it as its estimate cost will be updated.
                auto it = openNodes.find(&neighbor);
                if (it != openNodes.end()) // /!\ ALWAYS use an iterator with multiset::erase
                    openNodes.erase(it);   //otherwise, other nodes which are equivalent get removed too.
            }

            neighbor.smallestCost = currentNode.smallestCost + (currentNode.cost+neighbor.cost)/2.0*factor;
            neighbor.parent = &currentNode;
            neighbor.estimateCost = neighbor.smallestCost + distanceFunction(neighbor.pos, destination);

            openNodes.insert(&neighbor);
        }
    }

    std::unordered_map< NodePosition, Node > allNodes; ///< All the nodes
    std::multiset<Node*, Node::NodeComparator> openNodes; ///< Only the open nodes (Such that Node::open == true)
    const ScenePathfindingObstaclesManager & obstacles; ///< A reference to all the obstacles of the scene
    Node * finalNode; //If computation succeeded, the final node is stored here.
    NodePosition destination;
    int startX; ///< The start X position, in "world" coordinates (not in "node" coordinates!).
    int startY; ///< The start Y position, in "world" coordinates (not in "node" coordinates!).
    DistanceFunPtr distanceFunction;
    bool allowsDiagonal; ///< True to allow diagonals when planning the path.
    std::size_t maxComplexityFactor;
    float cellWidth;
    float cellHeight;
    float leftBorder;
    float rightBorder;
    float topBorder;
    float bottomBorder;

    static const float sqrt2;
};

const float SearchContext::sqrt2 = 1.414213562;

}

PathfindingBehavior::PathfindingBehavior() :
    parentScene(NULL),
    sceneManager(NULL),
    pathFound(false),
    allowDiagonals(true),
    acceleration(400),
    maxSpeed(200),
    angularMaxSpeed(180),
    rotateObject(true),
    angleOffset(0),
    cellWidth(20),
    cellHeight(20),
    extraBorder(0),
    speed(0),
    angularSpeed(0),
    timeOnSegment(0),
    totalSegmentTime(0),
    currentSegment(0),
    reachedEnd(false)
{
}

void PathfindingBehavior::MoveTo(RuntimeScene & scene, float x, float y)
{
    if ( parentScene != &scene ) //Parent scene has changed
    {
        parentScene = &scene;
        sceneManager = parentScene ? &ScenePathfindingObstaclesManager::managers[&scene] : NULL;
    }

    path.clear();

    //First be sure that there is a path to compute.
    int targetCellX = GDRound(x/(float)cellWidth);
    int targetCellY = GDRound(y/(float)cellHeight);
    int startCellX = GDRound(object->GetX()/(float)cellWidth);
    int startCellY = GDRound(object->GetY()/(float)cellHeight);
    if ( startCellX == targetCellX && startCellY == targetCellY ) {
        path.push_back(sf::Vector2f(object->GetX(), object->GetY()));
        path.push_back(sf::Vector2f(x, y));
        EnterSegment(0);
        pathFound = true;
        return;
    }

    //Start searching for a path
    //TODO: Customizable heuristic.
    ::SearchContext ctx(*sceneManager, allowDiagonals);
    ctx.SetCellSize(cellWidth, cellHeight).SetStartPosition(object->GetX(), object->GetY());
    ctx.SetObjectSize(object->GetX()-object->GetDrawableX()+extraBorder,
        object->GetY()-object->GetDrawableY()+extraBorder,
        object->GetWidth()-(object->GetX()-object->GetDrawableX())+extraBorder,
        object->GetHeight()-(object->GetY()-object->GetDrawableY())+extraBorder);
    if (ctx.ComputePathTo(x, y))
    {
        //Path found: memorize it
        const ::Node * node = ctx.GetFinalNode();
        while (node) {
            path.push_back(sf::Vector2f(node->pos.x*(float)cellWidth, node->pos.y*(float)cellHeight));
            node = node->parent;
        }

        std::reverse(path.begin(), path.end());
        path[0] = sf::Vector2f(object->GetX(), object->GetY());
        EnterSegment(0);
        pathFound = true;
        return;
    }

    //Not path found
    pathFound = false;
}

void PathfindingBehavior::EnterSegment(std::size_t segmentNumber)
{
    if ( path.empty() ) return;

    currentSegment = segmentNumber;
    if (currentSegment < path.size()-1)
    {
        sf::Vector2f newPath = (path[currentSegment + 1] - path[currentSegment]);
        totalSegmentTime = sqrtf(newPath.x*newPath.x+newPath.y*newPath.y);
        timeOnSegment = 0;
        reachedEnd = false;
    }
    else {
        reachedEnd = true;
        speed = 0;
    }
}

void PathfindingBehavior::DoStepPreEvents(RuntimeScene & scene)
{
    if ( parentScene != &scene ) //Parent scene has changed
    {
        parentScene = &scene;
        sceneManager = parentScene ? &ScenePathfindingObstaclesManager::managers[&scene] : NULL;
    }

    if ( !sceneManager ) return;

    if (path.empty() || reachedEnd) return;

    //Update the speed of the object
    float timeDelta = static_cast<double>(scene.GetTimeManager().GetElapsedTime()) / 1000000.0;
    speed += acceleration*timeDelta;
    if ( speed > maxSpeed ) speed = maxSpeed;
    angularSpeed = angularMaxSpeed; //No acceleration for angular speed for now

    //Update the time on the segment and change segment if needed
    timeOnSegment += speed*timeDelta;
    if (timeOnSegment >= totalSegmentTime && currentSegment < path.size())
        EnterSegment(currentSegment + 1);

    //Position object on the segment and update its angle
    sf::Vector2f newPos;
    float pathAngle = object->GetAngle();
    if ( currentSegment < path.size()-1 ) {
        newPos = path[currentSegment] + (path[currentSegment + 1] - path[currentSegment]) * (timeOnSegment / totalSegmentTime);
        pathAngle = atan2(path[currentSegment+1].y - path[currentSegment].y,
            path[currentSegment+1].x - path[currentSegment].x)*180/3.14159+angleOffset;
    }
    else
        newPos = path.back();

    object->SetX(newPos.x);
    object->SetY(newPos.y);

    //Also update angle if needed
    if ( rotateObject )
        object->RotateTowardAngle(pathAngle, angularSpeed, scene);
}

void PathfindingBehavior::DoStepPostEvents(RuntimeScene & scene)
{
    if ( parentScene != &scene ) //Parent scene has changed
    {
        parentScene = &scene;
        sceneManager = parentScene ? &ScenePathfindingObstaclesManager::managers[&scene] : NULL;
    }
}

float PathfindingBehavior::GetNodeX(std::size_t index) const
{
    if (index<path.size()) return path[index].x;
    return 0;
}
float PathfindingBehavior::GetNodeY(std::size_t index) const
{
    if (index<path.size()) return path[index].y;
    return 0;
}
std::size_t PathfindingBehavior::GetNextNodeIndex() const
{
    if (currentSegment+1 < path.size())
        return currentSegment+1;
    else
        return path.size()-1;
}
float PathfindingBehavior::GetNextNodeX() const
{
    if ( path.empty() ) return 0;

    if (currentSegment+1 < path.size())
        return path[currentSegment+1].x;
    else
        return path.back().x;
}
float PathfindingBehavior::GetNextNodeY() const
{
    if ( path.empty() ) return 0;

    if (currentSegment+1 < path.size())
        return path[currentSegment+1].y;
    else
        return path.back().y;
}
float PathfindingBehavior::GetLastNodeX() const
{
    if ( path.size() < 2 ) return 0;

    if (currentSegment < path.size()-1)
        return path[currentSegment].x;
    else
        return path[path.size()-1].x;
}
float PathfindingBehavior::GetLastNodeY() const
{
    if ( path.size() < 2 ) return 0;

    if (currentSegment < path.size()-1)
        return path[currentSegment].y;
    else
        return path[path.size()-1].y;
}
float PathfindingBehavior::GetDestinationX() const
{
    if ( path.empty() ) return 0;
    return path.back().x;
}
float PathfindingBehavior::GetDestinationY() const
{
    if ( path.empty() ) return 0;
    return path.back().y;
}

void PathfindingBehavior::UnserializeFrom(const gd::SerializerElement & element)
{
    allowDiagonals = element.GetBoolAttribute("allowDiagonals");
    acceleration = element.GetDoubleAttribute("acceleration");
    maxSpeed = element.GetDoubleAttribute("maxSpeed");
    angularMaxSpeed = element.GetDoubleAttribute("angularMaxSpeed");
    rotateObject = element.GetBoolAttribute("rotateObject");
    angleOffset = element.GetDoubleAttribute("angleOffset");
    extraBorder = element.GetDoubleAttribute("extraBorder");
    {
        int value = element.GetIntAttribute("cellWidth", 0);
        if (value > 0) cellWidth = value;
    }
    {
        int value = element.GetIntAttribute("cellHeight", 0);
        if (value > 0) cellHeight = value;
    }
}

#if defined(GD_IDE_ONLY)
void PathfindingBehavior::SerializeTo(gd::SerializerElement & element) const
{
    element.SetAttribute("allowDiagonals", allowDiagonals);
    element.SetAttribute("acceleration", acceleration);
    element.SetAttribute("maxSpeed", maxSpeed);
    element.SetAttribute("angularMaxSpeed", angularMaxSpeed);
    element.SetAttribute("rotateObject", rotateObject);
    element.SetAttribute("angleOffset", angleOffset);
    element.SetAttribute("cellWidth", (int)cellWidth);
    element.SetAttribute("cellHeight", (int)cellHeight);
    element.SetAttribute("extraBorder", extraBorder);
}

std::map<gd::String, gd::PropertyDescriptor> PathfindingBehavior::GetProperties(gd::Project & project) const
{
    std::map<gd::String, gd::PropertyDescriptor> properties;

    properties[_("Allows diagonals")].SetValue(allowDiagonals ? "true" : "false").SetType("Boolean");
    properties[_("Acceleration")].SetValue(gd::String::From(acceleration));
    properties[_("Max. speed")].SetValue(gd::String::From(maxSpeed));
    properties[_("Rotate speed")].SetValue(gd::String::From(angularMaxSpeed));
    properties[_("Rotate object")].SetValue(rotateObject ? "true" : "false").SetType("Boolean");
    properties[_("Angle offset")].SetValue(gd::String::From(angleOffset));
    properties[_("Virtual cell width")].SetValue(gd::String::From(cellWidth));
    properties[_("Virtual cell height")].SetValue(gd::String::From(cellHeight));
    properties[_("Extra border size")].SetValue(gd::String::From(extraBorder));

    return properties;
}

bool PathfindingBehavior::UpdateProperty(const gd::String & name, const gd::String & value, gd::Project & project)
{
    if ( name == _("Allows diagonals") ) {
        allowDiagonals = (value != "0");
        return true;
    }
    if ( name == _("Rotate object") ) {
        rotateObject = (value != "0");
        return true;
    }
    if ( name == _("Extra border size") ) {
        extraBorder = value.To<float>();
        return true;
    }

    if ( value.To<float>() < 0 ) return false;

    if ( name == _("Acceleration") )
        acceleration = value.To<float>();
    else if ( name == _("Max. speed") )
        maxSpeed = value.To<float>();
    else if ( name == _("Rotate speed") )
        angularMaxSpeed = value.To<float>();
    else if ( name == _("Angle offset") )
        angleOffset = value.To<float>();
    else if ( name == _("Virtual cell width") )
        cellWidth = value.To<unsigned int>();
    else if ( name == _("Virtual cell height") )
        cellHeight = value.To<unsigned int>();
    else
        return false;

    return true;
}

#endif
