/* 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include "bladerunner/actor_combat.h"

#include "bladerunner/actor.h"
#include "bladerunner/audio_speech.h"
#include "bladerunner/bladerunner.h"
#include "bladerunner/combat.h"
#include "bladerunner/game_constants.h"
#include "bladerunner/game_info.h"
#include "bladerunner/movement_track.h"
#include "bladerunner/savefile.h"
#include "bladerunner/scene.h"
#include "bladerunner/scene_objects.h"
#include "bladerunner/script/ai_script.h"
#include "bladerunner/set.h"
#include "bladerunner/settings.h"

namespace BladeRunner {

ActorCombat::ActorCombat(BladeRunnerEngine *vm) {
	_vm = vm;
	reset();
}

ActorCombat::~ActorCombat() {
}

void ActorCombat::setup() {
	reset();
}

void ActorCombat::combatOn(int actorId, int initialState, bool rangedAttackFlag, int enemyId, int waypointType, int fleeRatio, int coverRatio, int attackRatio, int damage, int range, bool unstoppable) {
	_actorId = actorId;
	_state = initialState;
	_rangedAttack = rangedAttackFlag;
	_enemyId = enemyId;
	_waypointType = waypointType;
	_damage = damage;
	_fleeRatioConst = fleeRatio;
	_coverRatioConst = coverRatio;
	_attackRatioConst = attackRatio;
	_fleeRatio = fleeRatio;
	_coverRatio = coverRatio;
	_attackRatio = attackRatio;
	_active = true;
	if (_rangedAttack) {
		_range = range;
	} else {
		_range = 300;
	}
	_unstoppable = unstoppable;

	Actor *actor = _vm->_actors[_actorId];

	_actorPosition = actor->getXYZ();
	_enemyPosition = _vm->_actors[_enemyId]->getXYZ();

	actor->_movementTrack->flush();
	actor->stopWalking(false);

	if (_enemyId == kActorMcCoy) {
		actor->setTarget(true);
	}

	_actorHp = actor->getCurrentHP();

	_coversWaypointCount = 0;
	for (int i = 0; i < (int)_vm->_gameInfo->getCoverWaypointCount(); ++i) {
		if (_vm->_combat->_coverWaypoints[i].type == waypointType && _vm->_combat->_coverWaypoints[i].setId == actor->getSetId()) {
			++_coversWaypointCount;
		}
	}
	if (_coversWaypointCount == 0) {
		_coverRatioConst = 0;
		_coverRatio = 0;
	}

	_fleeWaypointsCount = 0;
	for (int i = 0; i < (int)_vm->_gameInfo->getFleeWaypointCount(); ++i) {
		if (_vm->_combat->_fleeWaypoints[i].type == waypointType && _vm->_combat->_fleeWaypoints[i].setId == actor->getSetId()) {
			++_fleeWaypointsCount;
		}
	}
	if (_fleeWaypointsCount == 0) {
		_fleeRatioConst = 0;
		_fleeRatio = 0;
	}
}

void ActorCombat::combatOff() {
	_active = false;
	reset();
}

void ActorCombat::tick() {
	static int processingCounter = 0;

	if (!_active || processingCounter > 0) {
		return;
	}

	Actor *actor = _vm->_actors[_actorId];
	Actor *enemy = _vm->_actors[_enemyId];

	if (actor->getSetId() != enemy->getSetId()) {
		actor->combatModeOff();
		return;
	}

	++processingCounter;

	_actorPosition = actor->getXYZ();
	_enemyPosition = enemy->getXYZ();

	if (_attackRatioConst >= 0) {
		_attackRatio = _attackRatioConst;
	} else {
		_attackRatio = calculateAttackRatio();
	}

	if (_vm->_combat->findCoverWaypoint(_waypointType, _actorId, _enemyId) != -1) {
		if (_coverRatioConst >= 0) {
			_coverRatio = _coverRatioConst;
		} else {
			_coverRatio = calculateCoverRatio();
		}
	} else {
		_coverRatio = 0;
	}

	if (_fleeRatioConst >= 0) {
		_fleeRatio = _fleeRatioConst;
	} else {
		_fleeRatio = calculateFleeRatio();
	}

	float dist = actor->distanceFromActor(_enemyId);
	int oldState = _state;

	if (_attackRatio < _fleeRatio || _attackRatio < _coverRatio) {
		if (_coverRatio >= _fleeRatio && _coverRatio >= _attackRatio) {
			_state = kActorCombatStateCover;
		} else {
			_state = kActorCombatStateFlee;
		}
	} else {
		if (_rangedAttack) {
			if (dist > _range) {
				_state = kActorCombatStateApproachRangedAttack;
			} else {
				if (actor->isObstacleBetween(_enemyPosition)) {
					_state = kActorCombatStateUncover;
				} else {
					_state = kActorCombatStateRangedAttack;
				}
			}
		} else {
			if (dist > 36.0f) {
				_state = kActorCombatStateApproachCloseAttack;
			} else {
				_state = kActorCombatStateCloseAttack;
			}
		}
	}

	if (enemy->isRetired()) {
		_state = kActorCombatStateIdle;
	}

	if (actor->getAnimationMode() == kAnimationModeHit || actor->getAnimationMode() == kAnimationModeCombatHit) {
		_state = kActorCombatStateIdle;
	} else {
		if (_state != oldState) {
			actor->stopWalking(false);
		}
	}
	switch (_state) {
	case kActorCombatStateCover:
		cover();
		break;
	case kActorCombatStateApproachCloseAttack:
		approachToCloseAttack();
		break;
	case kActorCombatStateUncover:
		uncover();
		break;
	case kActorCombatStateAim:
		aim();
		break;
	case kActorCombatStateRangedAttack:
		rangedAttack();
		break;
	case kActorCombatStateCloseAttack:
		closeAttack();
		break;
	case kActorCombatStateFlee:
		flee();
		break;
	case kActorCombatStateApproachRangedAttack:
		approachToRangedAttack();
		break;
	}
	--processingCounter;
}

void ActorCombat::hitAttempt() {
	Actor *actor = _vm->_actors[_actorId];
	Actor *enemy = _vm->_actors[_enemyId];

	if (_enemyId == kActorMcCoy && !_vm->playerHasControl() && !_unstoppable) {
		return;
	}

	if (actor->isRetired()) {
		return;
	}

	int attackCoefficient = 0;
	if (_rangedAttack) {
		attackCoefficient = _rangedAttack ? getCoefficientRangedAttack() : 0;
	} else {
		attackCoefficient = getCoefficientCloseAttack();
	}

	if (attackCoefficient == 0) {
		return;
	}

	int random = _vm->_rnd.getRandomNumberRng(1, 100);

	if (random <= attackCoefficient) {
		if (enemy->isWalking()) {
			enemy->stopWalking(true);
		}

		int sentenceId = _vm->_rnd.getRandomNumberRng(0, 1) ? 9000 : 9005;
		if (enemy->inCombat()) {
			enemy->changeAnimationMode(22, false);
		} else {
			enemy->changeAnimationMode(21, false);
		}

		int damage = 0;
		if (_rangedAttack) {
			damage = getDamageRangedAttack(random, attackCoefficient);
		} else {
			damage = getDamageCloseAttack(random, attackCoefficient);
		}

		int enemyHp = MAX(enemy->getCurrentHP() - damage, 0);
		enemy->setCurrentHP(enemyHp);

		if (enemyHp <= 0) {
			if (!enemy->isRetired()) {
				if (enemy->inCombat()) {
					enemy->changeAnimationMode(kAnimationModeCombatDie, false);
				} else {
					enemy->changeAnimationMode(kAnimationModeDie, false);
				}
				sentenceId = 9020;
			}
			enemy->retire(true, 6, 3, _actorId);
		}

		if (_enemyId == kActorMcCoy) {
			sentenceId += 900;
		}

		_vm->_audioSpeech->playSpeechLine(_enemyId, sentenceId, 75, enemy->soundPan(), 99);
	}
}

void ActorCombat::save(SaveFileWriteStream &f) {
	f.writeInt(_actorId);
	f.writeBool(_active);
	f.writeInt(_state);
	f.writeBool(_rangedAttack);
	f.writeInt(_enemyId);
	f.writeInt(_waypointType);
	f.writeInt(_damage);
	f.writeInt(_fleeRatio);
	f.writeInt(_coverRatio);
	f.writeInt(_attackRatio);
	f.writeInt(_fleeRatioConst);
	f.writeInt(_coverRatioConst);
	f.writeInt(_attackRatioConst);
	f.writeInt(_range);
	f.writeInt(_unstoppable);
	f.writeInt(_actorHp);
	f.writeInt(_fleeingTowards);
	f.writeVector3(_actorPosition);
	f.writeVector3(_enemyPosition);
	f.writeInt(_coversWaypointCount);
	f.writeInt(_fleeWaypointsCount);
}

void ActorCombat::load(SaveFileReadStream &f) {
	_actorId = f.readInt();
	_active = f.readBool();
	_state = f.readInt();
	_rangedAttack = f.readBool();
	_enemyId = f.readInt();
	_waypointType = f.readInt();
	_damage = f.readInt();
	_fleeRatio = f.readInt();
	_coverRatio = f.readInt();
	_attackRatio = f.readInt();
	_fleeRatioConst = f.readInt();
	_coverRatioConst = f.readInt();
	_attackRatioConst = f.readInt();
	_range = f.readInt();
	_unstoppable = f.readInt();
	_actorHp = f.readInt();
	_fleeingTowards = f.readInt();
	_actorPosition = f.readVector3();
	_enemyPosition = f.readVector3();
	_coversWaypointCount = f.readInt();
	_fleeWaypointsCount = f.readInt();
}

void ActorCombat::reset() {
	_active              = false;
	_actorId             = -1;
	_state               = -1;
	_rangedAttack        = false;
	_enemyId             = -1;
	_waypointType        = -1;
	_damage              = 0;
	_fleeRatio           = -1;
	_coverRatio          = -1;
	_attackRatio         = -1;
	_fleeRatioConst      = -1;
	_coverRatioConst     = -1;
	_attackRatioConst    = -1;
	_actorHp             = 0;
	_range               = 300;
	_unstoppable         = false;
	_actorPosition       = Vector3(0.0f, 0.0f, 0.0f);
	_enemyPosition       = Vector3(0.0f, 0.0f, 0.0f);
	_coversWaypointCount = 0;
	_fleeWaypointsCount  = 0;
	_fleeingTowards      = -1;
}

void ActorCombat::cover() {
	Actor *actor = _vm->_actors[_actorId];

	if (actor->isWalking()) {
		return;
	}

	if (actor->isObstacleBetween(_enemyPosition)) {
		faceEnemy();
		return;
	}

	int coverWaypointId = _vm->_combat->findCoverWaypoint(_waypointType, _actorId, _enemyId);
	if (coverWaypointId == -1) {
		_state = kActorCombatStateIdle;
	} else {
		actor->asyncWalkToXYZ(_vm->_combat->_coverWaypoints[coverWaypointId].position, 0, true, 0);
	}
}

void ActorCombat::approachToCloseAttack() {
	Actor *actor = _vm->_actors[_actorId];
	Actor *enemy = _vm->_actors[_enemyId];

	float dist = actor->distanceFromActor(_enemyId);
	if (dist > 36.0f) {
		if (!actor->isWalking() || enemy->isWalking()) {
			Vector3 target;
			if (findClosestPositionToEnemy(target)) {
				actor->asyncWalkToXYZ(target, 0, dist >= 240.0f, 0);
			} else {
				_state = kActorCombatStateCover;
			}
		}
	} else {
		if (actor->isWalking()) {
			actor->stopWalking(false);
		}
		faceEnemy();
		_state = kActorCombatStateCloseAttack;
	}
}

void ActorCombat::approachToRangedAttack() {
	Actor *actor = _vm->_actors[_actorId];
	Actor *enemy = _vm->_actors[_enemyId];

	float dist = actor->distanceFromActor(_enemyId);
	if (dist > _range) {
		if (!actor->isWalking() || enemy->isWalking()) {
			Vector3 target;
			if (findClosestPositionToEnemy(target)) {
				actor->asyncWalkToXYZ(target, 0, dist >= 240.0f, 0);
			} else {
				_state = kActorCombatStateCover;
			}
		}
	} else {
		if (actor->isWalking()) {
			actor->stopWalking(false);
		}
		faceEnemy();
		_state = kActorCombatStateRangedAttack;
	}
}

void ActorCombat::uncover() {
	Actor *actor = _vm->_actors[_actorId];
	Actor *enemy = _vm->_actors[_enemyId];

	if (actor->isObstacleBetween(_enemyPosition)) {
		actor->asyncWalkToXYZ(enemy->getXYZ(), 16, false, 0);
	} else {
		if (actor->isWalking()) {
			actor->stopWalking(false);
		}
		faceEnemy();
	}
}

void ActorCombat::aim() {
	Actor *actor = _vm->_actors[_actorId];

	if (actor->isObstacleBetween(_enemyPosition)) {
		if (actor->getAnimationMode() != kAnimationModeCombatIdle) {
			actor->changeAnimationMode(kAnimationModeCombatIdle, false);
		}
	} else {
		faceEnemy();
		if (actor->getAnimationMode() != kAnimationModeCombatAim) {
			actor->changeAnimationMode(kAnimationModeCombatAim, false);
		}
	}
}

void ActorCombat::rangedAttack() {
	Actor *actor = _vm->_actors[_actorId];

	if (actor->isObstacleBetween(_enemyPosition) || (actor->distanceFromActor(_enemyId) > _range)) {
		_state = kActorCombatStateApproachRangedAttack;
	} else {
		faceEnemy();
		if (actor->getAnimationMode() != kAnimationModeCombatAttack) {
			if (_enemyId != kActorMcCoy || _vm->playerHasControl() || _unstoppable) {
				actor->changeAnimationMode(kAnimationModeCombatAttack, false);
			}
		}
	}
}

void ActorCombat::closeAttack() {
	Actor *actor = _vm->_actors[_actorId];

	if (actor->isObstacleBetween(_enemyPosition) || (actor->distanceFromActor(_enemyId) > 36.0f)) {
		_state = kActorCombatStateApproachCloseAttack;
	} else {
		faceEnemy();
		if (actor->getAnimationMode() != kAnimationModeCombatAttack) {
			if (_enemyId != kActorMcCoy || _vm->playerHasControl() || _unstoppable) {
				actor->changeAnimationMode(kAnimationModeCombatAttack, false);
			}
		}
	}
}

void ActorCombat::flee() {
	Actor *actor = _vm->_actors[_actorId];

	if (_fleeingTowards != -1 && actor->isWalking()) {
		Vector3 fleeWaypointPosition = _vm->_combat->_fleeWaypoints[_fleeingTowards].position;
		if (distance(_actorPosition, fleeWaypointPosition) <= 12.0f) {
			_vm->_aiScripts->fledCombat(_actorId/*, _enemyId*/);
			actor->setSetId(kSetFreeSlotG);
			actor->combatModeOff();
			_fleeingTowards = -1;
		}
	} else {
		int fleeWaypointId = _vm->_combat->findFleeWaypoint(actor->getSetId(), _enemyId, _actorPosition);
		if (fleeWaypointId == -1) {
			_state = kActorCombatStateIdle;
		} else {
			Vector3 fleeWaypointPosition = _vm->_combat->_fleeWaypoints[fleeWaypointId].position;
			actor->asyncWalkToXYZ(fleeWaypointPosition, 0, true, 0);
			_fleeingTowards = fleeWaypointId;
		}
	}
}

