/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "common/system.h"
#include "ultima/ultima.h"
#include "ultima/ultima8/misc/direction_util.h"
#include "ultima/ultima8/world/actors/actor.h"
#include "ultima/ultima8/world/actors/animation_tracker.h"

#ifdef DEBUG_PATHFINDER
#include "graphics/screen.h"
#include "ultima/ultima8/gumps/game_map_gump.h"
#endif

namespace Ultima {
namespace Ultima8 {


#ifdef DEBUG_PATHFINDER
ObjId Pathfinder::_visualDebugActor = 0xFFFF;
#endif

struct PathNode {
	PathfindingState state;
	unsigned int depth;
	unsigned int cost;
	unsigned int heuristicTotalCost;
	PathNode *parent;
	uint32 stepsfromparent;
};

// NOTE: this is just to keep some statistics
static unsigned int expandednodes = 0;

void PathfindingState::load(const Actor *_actor) {
	_point = _actor->getLocation();
	_lastAnim = _actor->getLastAnim();
	_direction = _actor->getDir();
	_firstStep = _actor->hasActorFlags(Actor::ACT_FIRSTSTEP);
	_flipped = _actor->hasFlags(Item::FLG_FLIPPED);
	_combat = _actor->isInCombat();
}

bool PathfindingState::checkPoint(const Point3 &pt,
								  int sqr_range) const {
	int distance = _point.sqrDist(pt);
	return distance < sqr_range;
}

bool PathfindingState::checkItem(const Item *item, int xyRange, int zRange) const {
	int32 itemXd, itemYd, itemZd;
	int32 itemXmin, itemYmin;

	Point3 pt = item->getLocationAbsolute();
	item->getFootpadWorld(itemXd, itemYd, itemZd);

	itemXmin = pt.x - itemXd;
	itemYmin = pt.y - itemYd;

	int range = 0;
	if (_point.x - pt.x > range)
		range = _point.x - pt.x;
	if (itemXmin - _point.x > range)
		range = itemXmin - _point.x;
	if (_point.y - pt.y > range)
		range = _point.y - pt.y;
	if (itemYmin - _point.y > range)
		range = itemYmin - _point.y;

	// FIXME: check _point.z properly

	return (range <= xyRange);
}

bool PathfindingState::checkHit(const Actor *_actor, const Item *target) const {
	assert(target);
	debugC(kDebugPath, "Trying hit in _direction %d", _actor->getDirToItemCentre(*target));
	AnimationTracker tracker;
	if (!tracker.init(_actor, Animation::attack,
	                  _actor->getDirToItemCentre(*target), this)) {
		return false;
	}

	while (tracker.step()) {
		if (tracker.hitSomething()) break;
	}

	ObjId hit = tracker.hitSomething();
	if (hit == target->getObjId()) return true;

	return false;
}

bool PathNodeCmp::operator()(const PathNode *n1, const PathNode *n2) const {
	return (n1->heuristicTotalCost > n2->heuristicTotalCost);
}

Pathfinder::Pathfinder() : _actor(nullptr), _targetItem(nullptr),
		_hitMode(false), _expandTime(0), _target(),
		_actorXd(0), _actorYd(0), _actorZd(0) {
	expandednodes = 0;
	_visited.reserve(1500);
}

Pathfinder::~Pathfinder() {
	debugC(kDebugPath, "~Pathfinder: %u nodes to clean up, visited %u and %u expanded nodes in %dms.",
		_cleanupNodes.size(), _visited.size(), expandednodes, _expandTime);

	// clean up _nodes
	Std::vector<PathNode *>::iterator iter;
	for (iter = _cleanupNodes.begin(); iter != _cleanupNodes.end(); ++iter)
		delete *iter;
	_cleanupNodes.clear();
}

void Pathfinder::init(Actor *actor, PathfindingState *state) {
	_actor = actor;

	_actor->getFootpadWorld(_actorXd, _actorYd, _actorZd);

	if (state)
		_start = *state;
	else
		_start.load(_actor);
}

void Pathfinder::setTarget(const Point3 &pt) {
	_target = pt;
	_targetItem = 0;
	_hitMode = false;
}

void Pathfinder::setTarget(Item *item, bool hit) {
	Container *root = item->getRootContainer();
	_targetItem = root ? root : item;

	// set target to centre of item for the cost heuristic
	_target = item->getCentre();
	_target.z = item->getZ();

	if (hit) {
		assert(_start._combat);
		assert(dynamic_cast<Actor *>(_targetItem));
		_hitMode = true;
	} else {
		_hitMode = false;
	}
}

bool Pathfinder::canReach() {
	Std::vector<PathfindingAction> path;
	return pathfind(path);
}

bool Pathfinder::alreadyVisited(const Point3 &pt) const {
	//
	// There are more efficient search structures we could use for
	// this, but for the number of points we end up having even on
	// pathfind failure (~1200) the fancy structures don't justify
	// their extra overhead.
	//
	// Linear search of an array is just as fast, or slightly faster.
	//
	Common::Array<PathfindingState>::const_iterator iter;
	for (iter = _visited.begin(); iter != _visited.end(); iter++) {
		if (iter->checkPoint(pt, 8*8))
			return true;
	}

	return false;
}

bool Pathfinder::checkTarget(const PathNode *node) const {
	// TODO: these ranges are probably a bit too high,
	// but otherwise it won't work properly yet -wjp
	if (_targetItem) {
		if (_hitMode) {
			return node->state.checkHit(_actor, _targetItem);
		} else {
			return node->state.checkItem(_targetItem, 32, 8);
		}
	} else {
		return node->state.checkPoint(_target, 48*48);
	}
}

unsigned int Pathfinder::costHeuristic(PathNode *node) const {
	unsigned int cost = node->cost;

#if 0
	double sqrddist;

	sqrddist = (_target.x - node->state._point.x + _actorXd / 2) *
	           (_target.x - node->state._point.x + _actorXd / 2);
	sqrddist += (_target.y - node->state._point.y + _actorYd / 2) *
	            (_target.y - node->state._point.y + _actorYd / 2);

	unsigned int dist = static_cast<unsigned int>(sqrt(sqrddist));
#else
	// This calculates the distance to the target using only lines in
	// the 8 available directions (instead of the straight line above)
	int xd = ABS(_target.x - node->state._point.x + _actorXd / 2);
	int yd = ABS(_target.y - node->state._point.y + _actorYd / 2);
	double m = (xd < yd) ? xd : yd;
	unsigned int dist = ABS(xd - yd) + static_cast<unsigned int>(m * 1.41421356);

#endif

#if 0
	//!! TODO: divide dist by walking distance
	// (using 32 for now)
	dist /= 32;

	node->heuristicTotalCost = cost + (dist * 4); //!! constant
#else

	// Weigh remaining distance more than already travelled distance,
	// to try to explore more nodes closer to the target.
	node->heuristicTotalCost = 2 * cost + 3 * dist;
#endif

	return node->heuristicTotalCost;
}

#ifdef DEBUG_PATHFINDER

static void drawbox(Graphics::ManagedSurface *screen, const Item *item) {
	int32 cx, cy, cz;
	Ultima8Engine::get_instance()->getGameMapGump()->GetCameraLocation(cx, cy, cz);

	Common::Rect d = screen->getBounds();

	int32 ix, iy, iz;
	item->getLocation(ix, iy, iz);

	int32 xd, yd, zd;
	item->getFootpadWorld(xd, yd, zd);

	ix -= cx;
	iy -= cy;
	iz -= cz;

	int32 x0, y0, x1, y1, x2, y2, x3, y3;

	x0 = (d.width() / 2) + (ix - iy) / 4;
	y0 = (d.height() / 2) + (ix + iy) / 8 - iz;

	x1 = (d.width() / 2) + (ix - iy) / 4;
	y1 = (d.height() / 2) + (ix + iy) / 8 - (iz + zd);

	x2 = (d.width() / 2) + (ix - xd - iy) / 4;
	y2 = (d.height() / 2) + (ix - xd + iy) / 8 - iz;

	x3 = (d.width() / 2) + (ix - iy + yd) / 4;
	y3 = (d.height() / 2) + (ix + iy - yd) / 8 - iz;

	uint32 color = screen->format.RGBToColor(0x00, 0x00, 0xFF);
	screen->fillRect(Common::Rect(x0 - 1, y0 - 1, x0 + 2, y0 + 2), color);

	color = screen->format.RGBToColor(0x00, 0xFF, 0x00);
	screen->drawLine(x0, y0, x1, y1, color);
	screen->drawLine(x0, y0, x2, y2, color);
	screen->drawLine(x0, y0, x3, y3, color);
}

static void drawdot(Graphics::ManagedSurface *screen, int32 x, int32 y, int32 Z, int size, uint32 rgb) {
	int32 cx, cy, cz;

	Ultima8Engine::get_instance()->getGameMapGump()->GetCameraLocation(cx, cy, cz);

	Common::Rect d = screen->getBounds();
	x -= cx;
	y -= cy;
	Z -= cz;
	int32 x0, y0;
	x0 = (d.width() / 2) + (x - y) / 4;
	y0 = (d.height() / 2) + (x + y) / 8 - Z;
	screen->fillRect(Common::Rect(x0 - size, y0 - size, x0 + size + 1, y0 + size + 1), rgb);
}

static void drawedge(Graphics::ManagedSurface *screen, const PathNode *from, const PathNode *to, uint32 rgb) {
	int32 cx, cy, cz;

	Ultima8Engine::get_instance()->getGameMapGump()->GetCameraLocation(cx, cy, cz);

	Common::Rect d = screen->getBounds();

	int32 x0, y0, x1, y1;

	cx = from->state._point.x - cx;
	cy = from->state._point.y - cy;
	cz = from->state._point.z - cz;

	x0 = (d.width() / 2) + (cx - cy) / 4;
	y0 = (d.height() / 2) + (cx + cy) / 8 - cz;

	Ultima8Engine::get_instance()->getGameMapGump()->GetCameraLocation(cx, cy, cz);

	cx = to->state._point.x - cx;
	cy = to->state._point.y - cy;
	cz = to->state._point.z - cz;

	x1 = (d.width() / 2) + (cx - cy) / 4;
	y1 = (d.height() / 2) + (cx + cy) / 8 - cz;

	screen->drawLine(x0, y0, x1, y1, rgb);
}

static void drawpath(Graphics::ManagedSurface *screen, PathNode *to, uint32 rgb, bool done) {
	PathNode *n1 = to;
	PathNode *n2 = to->parent;
	uint32 color1 = screen->format.RGBToColor(0xFF, 0x00, 0x00);
	uint32 color2 = screen->format.RGBToColor(0xFF, 0xFF, 0xFF);

	while (n2) {
		drawedge(screen, n1, n2, rgb);

		if (done && n1 == to)
			drawdot(screen, n1->state._point.x, n1->state._point.y, n1->state._point.z, 2, color1);
		else
			drawdot(screen, n1->state._point.x, n1->state._point.y, n1->state._point.z, 1, color2);


		drawdot(screen, n2->state._point.x, n2->state._point.y, n2->state._point.z, 2, color2);

		n1 = n2;
		n2 = n1->parent;
	}
}

#endif

void Pathfinder::newNode(PathNode *oldnode, PathfindingState &state,
						 unsigned int steps) {
	PathNode *newnode = new PathNode();
	newnode->state = state;
	newnode->parent = oldnode;
	newnode->depth = oldnode->depth + 1;
	newnode->stepsfromparent = 0;

	double sqrddist;

	sqrddist = ((newnode->state._point.x - oldnode->state._point.x) *
	            (newnode->state._point.x - oldnode->state._point.x));
	sqrddist += ((newnode->state._point.y - oldnode->state._point.y) *
	             (newnode->state._point.y - oldnode->state._point.y));
	sqrddist += ((newnode->state._point.z - oldnode->state._point.z) *
	             (newnode->state._point.z - oldnode->state._point.z));

	unsigned int dist;
	dist = static_cast<unsigned int>(sqrt(sqrddist));

	int turn = 0;

	if (oldnode->depth > 0) {
		turn = state._direction - oldnode->state._direction;
		if (turn < 0) turn = -turn;
		if (turn > 8) turn = 16 - turn;
	}

	newnode->cost = oldnode->cost + dist + 32 * turn; //!! constant

	bool done = checkTarget(newnode);
	if (done)
		newnode->heuristicTotalCost = 0;
	else
		costHeuristic(newnode);

	debugC(kDebugPath, "Trying dir %d, steps %d from (%d, %d) to (%d, %d), cost %d, heurtotcost %d",
		   state._direction, steps,
		   oldnode->state._point.x, oldnode->state._point.y, newnode->state._point.x, newnode->state._point.y,
		   newnode->cost, newnode->heuristicTotalCost);

#ifdef DEBUG_PATHFINDER
	if (_actor->getObjId() == _visualDebugActor) {
		Graphics::Screen *screen = Ultima8Engine::get_instance()->getScreen();
		uint32 color = screen->format.RGBToColor(0xFF, 0xFF, 0x00);
		drawpath(screen, newnode, color, done);
		screen->update();
		g_system->delayMillis(50);
		if (!done) {
			color = screen->format.RGBToColor(0xB0, 0xB0, 0x00);
			drawpath(screen, newnode, color, done);
			screen->update();
		}
	}
#endif

	_nodes.push(newnode);
}

void Pathfinder::expandNode(PathNode *node) {
	Animation::Sequence walkanim = Animation::walk;
	PathfindingState state, closeststate;
	AnimationTracker tracker;
	expandednodes++;

	if (_actor->isInCombat())
		walkanim = Animation::advance;

	// try walking in all 8 directions - TODO: should this support 16 dirs?
	Direction dir = dir_north;
	for (int i = 0; i < 8; i++) {
		dir = Direction_OneRight(dir, dirmode_8dirs);
		state = node->state;
		state._lastAnim = walkanim;
		state._direction = dir;
		state._combat = _actor->isInCombat();

		if (!tracker.init(_actor, walkanim, dir, &state)) continue;

		// determine how far the _actor will travel if the animation runs to completion
		Point3 max_end;
		tracker.evaluateMaxAnimTravel(max_end.x, max_end.y, dir);
		max_end.z = state._point.z;
		if (alreadyVisited(max_end))
			continue;

		const int x_travel = ABS(max_end.x - state._point.x);
		const int y_travel = ABS(max_end.y - state._point.y);
		const int xy_maxtravel = MAX(x_travel, y_travel);

		int sqrddist = x_travel * x_travel + y_travel * y_travel;
		if (sqrddist > 400) {
			// range is greater than 20; see if a node has been visited at range 10
			Point3 pt = state._point;
			pt.x += x_travel * 10 / xy_maxtravel;
			pt.y += y_travel * 10 / xy_maxtravel;
			if (alreadyVisited(pt)) {
				continue;
			}
		}

		uint32 steps = 0, beststeps = 0;
		int bestsqdist;
		bestsqdist = (_target.x - node->state._point.x + _actorXd / 2) *
					 (_target.x - node->state._point.x + _actorXd / 2);
		bestsqdist += (_target.y - node->state._point.y + _actorYd / 2) *
					  (_target.y - node->state._point.y + _actorYd / 2);

		while (tracker.step()) {
			steps++;
			tracker.updateState(state);

			sqrddist = (_target.x - state._point.x + _actorXd / 2) *
			           (_target.x - state._point.x + _actorXd / 2);
			sqrddist += (_target.y - state._point.y + _actorYd / 2) *
			            (_target.y - state._point.y + _actorYd / 2);

			if (sqrddist < bestsqdist) {
				bestsqdist = sqrddist;
				beststeps = steps;
				closeststate = state;
			}
		}

		if (tracker.isDone()) {
			tracker.updateState(state);
			if (!alreadyVisited(state._point)) {
				newNode(node, state, 0);
				_visited.push_back(state);
			}
		} else {
			// an obstruction was encountered, so generate a visited node to block
			// future evaluation at the endpoint.
			_visited.push_back(state);
		}

		// TODO: maybe only allow partial steps close to target?
		if (beststeps != 0 && (beststeps != steps ||
		                       (!tracker.isDone() && _targetItem))) {
			newNode(node, closeststate, beststeps);
			_visited.push_back(closeststate);
		}
	}
}

bool Pathfinder::pathfind(Std::vector<PathfindingAction> &path) {
	if (_targetItem) {
		debugC(kDebugPath, "Actor %u pathfinding to item %u", _actor->getObjId(), _targetItem->getObjId());
		debugC(kDebugPath, "Target Item: %s", _targetItem->dumpInfo().c_str());
	} else {
		debugC(kDebugPath, "Actor %u pathfinding to (%d, %d, %d)", _actor->getObjId(), _target.x, _target.y, _target.z);
	}

#ifdef DEBUG_PATHFINDER
	if (_actor->getObjId() == _visualDebugActor) {
		Graphics::Screen *screen = Ultima8Engine::get_instance()->getScreen();
		if (_targetItem) {
			drawbox(screen, _targetItem);
		} else {
			uint32 color = screen->format.RGBToColor(0x00, 0x00, 0xFF);
			drawdot(screen, _targetX, _targetY, _targetZ, 2, color);
		}
		screen->update();
	}
#endif


	path.clear();

	PathNode *startnode = new PathNode();
	startnode->state = _start;
	startnode->cost = 0;
	startnode->parent = nullptr;
	startnode->depth = 0;
	startnode->stepsfromparent = 0;
	_nodes.push(startnode);

	unsigned int expandedNodes = 0;
	const unsigned int NODELIMIT_MIN = 30;  //! constant
	const unsigned int NODELIMIT_MAX = 200; //! constant
	bool found = false;
	uint32 starttime = g_system->getMillis();

	while (expandedNodes < NODELIMIT_MAX && !_nodes.empty() && !found) {
		// Take a copy here as the pop() below deletes the old node
		PathNode *node = new PathNode(*_nodes.top());
		_cleanupNodes.push_back(node);
		_nodes.pop();

		debugC(kDebugPath, "Trying node: (%d, %d, %d) target=(%d, %d, %d)",
			node->state._point.x, node->state._point.y, node->state._point.z,
			_target.x, _target.y, _target.z);

		if (checkTarget(node)) {
			// done!

			// find path length
			const PathNode *n = node;
			unsigned int length = 0;
			while (n->parent) {
				n = n->parent;
				length++;
			}

			debugC(kDebugPath, "Pathfinder: path found (length = %u)", length);

			unsigned int i = length;
			if (length > 0) length++; // add space for final 'stand' action
			path.resize(length);

			// now backtrack through the _nodes to assemble the final animation
			while (node->parent) {
				PathfindingAction action;
				action._action = node->state._lastAnim;
				action._direction = node->state._direction;
				action._steps = node->stepsfromparent;
				path[--i] = action;

				debugC(kDebugPath, "anim = %d, dir = %d, steps = %d",
					node->state._lastAnim, node->state._direction, node->stepsfromparent);

				//TODO: check how turns work
				//TODO: append final 'stand' animation

				node = node->parent;
			}

			if (length) {
				if (node->state._combat)
					path[length - 1]._action = Animation::combatStand;
				else
					path[length - 1]._action = Animation::stand;
				path[length - 1]._direction = path[length - 2]._direction;
			}

			_expandTime = g_system->getMillis() - starttime;
			return true;
		}

		expandNode(node);
		expandedNodes++;

		if (expandedNodes >= NODELIMIT_MIN && ((expandedNodes) % 5) == 0) {
			uint32 elapsed_ms = g_system->getMillis() - starttime;
			if (elapsed_ms > 350) break;
		}
	}

	_expandTime = g_system->getMillis() - starttime;

	static int32 pfcalls = 0;
	static int32 pftotaltime = 0;
	pfcalls++;
	pftotaltime += _expandTime;
	debugC(kDebugPath, "maxout average = %dms.", pftotaltime / pfcalls);

	return false;
}

} // End of namespace Ultima8
} // End of namespace Ultima
