/* 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 "bladerunner/scene_objects.h"

#include "bladerunner/bladerunner.h"

#include "bladerunner/obstacles.h"
#include "bladerunner/savefile.h"
#include "bladerunner/view.h"

namespace BladeRunner {

SceneObjects::SceneObjects(BladeRunnerEngine *vm, View *view) {
	_vm = vm;
	_view = view;

	_count = 0;

	clear();
}

SceneObjects::~SceneObjects() {
	_vm = nullptr;
	_view = nullptr;
	_count = 0;
}

void SceneObjects::clear() {
	for (int i = 0; i < kSceneObjectCount; ++i) {
		_sceneObjects[i].id               = -1;
		_sceneObjects[i].type             = kSceneObjectTypeUnknown;
		_sceneObjects[i].distanceToCamera = 0.0f;
		_sceneObjects[i].isPresent        = false;
		_sceneObjects[i].isClickable      = false;
		_sceneObjects[i].isObstacle       = false;
		_sceneObjects[i].unknown1         = 0;
		_sceneObjects[i].isTarget         = false;
		_sceneObjects[i].isMoving         = false;
		_sceneObjects[i].isRetired        = false;

		_sceneObjectsSortedByDistance[i]  = -1;
	}
	_count = 0;
}

bool SceneObjects::addActor(int sceneObjectId, const BoundingBox &boundingBox, const Common::Rect &screenRectangle, bool isClickable, bool isMoving, bool isTarget, bool isRetired) {
	return addSceneObject(sceneObjectId, kSceneObjectTypeActor, boundingBox, screenRectangle, isClickable, false, 0, isTarget, isMoving, isRetired);
}

bool SceneObjects::addObject(int sceneObjectId, const BoundingBox &boundingBox, bool isClickable, bool isObstacle, uint8 unknown1, bool isTarget) {
	Common::Rect rect(-1, -1, -1, -1);
	return addSceneObject(sceneObjectId, kSceneObjectTypeObject, boundingBox, rect, isClickable, isObstacle, unknown1, isTarget, false, false);
}

bool SceneObjects::addItem(int sceneObjectId, const BoundingBox &boundingBox, const Common::Rect &screenRectangle, bool isTarget, bool isObstacle) {
	return addSceneObject(sceneObjectId, kSceneObjectTypeItem, boundingBox, screenRectangle, isObstacle, 0, 0, isTarget, 0, 0);
}

bool SceneObjects::remove(int sceneObjectId) {
	int i = findById(sceneObjectId);
	if (i == -1) {
		return false;
	}
	_sceneObjects[i].isPresent = false;
	int j;
	for (j = 0; j < _count; ++j) {
		if (_sceneObjectsSortedByDistance[j] == i) {
			break;
		}
	}
	for (int k = j; k < _count - 1; ++k) {
		_sceneObjectsSortedByDistance[k] = _sceneObjectsSortedByDistance[k + 1];
	}

	--_count;
	return true;
}

int SceneObjects::findByXYZ(bool *isClickable, bool *isObstacle, bool *isTarget, Vector3 &position, bool findClickables, bool findObstacles, bool findTargets) const {
	*isClickable = false;
	*isObstacle  = false;
	*isTarget    = false;

	for (int i = 0; i < _count; ++i) {
		assert(_sceneObjectsSortedByDistance[i] < kSceneObjectCount);

		const SceneObject *sceneObject = &_sceneObjects[_sceneObjectsSortedByDistance[i]];

		if ((findClickables && sceneObject->isClickable) ||
			(findObstacles  && sceneObject->isObstacle) ||
			(findTargets    && sceneObject->isTarget)) {
			BoundingBox boundingBox = sceneObject->boundingBox;

			if (sceneObject->type == kSceneObjectTypeActor) {
				boundingBox.expand(-4.0, 0.0, -4.0, 4.0, 0.0, 4.0);
			}

			if (boundingBox.inside(position)) {
				*isClickable = sceneObject->isClickable;
				*isObstacle  = sceneObject->isObstacle;
				*isTarget    = sceneObject->isTarget;

				return sceneObject->id;
			}
		}
	}

	return -1;
}

bool SceneObjects::existsOnXZ(int exceptSceneObjectId, float x, float z, bool movingActorIsObstacle, bool standingActorIsObstacle) const {
	float xMin = x - 12.5f;
	float xMax = x + 12.5f;
	float zMin = z - 12.5f;
	float zMax = z + 12.5f;

	int count = _count;

	if (count > 0) {
		for (int i = 0; i < count; ++i) {
			const SceneObject *sceneObject = &_sceneObjects[_sceneObjectsSortedByDistance[i]];
			bool isObstacle = false;
			if (sceneObject->type == kSceneObjectTypeActor) {
				if (sceneObject->isRetired) {
					isObstacle = false;
				} else if (sceneObject->isMoving) {
					isObstacle = movingActorIsObstacle;
				} else {
					isObstacle = standingActorIsObstacle;
				}
			} else {
				isObstacle = sceneObject->isObstacle;
			}

			if (isObstacle && sceneObject->id != exceptSceneObjectId) {
				float x1, y1, z1, x2, y2, z2;
				sceneObject->boundingBox.getXYZ(&x1, &y1, &z1, &x2, &y2, &z2);
				if (z1 <= zMax && z2 >= zMin && x1 <= xMax && x2 >= xMin) {
//					if (sceneObject->type == kSceneObjectTypeObject) {
//						Vector3 a(x1,y1,z1);
//						Vector3 b(x2,y2,z2);
//						Vector3 pos = _vm->_view->calculateScreenPosition(0.5 * (a + b));
//						debug("%d: %s (Clk: %s, Trg: %s, Prs: %s, Obs: %s, Mvg: %s), Pos(%02.2f,%02.2f,%02.2f)\n     Bbox(%02.2f,%02.2f,%02.2f) ~ (%02.2f,%02.2f,%02.2f)\n",
//								 sceneObject->id - kSceneObjectOffsetObjects,
//								 _vm->_scene->objectGetName(sceneObject->id - kSceneObjectOffsetObjects).c_str(),
//								 sceneObject->isClickable? "T" : "F",
//								 sceneObject->isTarget?    "T" : "F",
//								 sceneObject->isPresent?   "T" : "F",
//								 sceneObject->isObstacle?  "T" : "F",
//								 sceneObject->isMoving?    "T" : "F",
//								 pos.x, pos.y, pos.z,
//								 a.x, a.y, a.z, b.x, b.y, b.z);
//					}
					return true;
				}
			}
		}
	}
	return false;
}

int SceneObjects::findById(int sceneObjectId) const {
	for (int i = 0; i < _count; ++i) {
		int j = this->_sceneObjectsSortedByDistance[i];

		if (_sceneObjects[j].isPresent && _sceneObjects[j].id == sceneObjectId) {
			return j;
		}
	}
	return -1;
}

bool SceneObjects::addSceneObject(int sceneObjectId, SceneObjectType sceneObjectType, const BoundingBox &boundingBox, const Common::Rect &screenRectangle, bool isClickable, bool isObstacle, uint8 unknown1, bool isTarget, bool isMoving, bool isRetired) {
	int index = findEmpty();
	if (index == -1) {
		return false;
	}

	_sceneObjects[index].id              = sceneObjectId;
	_sceneObjects[index].type            = sceneObjectType;
	_sceneObjects[index].isPresent       = true;
	_sceneObjects[index].boundingBox     = boundingBox;
	_sceneObjects[index].screenRectangle = screenRectangle;
	_sceneObjects[index].isClickable     = isClickable;
	_sceneObjects[index].isObstacle      = isObstacle;
	_sceneObjects[index].unknown1        = unknown1;
	_sceneObjects[index].isTarget        = isTarget;
	_sceneObjects[index].isMoving        = isMoving;
	_sceneObjects[index].isRetired       = isRetired;

	float centerZ = (_sceneObjects[index].boundingBox.getZ0() + _sceneObjects[index].boundingBox.getZ1()) / 2.0f;

	float distanceToCamera = fabs(-centerZ - _view->_cameraPosition.y); // y<->z is intentional, not a bug
	_sceneObjects[index].distanceToCamera = distanceToCamera;

	// insert according to distance from camera
	int i;
	for (i = 0; i < _count; ++i) {
		if (distanceToCamera < _sceneObjects[_sceneObjectsSortedByDistance[i]].distanceToCamera) {
			break;
		}
	}
	for (int j = CLIP(_count - 1, 0, kSceneObjectCount - 2); j >= i; --j) {
		_sceneObjectsSortedByDistance[j + 1] = _sceneObjectsSortedByDistance[j];
	}

	_sceneObjectsSortedByDistance[i] = index;
	++_count;
	return true;
}

int SceneObjects::findEmpty() const {
	for (int i = 0; i < kSceneObjectCount; ++i) {
		if (!_sceneObjects[i].isPresent)
			return i;
	}
	return -1;
}

void SceneObjects::setMoving(int sceneObjectId, bool isMoving) {
	int i = findById(sceneObjectId);
	if (i == -1) {
		return;
	}
	_sceneObjects[i].isMoving = isMoving;
}

void SceneObjects::setRetired(int sceneObjectId, bool isRetired) {
	int i = findById(sceneObjectId);
	if (i == -1) {
		return;
	}
	_sceneObjects[i].isRetired = isRetired;
}

bool SceneObjects::isBetween(float sourceX, float sourceZ, float targetX, float targetZ, int sceneObjectId) const {
	int i = findById(sceneObjectId);
	if (i == -1) {
		return false;
	}

	float objectX1, objectY1, objectZ1, objectX2, objectY2, objectZ2;
	_sceneObjects[i].boundingBox.getXYZ(&objectX1, &objectY1, &objectZ1, &objectX2, &objectY2, &objectZ2);

	Vector2 intersection;
	return lineIntersection(Vector2(sourceX, sourceZ), Vector2(targetX, targetZ), Vector2(objectX1, objectZ1), Vector2(objectX2, objectZ1), &intersection)
	    || lineIntersection(Vector2(sourceX, sourceZ), Vector2(targetX, targetZ), Vector2(objectX2, objectZ1), Vector2(objectX2, objectZ2), &intersection)
	    || lineIntersection(Vector2(sourceX, sourceZ), Vector2(targetX, targetZ), Vector2(objectX2, objectZ2), Vector2(objectX1, objectZ2), &intersection)
	    || lineIntersection(Vector2(sourceX, sourceZ), Vector2(targetX, targetZ), Vector2(objectX1, objectZ2), Vector2(objectX1, objectZ1), &intersection);
}

bool SceneObjects::isObstacleBetween(const Vector3 &source, const Vector3 &target, int exceptSceneObjectId) const {
	for (int i = 0; i < _count; ++i) {
		const SceneObject *sceneObject = &_sceneObjects[_sceneObjectsSortedByDistance[i]];

		if (sceneObject->type == kSceneObjectTypeActor || !sceneObject->isObstacle || sceneObject->id == exceptSceneObjectId) {
			continue;
		}

		float objectX1, objectY1, objectZ1, objectX2, objectY2, objectZ2;
		sceneObject->boundingBox.getXYZ(&objectX1, &objectY1, &objectZ1, &objectX2, &objectY2, &objectZ2);

		if (84.0f <= objectY1 - source.y || 72.0f >= objectY2 - source.y) {
			continue;
		}

		float xAdjustement = (objectX2 - objectX1) * 0.1f;
		float zAdjustement = (objectZ2 - objectZ1) * 0.1f;

		objectX1 = objectX1 + xAdjustement;
		objectZ1 = objectZ1 + zAdjustement;
		objectX2 = objectX2 - xAdjustement;
		objectZ2 = objectZ2 - zAdjustement;

		Vector2 intersection;
		if (lineIntersection(Vector2(source.x, source.z), Vector2(target.x, target.z), Vector2(objectX1, objectZ1), Vector2(objectX2, objectZ1), &intersection)
		 || lineIntersection(Vector2(source.x, source.z), Vector2(target.x, target.z), Vector2(objectX2, objectZ1), Vector2(objectX2, objectZ2), &intersection)
		 || lineIntersection(Vector2(source.x, source.z), Vector2(target.x, target.z), Vector2(objectX2, objectZ2), Vector2(objectX1, objectZ2), &intersection)
		 || lineIntersection(Vector2(source.x, source.z), Vector2(target.x, target.z), Vector2(objectX1, objectZ2), Vector2(objectX1, objectZ1), &intersection)) {
			return true;
		}
	}
	return false;
}

void SceneObjects::setIsClickable(int sceneObjectId, bool isClickable) {
	int i = findById(sceneObjectId);
	if (i == -1) {
		return;
	}
	_sceneObjects[i].isClickable = isClickable;
}

void SceneObjects::setIsObstacle(int sceneObjectId, bool isObstacle) {
	int i = findById(sceneObjectId);
	if (i == -1) {
		return;
	}
	_sceneObjects[i].isObstacle = isObstacle;
}

void SceneObjects::setIsTarget(int sceneObjectId, bool isTarget) {
	int i = findById(sceneObjectId);
	if (i == -1) {
		return;
	}
	_sceneObjects[i].isTarget = isTarget;
}

void SceneObjects::updateObstacles() {
	_vm->_obstacles->clear();
	for (int i = 0; i < _count; ++i) {
		int index = _sceneObjectsSortedByDistance[i];
		const SceneObject *sceneObject = &_sceneObjects[index];
		if (sceneObject->isObstacle) {
			float x0, y0, z0, x1, y1, z1;
			sceneObject->boundingBox.getXYZ(&x0, &y0, &z0, &x1, &y1, &z1);
			_vm->_obstacles->add(x0, z0, x1, z1);
		}
	}
	_vm->_obstacles->backup();
}

bool SceneObjects::isEmptyScreenRectangle(int sceneObjectId) {
	int index = findById(sceneObjectId);
	if (index != -1) {
		const SceneObject *sceneObject = &_sceneObjects[index];
		return sceneObject->screenRectangle.isEmpty();
	}
	return true;
}

int SceneObjects::compareScreenRectangle(int sceneObjectId, const Common::Rect &rectangle) {
	int index = findById(sceneObjectId);
	if (index != -1) {
		const SceneObject *sceneObject = &_sceneObjects[index];
		if (sceneObject->screenRectangle == rectangle) {
			return 0;
		}
	}
	return -1;
}

void SceneObjects::resetScreenRectangleAndBbox(int sceneObjectId) {
	int index = findById(sceneObjectId);
	if (index != -1) {
		SceneObject *sceneObject = &_sceneObjects[index];
		sceneObject->screenRectangle.left   = -1;
		sceneObject->screenRectangle.top    = -1;
		sceneObject->screenRectangle.right  = -1;
		sceneObject->screenRectangle.bottom = -1;
		sceneObject->boundingBox.setXYZ(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
	}
}

void SceneObjects::synchScreenRectangle(int sceneObjectId, const Common::Rect &targetScreenRect) {
	int index = findById(sceneObjectId);
	if (index != -1) {
		SceneObject *sceneObject = &_sceneObjects[index];
		sceneObject->screenRectangle.left   = targetScreenRect.left;
		sceneObject->screenRectangle.top    = targetScreenRect.top;
		sceneObject->screenRectangle.right  = targetScreenRect.right;
		sceneObject->screenRectangle.bottom = targetScreenRect.bottom;
	}
}

void SceneObjects::save(SaveFileWriteStream &f) {
	f.writeInt(_count);
	for (int i = 0; i < kSceneObjectCount; ++i) {
		f.writeInt(_sceneObjects[i].id);
		f.writeInt(_sceneObjects[i].type);
		f.writeBoundingBox(_sceneObjects[i].boundingBox, true);
		f.writeRect(_sceneObjects[i].screenRectangle);
		f.writeFloat(_sceneObjects[i].distanceToCamera);
		f.writeBool(_sceneObjects[i].isPresent);
		f.writeBool(_sceneObjects[i].isClickable);
		f.writeBool(_sceneObjects[i].isObstacle);
		f.writeInt(_sceneObjects[i].unknown1);
		f.writeBool(_sceneObjects[i].isTarget);
		f.writeBool(_sceneObjects[i].isMoving);
		f.writeBool(_sceneObjects[i].isRetired);
	}
	for (int i = 0; i < kSceneObjectCount; ++i) {
		f.writeInt(_sceneObjectsSortedByDistance[i]);
	}
}

void SceneObjects::load(SaveFileReadStream &f) {
	_count = f.readInt();
	for (int i = 0; i < kSceneObjectCount; ++i) {
		_sceneObjects[i].id = f.readInt();
		_sceneObjects[i].type = (SceneObjectType)f.readInt();
		_sceneObjects[i].boundingBox = f.readBoundingBox(true);
		_sceneObjects[i].screenRectangle = f.readRect();
		_sceneObjects[i].distanceToCamera = f.readFloat();
		_sceneObjects[i].isPresent = f.readBool();
		_sceneObjects[i].isClickable = f.readBool();
		_sceneObjects[i].isObstacle = f.readBool();
		_sceneObjects[i].unknown1 = f.readInt();
		_sceneObjects[i].isTarget = f.readBool();
		_sceneObjects[i].isMoving = f.readBool();
		_sceneObjects[i].isRetired = f.readBool();
	}
	for (int i = 0; i < kSceneObjectCount; ++i) {
		_sceneObjectsSortedByDistance[i] = f.readInt();
	}
}

} // End of namespace BladeRunner