void ActorCombat::faceEnemy() {
	_vm->_actors[_actorId]->setFacing(angle_1024(_actorPosition.x, _actorPosition.z, _enemyPosition.x, _enemyPosition.z), false);
}

int ActorCombat::getCoefficientCloseAttack() const{
	Actor *actor = _vm->_actors[_actorId];
	Actor *enemy = _vm->_actors[_enemyId];

	float distance = actor->distanceFromActor(_enemyId);

	if (distance > 36.0f) {
		return 0;
	}

	int aggressiveness = 0;
	if (enemy->isRunning()) {
		aggressiveness = 11;
	} else if (enemy->isMoving()) {
		aggressiveness = 22;
	} else {
		aggressiveness = 33;
	}

	aggressiveness += actor->getCombatAggressiveness() / 3;

	int angle = abs(actor->angleTo(_enemyPosition));

	if (angle > 128) {
		return false;
	}

	return aggressiveness + (abs(angle - 128) / 3.7f);
}

int ActorCombat::getCoefficientRangedAttack() const {
	Actor *actor = _vm->_actors[_actorId];
	Actor *enemy = _vm->_actors[_enemyId];

	if (actor->isObstacleBetween(_enemyPosition)) {
		return 0;
	}

	int distance = MIN(actor->distanceFromActor(_enemyId), 900.0f);

	int aggressiveness = 0;
	if (enemy->isRunning()) {
		aggressiveness = 10;
	} else if (enemy->isMoving()) {
		aggressiveness = 20;
	} else {
		aggressiveness = 30;
	}

	aggressiveness += actor->getCombatAggressiveness() / 5;
	return aggressiveness + abs((distance / 30) - 30) + actor->getIntelligence() / 5;
}

