/* 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 "illusions/illusions.h"
#include "illusions/pathfinder.h"
#include "camera.h"

namespace Illusions {

PointArray *PathFinder::findPath(Camera *camera, Common::Point sourcePt, Common::Point destPt,
	PointArray *walkPoints, PathLines *walkRects, WidthHeight bgDimensions) {
	Common::Point cameraPt = camera->getScreenOffset();
	_screenRect.p0 = cameraPt;
	_screenRect.p1.x = cameraPt.x + 320; //TODO fix me get screen dimensions here.
	_screenRect.p1.y = cameraPt.y + 200;
	_walkPoints = walkPoints;
	_walkRects = walkRects;
	_bgDimensions = bgDimensions;
	return findPathInternal(sourcePt, destPt);
}

PointArray *PathFinder::findPathInternal(Common::Point sourcePt, Common::Point destPt) {
	PathLine line;
	PointArray *foundPath = new PointArray();
	line.p0 = sourcePt;
	line.p1 = destPt;

	if (_walkRects && _walkPoints && isLineBlocked(line)) {
		Common::Point nextStartPt = sourcePt, outPt;

		if (!findValidDestLine(destPt)) {
			findValidDestPt(destPt);
			line.p1 = destPt;
		}

		_pathBytes = (byte*)calloc(1, _walkPoints->size());

		bool done = false;
		while (!done) {
			line.p0 = nextStartPt;
			if (!isLineBlocked(line)) {
				foundPath->push_back(destPt);
				done = true;
			} else {
				if (foundPath->size() < _walkPoints->size() + 2 && findClosestPt(nextStartPt, outPt, destPt)) {
					foundPath->push_back(outPt);
					nextStartPt = outPt;
				} else {
					if (foundPath->size() == 0)
						foundPath->push_back(sourcePt);
					done = true;
				}
			}
		}

		free(_pathBytes);
		postProcess(sourcePt, foundPath);

	} else {
		foundPath->push_back(destPt);
	}
	return foundPath;
}

void PathFinder::postProcess(Common::Point sourcePt, PointArray *foundPath) {
	// For each three points A, B and C, removes B if the line between A and C is not blocked
	for (uint index = 0; index + 2 < foundPath->size(); ++index) {
		PathLine line;
		line.p0 = index == 0 ? sourcePt : (*foundPath)[index - 1];
		line.p1 = (*foundPath)[index + 1];
		if (!isLineBlocked(line)) {
			debug("remove point");
			foundPath->remove_at(index);
		}
	}
}

bool PathFinder::isLineBlocked(PathLine &line) {
	for (uint i = 0; i < _walkRects->size(); ++i) {
		if (calcLineStatus(line, (*_walkRects)[i], nullptr) != 3)
			return true;
	}
	return false;
}

int PathFinder::calcLineDistance(PathLine &line) {
	int16 deltaX = line.p0.x - line.p1.x;
	int16 deltaY = line.p0.y - line.p1.y;
	if (deltaX != 0 || deltaY != 0)
		return sqrt(deltaX * deltaX + deltaY * deltaY);
	return 0;
}

bool PathFinder::findClosestPt(Common::Point &sourcePt, Common::Point &closestPt, Common::Point &destPt) {
	PathLine sourceLine, destLine;
	uint minIndex = 0;
	int minDistance = 0xFFFF;
	sourceLine.p0 = sourcePt;
	destLine.p1 = destPt;
	for (uint i = 0; i < _walkPoints->size(); ++i) {
		sourceLine.p1 = (*_walkPoints)[i];
		destLine.p0 = (*_walkPoints)[i];
		if (!_pathBytes[i] && !isLineBlocked(sourceLine)) {
			int currDistance = calcLineDistance(destLine);
			if (currDistance <= minDistance) {
				minDistance = currDistance;
				minIndex = i + 1;
			}
		}
	}
	if (minIndex) {
		closestPt = (*_walkPoints)[minIndex - 1];
		_pathBytes[minIndex - 1] = 1;
		return true;
	}
	return false;
}

bool PathFinder::findValidDestLine(Common::Point &destPt) {
	PathLine destLine;
	destLine.p0 = destPt;
	for (uint i = 0; i < _walkPoints->size(); ++i) {
		destLine.p1 = (*_walkPoints)[i];
		if (!isLineBlocked(destLine))
			return true;
	}
	return false;
}

void PathFinder::findValidDestPt(Common::Point &destPt) {
	Common::Point minPt, outPt, deltaPt;
	int minDistance = 0xFFFF, currDistance;
	PathLine destLine;
	for (uint i = 0; i < _walkRects->size(); ++i) {
		PathLine currRect = (*_walkRects)[i];
		//TODO fix this hack. Used here to get xmas tree scene to work.
		if (currRect.p1.x > _screenRect.p1.x) {
			currRect.p1.x = _screenRect.p1.x;
		}
		if (currRect.p0.x < _screenRect.p0.x) {
			currRect.p0.x = _screenRect.p0.x;
		}
		WidthHeight rectDimensions = calcRectDimensions(currRect);

		adjustRectDimensions(rectDimensions);
		clipLineToBg(destPt, rectDimensions, destLine);
		if (calcLineStatus(destLine, currRect, &outPt) == 3) {
			destLine.p0 = destPt;
			destLine.p1 = currRect.p0;
			currDistance = calcLineDistance(destLine);
			if (currDistance < minDistance) {
				minDistance = currDistance;
				minPt = currRect.p0;
			}
			destLine.p0 = destPt;
			destLine.p1 = currRect.p1;
			currDistance = calcLineDistance(destLine);
			if (currDistance < minDistance) {
				minDistance = currDistance;
				minPt = currRect.p1;
			}
		} else {
			destLine.p0 = destPt;
			destLine.p1 = outPt;
			currDistance = calcLineDistance(destLine);
			if (currDistance < minDistance) {
				minDistance = currDistance;
				minPt = outPt;
			}
		}
	}
	findDeltaPt(minPt, deltaPt);
	destPt.x = deltaPt.x + minPt.x;
	destPt.y = deltaPt.y + minPt.y;
}

WidthHeight PathFinder::calcRectDimensions(PathLine &rect) {
	WidthHeight dimensions;
	dimensions._width = rect.p1.x - rect.p0.x;
	dimensions._height = rect.p1.y - rect.p0.y;
	swapDimensions(dimensions);
	return dimensions;
}

void PathFinder::adjustRectDimensions(WidthHeight &dimensions) {
	dimensions._width = ABS(dimensions._height) * (dimensions._width < 0 ? -1 : 1);
	dimensions._height = ABS(dimensions._width) * (dimensions._height < 0 ? -1 : 1);
	if (dimensions._width)
		dimensions._width = -dimensions._width;
	else
		dimensions._height = -dimensions._height;
	swapDimensions(dimensions);
}

void PathFinder::swapDimensions(WidthHeight &dimensions) {
	if (dimensions._width < 0) {
		dimensions._width = -dimensions._width;
		dimensions._height = -dimensions._height;
	} else if (dimensions._width == 0)
		dimensions._height = abs(dimensions._height);
	else if (dimensions._height == 0)
		dimensions._width = abs(dimensions._width);
}

void PathFinder::clipLineToBg(Common::Point &destPt, WidthHeight &rectDimensions, PathLine &outDestLine) {
	if (rectDimensions._height == 0) {
		outDestLine.p0.x = 0;
		outDestLine.p0.y = destPt.y;
		outDestLine.p1.x = _bgDimensions._width;
		outDestLine.p1.y = destPt.y;
	} else if (rectDimensions._width == 0) {
		outDestLine.p0.y = 0;
		outDestLine.p0.x = destPt.x;
		outDestLine.p1.x = destPt.x;
		outDestLine.p1.y = _bgDimensions._height;
	} else {
		outDestLine.p0 = destPt;
		outDestLine.p1.x = destPt.x + rectDimensions._width;
		outDestLine.p1.y = destPt.y + rectDimensions._height;
		int16 y1 = destPt.y + (rectDimensions._height * -destPt.x / rectDimensions._width);
		int16 y2 = destPt.y + (rectDimensions._height * (_bgDimensions._width - destPt.x) / rectDimensions._width);
		int16 x1 = destPt.x + (rectDimensions._width * -destPt.y / rectDimensions._height);
		int16 x2 = destPt.x + (rectDimensions._width * (_bgDimensions._height - destPt.y) / rectDimensions._height);
		if (ABS(rectDimensions._height) <= ABS(rectDimensions._width)) {
			outDestLine.p0.y = 0;
			outDestLine.p0.x = _bgDimensions._width;
			if (x1 < 0 || _bgDimensions._width < x1)
				outDestLine.p0.y = y2;
			else
				outDestLine.p0.x = x1;
			outDestLine.p1.x = 0;
			outDestLine.p1.y = _bgDimensions._height;
			if (x2 < 0 || _bgDimensions._width < x2)
				outDestLine.p1.y = y1;
			else
				outDestLine.p1.x = x2;
		} else {
			outDestLine.p0.y = 0;
			outDestLine.p0.x = 0;
			if (x1 < 0 || _bgDimensions._width < x1)
				outDestLine.p0.y = y1;
			else
				outDestLine.p0.x = x1;
			outDestLine.p1.x = _bgDimensions._width;
			outDestLine.p1.y = _bgDimensions._height;
			if (x2 < 0 || _bgDimensions._width < x2)
				outDestLine.p1.y = y2;
			else
				outDestLine.p1.x = x2;
		}
	}
}

void PathFinder::findDeltaPt(Common::Point pt, Common::Point &outDeltaPt) {
	static const struct { int16 x, y; } kDeltaPoints[] = {
		{ 0, -4}, {0, 4}, {-4, 0}, { 4,  0}, {-3, -3}, {3, 3}, {-3, 3}, { 3, -3},
		{-2, -4}, {2, 4}, {-2, 4}, { 2, -4}, {-4, -2}, {4, 2}, {-4, 2}, { 4, -2},
		{-1, -4}, {1, 4}, {-1, 4}, { 1, -4}, {-4, -1}, {4, 1}, {-4, 1}, { 4, -1},
		{-2, -3}, {2, 3}, {-2, 3}, { 2, -3}, {-3, -2}, {3, 2}, {-3, 2}, { 3, -2}
	};
	Common::Point testPt;
	for (uint i = 0; i < 32; ++i) {
		testPt.x = pt.x + kDeltaPoints[i].x;
		testPt.y = pt.y + kDeltaPoints[i].y;
		if (findValidDestLine(testPt)) {
			outDeltaPt.x = kDeltaPoints[i].x;
			outDeltaPt.y = kDeltaPoints[i].y;
			break;
		}
	}
}
/**
 * returns true if line is contained within rect.
 */
