#pragma once
#include "../../../../Commands/MoveCommand.h"
#include "../../../../Pathfinding.h"
#include "../../../UnitBehavior.h"
#include "../../../Utils/TimerState.h"
#include <functional>
#include <boost/function.hpp>
#include <boost/shared_ptr.hpp>

static const int DEFAULT_UNIT_MAX_DISTANCE = TILE_SIZE * 2;
static const int DEFAULT_GROUP_MAX_DISTANCE = TILE_SIZE * 6;

/**
 * @brief The UnitMoveBehavior instructs a unit to move to some destination.
 */
class UnitMoveBehavior : public virtual UnitBehavior
{
public:
  static double DefaultEvaluator(BWAPI::TilePosition)
  {
    return 0; // No threat
  }

  static bool DefaultIsObstacle(BWAPI::TilePosition, BWAPI::TilePosition)
  {
    return false;
  }

  class MoveOptions
  {
  public:
    MoveOptions(const ThreatEvaluator &evaluator, const ObstacleEvaluator &isObstacle, lookahead_t lookahead, 
                int maxDistance, size_t refreshRate, size_t givingUpDelay, double moveWeight, bool threatAware, bool attackMove)
    : m_maxDistance(maxDistance)
    , m_refreshRate(refreshRate)
    , m_givingUpDelayIfNoPathFound(givingUpDelay)
    , m_threatAware(threatAware)
    , m_attackMove(attackMove)
    , m_threatEvaluator(evaluator)
    , m_isObstacle(isObstacle)
    , m_lookahead(lookahead)
    , m_moveWeight(moveWeight)
    {}

    /**
     * @brief The maximum distance before considering that the unit has reached its destination - note that if a group of units
     *        all get sent to the same destination, some of the units will end up far from the destination. Suggested: 4 * TILE_SIZE if the unit is in a group and TILE_SIZE for single units.
     */
    // TODO: this should be calculated automatically from the group's size and composition
    int m_maxDistance;
    /**
     * @brief The number of frames before successive revisions of the current path. Must be > 0.
     */
    size_t m_refreshRate;
    /**
     * @brief The number of frames before giving up if no plan is found (default is to never give up). Use 0 to give up immediately when no plan is found.
     *        Note that you can register on the OnNoPathFound event to integrate additional conditions.
     */
    size_t m_givingUpDelayIfNoPathFound;
    /**
     * @brief Whether or not to use a threat-aware behavior.
     */
    bool m_threatAware;
    /**
     * @brief Whether to use an underlying attack-move command instead of a move command.
     */
    bool m_attackMove;
    /**
     * @brief An evaluation function that estimates the threat of a specific tile.
     */
    ThreatEvaluator m_threatEvaluator;
    /**
     * @brief An evaluation function that determines whether a movement from a tile to a neighboring tile is possible.
     */
    ObstacleEvaluator m_isObstacle;
    /**
     * @brief TODO 
     */
    lookahead_t m_lookahead;
    /**
     * @brief Weight of move cost (horizontal/vertical:1 or diagonal:sqrt(2)) relative to threat cost (calculated with m_threatEvaluator)
     */
    double m_moveWeight;

  private:
    //Prevents using the assignment operator
    MoveOptions& operator=(const MoveOptions&);
  };

  static MoveOptions getDefaultMoveOptions(
    const ThreatEvaluator &evaluator = DefaultEvaluator, 
    const ObstacleEvaluator &isObstacle = DefaultIsObstacle, 
    lookahead_t lookahead = 0, 
    size_t maxDistance = DEFAULT_UNIT_MAX_DISTANCE, 
    size_t refreshRate = 10, 
    size_t givingUpDelay = boost::numeric::bounds<size_t>::highest(), 
    double moveWeight = 5.0, 
    bool threatAware = true, 
    bool attackMove = false)
  {
    return MoveOptions(evaluator, isObstacle, lookahead, maxDistance, refreshRate, givingUpDelay, moveWeight, threatAware, attackMove);
  }

  /**
   * @brief UnitMoveBehavior behavior constructor.
   * @param parent The parent of the new behavior.
   * @param initialPriority The initial priority of the new behavior.
   * @param unit The unit instructed to move.
   * @param destination The position (pixel) to go to.
   * @param moveOptions The options related to the motion.
   */
  UnitMoveBehavior(const Process* parent, unsigned int initialPriority, BWAPI::Unit* unit, 
                   BWAPI::Position destination, const MoveOptions& moveOptions);

  /**
   * @brief The type of event to raise when the unit is not able to reach its destination.
   *        The argument is the first frame at which a plan could not be found (which is the current frame if it is the
   *        first time the path planning failed).
   */
  DECLARE_EVENT1(OnNoPathFound, int);                           
  OnNoPathFound& getNoPathFoundEvent() const { return m_noPathFoundEvent; }