int ActorCombat::getDamageCloseAttack(int min, int max) const {
	if (_enemyId == kActorMcCoy && _vm->_settings->getDifficulty() == 0) {
		return _damage / 2;
	}
	if (_enemyId == kActorMcCoy && _vm->_settings->getDifficulty() == 2) {
		return _damage;
	}
	return ((MIN(max - min, 30) * 100.0f / 60.0f) + 50) * _damage / 100;
}

int ActorCombat::getDamageRangedAttack(int min, int max) const {
	if (_enemyId == kActorMcCoy && _vm->_settings->getDifficulty() == 0) {
		return _damage / 2;
	}
	if (_enemyId == kActorMcCoy && _vm->_settings->getDifficulty() == 2) {
		return _damage;
	}
	return ((MIN(max - min, 30) * 100.0f / 60.0f) + 50) * _damage / 100;
}

int ActorCombat::calculateAttackRatio() const {
	Actor *actor = _vm->_actors[_actorId];
	Actor *enemy = _vm->_actors[_enemyId];

	int aggressivenessFactor = actor->getCombatAggressiveness();
	int actorHpFactor        = actor->getCurrentHP();
	int enemyHpFactor        = 100 - enemy->getCurrentHP();
	int combatFactor         = enemy->inCombat() ? 0 : 100;
	int angleFactor          = (100 * abs(enemy->angleTo(_actorPosition))) / 512;
	int distanceFactor       = 2 * (50 - MIN(actor->distanceFromActor(_enemyId) / 12.0f, 50.0f));

	if (_rangedAttack) {
		return
			angleFactor          * 0.25f +
			combatFactor         * 0.05f +
			enemyHpFactor        * 0.20f +
			actorHpFactor        * 0.10f +
			aggressivenessFactor * 0.40f;
	} else {
		return
			distanceFactor       * 0.20f +
			angleFactor          * 0.10f +
			combatFactor         * 0.10f +
			enemyHpFactor        * 0.15f +
			actorHpFactor        * 0.15f +
			aggressivenessFactor * 0.30f;
	}
}