bool PathFinder::isLineWithinRectangle(PathLine &line, PathLine &rect) {
	return line.p0.x <= rect.p1.x && line.p1.x >= rect.p0.x &&
		line.p0.y <= rect.p1.y && line.p1.y >= rect.p0.y;
}

/**
 * flip line coordinates so it starts top left and finishes bottom right
 */
void PathFinder::swapLine(PathLine &line, PathLine &outLine) {
	if (line.p1.x <= line.p0.x) {
		outLine.p1.x = line.p0.x;
		outLine.p0.x = line.p1.x;
	} else {
		outLine.p0.x = line.p0.x;
		outLine.p1.x = line.p1.x;
	}
	if (line.p1.y <= line.p0.y) {
		outLine.p1.y = line.p0.y;
		outLine.p0.y = line.p1.y;
	} else {
		outLine.p0.y = line.p0.y;
		outLine.p1.y = line.p1.y;
	}
}

int PathFinder::calcLineStatus(PathLine &sourceLine, PathLine &destRect, Common::Point *outPoint) {
	PathLine sourceLine1, destRect1;
	swapLine(sourceLine, sourceLine1);
	swapLine(destRect, destRect1);

	if (!isLineWithinRectangle(sourceLine1, destRect1))
		return 3;

	int sourceDeltaX = sourceLine.p1.x - sourceLine.p0.x;
	int sourceDeltaY = sourceLine.p1.y - sourceLine.p0.y;
	int destDeltaX = destRect.p0.x - destRect.p1.x;
	int destDeltaY = destRect.p0.y - destRect.p1.y;
	int sdDeltaX = sourceLine.p0.x - destRect.p0.x;
	int sdDeltaY = sourceLine.p0.y - destRect.p0.y;
	int delta1 = destDeltaY * sdDeltaX - destDeltaX * sdDeltaY;
	int delta2 = sourceDeltaY * destDeltaX - sourceDeltaX * destDeltaY;
	int delta3 = sourceDeltaX * sdDeltaY - sourceDeltaY * sdDeltaX;

	if ((delta2 <= 0 && (delta1 > 0 || delta2 > delta1)) ||
		(delta2 > 0 && (delta1 < 0 || delta2 < delta1)) ||
		(delta2 <= 0 && (delta3 > 0 || delta2 > delta3)) ||
		(delta2 > 0 && (delta3 < 0 || delta2 < delta3)))
		return 3;

	if (!outPoint)
		return 1;

	if (delta2 == 0)
		return 2;

	int v15 = sourceDeltaX * delta1, v18 = sourceDeltaY * delta1;
	int v16 = 0;
	int v17 = 0;

	if ((v15 >= 0 && delta2 >= 0) || (v15 < 0 && delta2 < 0)) {
		v16 = delta2 / 2;
		v17 = delta2 / 2;
	} else if ((v15 < 0 && delta2 >= 0) || (v15 >= 0 && delta2 < 0)) {
		v17 = delta2 / 2;
		v16 = delta2 / -2;
	}

	outPoint->x = sourceLine.p0.x + (v15 + v16) / delta2;

	if ((v18 >= 0 && delta2 < 0) || (v18 < 0 && delta2 >= 0))
		v17 = -v17;

	outPoint->y = sourceLine.p0.y + (v18 + v17) / delta2;

	return 1;
}

} // End of namespace Illusions