  /**
   * @brief The behavior failed to find a path in the alloted time frame.
   */
  static const TerminationMode NoPathFound;

protected:
  /**
   * @brief The position (pixel) to reach.
   */
  const BWAPI::Position m_destination;
  /**
   * @brief The tile position to reach (which contains the pixel).
   */
  const BWAPI::TilePosition m_destinationTile;
  /**
   * @brief The options related to the motion.
   */
  const MoveOptions m_moveOptions;
  /**
   * @brief The event to raise when the unit is currently not able to reach its destination.
   */
  EVENT(OnNoPathFound) m_noPathFoundEvent;
};

template <class DerivedBehavior>
class UnitMoveBehaviorT : public virtual UnitMoveBehavior, public UnitBehaviorT<DerivedBehavior>
{
public:
  UnitMoveBehaviorT(const Process* parent, unsigned int initialPriority, BWAPI::Unit* unit,
                    BWAPI::Position destination, const MoveOptions& moveOptions)
    : UnitBehaviorT(parent, initialPriority, unit)
    , UnitMoveBehavior(parent, initialPriority, unit, destination, moveOptions)
    , UnitBehavior(parent, initialPriority, unit)
    , Behavior(parent, initialPriority)
    , FSM(parent)
    , Process(parent)
  {
  }

protected:
  class Goto;
  friend class Goto;

  class Goto : public TimerState<DerivedBehavior, typename DerivedBehavior::Goto>
  {
  public:
    Goto(DerivedBehavior& behavior, size_t refreshDelay)
      : TimerState(behavior, refreshDelay, 1) // TODO: why does '0' crash?
      , m_frameDebugEvent(static_cast<typename DerivedBehavior::Goto&>(*this), Scheduler::FSM_SCHEDULING_PRIORITY, 1, 1)
      , m_firstFrameFailure(boost::numeric::bounds<int>::highest())
      , m_lastFrameMoveSubmitted(boost::numeric::bounds<int>::lowest())
    {}

    void onTimer()
    {
      const int distance = m_context.m_unit->getPosition().getApproxDistance(m_context.m_destination);

      // Warning if many unit want to reach the same destination, they will not all fit on the same tile.
      if(distance <= static_cast<int>(m_context.m_moveOptions.m_maxDistance))
      {
        m_context.terminate(m_context.Success);
      }
    }

    typedef typename DerivedBehavior::Goto DerivedGoto;
    typedef SCHEDULER_EVENT_HANDLER(DerivedGoto, onFrameDebug) FrameDebugEvent;

  protected:
    void submitMoveCommand(BWAPI::Position pos)
    {
      // Can be replaced eventually by commands so that arbitrator can properly arbitrate
      if (m_context.m_moveOptions.m_attackMove)
        m_context.m_unit->attack(pos);
      else
        m_context.m_unit->move(pos);

      m_lastFrameMoveSubmitted = BWAPI::Broodwar->getFrameCount();
    }

    void onPathPlanning(bool pathFound)
    {
      if (pathFound)
      {
        m_firstFrameFailure = boost::numeric::bounds<int>::highest();
      }
      else
      {
        const int currentFrame = BWAPI::Broodwar->getFrameCount();
        m_firstFrameFailure = std::min(m_firstFrameFailure, currentFrame);
        m_context.m_noPathFoundEvent.raise(m_firstFrameFailure);
        if (!m_context.isTerminated() && static_cast<size_t>(currentFrame - m_firstFrameFailure) >= m_context.m_moveOptions.m_givingUpDelayIfNoPathFound)
          m_context.terminate(NoPathFound);
      }
    }

    void onFrameDebug()
    {
      const std::set<BWAPI::Unit*>& selectedUnits = BWAPI::Broodwar->getSelectedUnits();
      if (Config::getInstance().displayOwnUnitsActions() && selectedUnits.find(m_context.m_unit) != selectedUnits.end() /*m_context.m_unit->isSelected()*/)
      {
        BWAPI::Position unitPosition = m_context.m_unit->getPosition();

        // Display goal
        BWAPI::Broodwar->drawLineMap(unitPosition.x(), unitPosition.y(), m_context.m_destination.x(), m_context.m_destination.y(), BWAPI::Colors::Yellow);
        BWAPI::Broodwar->drawCircleMap(m_context.m_destination.x() , m_context.m_destination.y(), 15, BWAPI::Colors::Yellow, false);
      }
    }

  protected:
    FrameDebugEvent m_frameDebugEvent;

    int m_firstFrameFailure;
    int m_lastFrameMoveSubmitted;
  };

protected:
  virtual void onStart()
  {
    UnitBehaviorT::onStart();
    static_cast<DerivedBehavior*>(this)->m_gotoState.enter();
  }

};