int ActorCombat::calculateCoverRatio() const {
	if (_coversWaypointCount == 0) {
		return 0;
	}

	Actor *actor = _vm->_actors[_actorId];
	Actor *enemy = _vm->_actors[_enemyId];

	int angleFactor          = 100 - (100 * abs(enemy->angleTo(_actorPosition))) / 512;
	int actorHpFactor        = 100 - actor->getCurrentHP();
	int enemyHpFactor        = enemy->getCurrentHP();
	int aggressivenessFactor = 100 - actor->getCombatAggressiveness();
	int distanceFactor       = 2 * MIN(actor->distanceFromActor(_enemyId) / 12.0f, 50.0f);

	if (_rangedAttack) {
		return
			angleFactor          * 0.40f +
			enemyHpFactor        * 0.05f +
			actorHpFactor        * 0.15f +
			aggressivenessFactor * 0.50f;
	} else {
		return
			distanceFactor       * 0.25f +
			angleFactor          * 0.20f +
			enemyHpFactor        * 0.05f +
			actorHpFactor        * 0.10f +
			aggressivenessFactor * 0.50f;
	}
}

int ActorCombat::calculateFleeRatio() const {
	if (_fleeWaypointsCount == 0) {
		return 0;
	}

	Actor *actor = _vm->_actors[_actorId];
	Actor *enemy = _vm->_actors[_enemyId];

	int aggressivenessFactor = 100 - actor->getCombatAggressiveness();
	int actorHpFactor        = 100 - actor->getCurrentHP();
	int combatFactor         = enemy->inCombat() ? 100 : 0;

	return
		combatFactor * 0.2f +
		actorHpFactor * 0.4f +
		aggressivenessFactor * 0.4f;
}

bool ActorCombat::findClosestPositionToEnemy(Vector3 &output) const {
	output = Vector3();

	Vector3 offsets[] = {
		Vector3(  0.0f, 0.0f, -28.0f),
		Vector3( 28.0f, 0.0f,   0.0f),
		Vector3(  0.0f, 0.0f,  28.0f),
		Vector3(-28.0f, 0.0f,   0.0f)
	};

	float min = -1.0f;

	for (int i = 0; i < 4; ++i) {
		Vector3 test = _enemyPosition + offsets[i];
		float dist = distance(_actorPosition, test);
		if ( min == -1.0f || dist < min) {
			if (!_vm->_sceneObjects->existsOnXZ(_actorId, test.x, test.z, true, true) && _vm->_scene->_set->findWalkbox(test.x, test.z) >= 0) {
				output = test;
				min = dist;
			}
		}
	}

	return min >= 0.0f;
}

} // End of namespace BladeRunner