/* 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/hashmap.h"
#include "common/path.h"
#include "common/file.h"
#include "common/debug.h"
#include "common/util.h"

#include "tetraedge/tetraedge.h"
#include "tetraedge/game/character.h"
#include "tetraedge/game/application.h"
#include "tetraedge/game/game.h"
#include "tetraedge/game/character_settings_xml_parser.h"
#include "tetraedge/te/te_model_animation.h"
#include "tetraedge/te/te_core.h"

namespace Tetraedge {

/*static*/ Common::HashMap<Common::String, Character::CharacterSettings> *Character::_globalCharacterSettings = nullptr;
/*static*/ Common::HashMap<Common::String, TeIntrusivePtr<TeModelAnimation>> *Character::_animCacheMap = nullptr;

// /*static*/ Common::Array<Character::AnimCacheElement> *Character::_animCache = nullptr;
// /*static*/ uint Character::_animCacheSize = 0;

void Character::CharacterSettings::clear() {
	_name.clear();
	_modelFileName.clear();
	_defaultScale = TeVector3f32();
	_idleAnimFileName.clear();
	_walkSettings.clear();
	_walkSpeed = 0.0f;
	_cutSceneCurveDemiPosition = TeVector3f32();
	_defaultEyes.clear();
	_defaultMouth.clear();
	_defaultBody.clear();
	_invertNormals = false;
}

void Character::WalkSettings::clear() {
	for (int i = 0; i < 4; i++) {
		_walkParts[i] = AnimSettings();
	}
}

Character::Character() : _walkCurveStart(0), _lastFrame(-1), _callbacksChanged(false),
_notWalkAnim(false), _returnToIdleAnim(false), _walkModeStr("Walk"),
_needsSomeUpdate(false), _positionFlag(false), _lookingAtTallThing(false),
_stepSound1("sounds/SFX/PAS_H_BOIS1.ogg"), _stepSound2("sounds/SFX/PAS_H_BOIS2.ogg"),
_freeMoveZone(nullptr), _animSoundOffset(0), _lastAnimFrame(0), _charLookingAt(nullptr),
_recallageY(true), _walkToFlag(false), _walkCurveEnd(0.0f), _walkCurveLast(0.0f),
_walkCurveLen(0.0f), _walkCurveIncrement(0.0f), _walkEndAnimG(false), _walkTotalFrames(0),
_walkCurveNextLength(0.0f), _walkedLength(0.0f), _walkLoopAnimLen(0.0f), _walkEndGAnimLen(0.0f),
_walkStartAnimLen(0.0f), _walkStartAnimFrameCount(0), _walkLoopAnimFrameCount(0),
_walkEndGAnimFrameCount(0), _hasAnchor(false), _charLookingAtOffset(0.0f) {
	_curModelAnim.setDeleteFn(&TeModelAnimation::deleteLaterStatic);
}

Character::~Character() {
	_model->setVisible(false);
	_model->bonesUpdatedSignal().remove(this, &Character::onBonesUpdate);
	deleteAnim();
	Game *game = g_engine->getGame();
	Common::Array<TeIntrusivePtr<TeModel>> &models = game->scene().models();
	for (uint i = 0; i < models.size(); i++) {
		if (models[i] == _model) {
			models.remove_at(i);
			break;
		}
	}
	removeAnim();
	for (uint s = 0; s < 2; s++) {
		if (!_shadowModel[s])
			continue;
		for (uint i = 0; i < models.size(); i++) {
			if (models[i] == _shadowModel[s]) {
				models.remove_at(i);
				break;
			}
		}
	}
}

/*static*/
void Character::cleanup() {
	if (_globalCharacterSettings)
		delete _globalCharacterSettings;
	_globalCharacterSettings = nullptr;
	animCacheFreeAll();
}

void Character::addCallback(const Common::String &animKey, const Common::String &fnName, float triggerFrame, float maxCalls) {
	Callback *c = new Callback();
	c->_luaFn = fnName;
	c->_lastCheckFrame = 0;
	c->_triggerFrame = (int)triggerFrame;
	c->_maxCalls = (int)maxCalls;
	// Slight difference here to orig (that sets -NAN) because of
	// the way this gets used later, setting large negative is more correct.
	c->_callsMade = (maxCalls == -1.0 ? -1e9 : 0.0f);

	if (g_engine->gameType() == TetraedgeEngine::kSyberia) {
		// Syberia 1 has slightly weird logic to decide what key to use.

		//
		// WORKAROUND: This callback seems to be set too late.. frame 31, but it
		// only gets to 15?  Some bug in the way anim blends hand off?
		// for scenes/CitSpace2/34230/Logic34230.lua
		//
		if (fnName == "ChangeClef" && c->_triggerFrame == 31)
			c->_triggerFrame = 15;

		const Common::Path animPath = _model->anim()->loadedPath();

		// Another difference.. the original messes with paths a bit - just
		// use the file name, since it's already limited by character.
		Common::String animName = animPath.baseName();
		if (animName.empty())
			animName = animPath.toString();

		if (_callbacks.contains(animName)) {
			_callbacks[animName].push_back(c);
		} else {
			Common::Path animKeyPath(animKey);
			Common::Array<Callback *> callbacks;
			callbacks.push_back(c);

			_callbacks.setVal(animKeyPath.baseName(), callbacks);
		}
	} else if (g_engine->gameType() == TetraedgeEngine::kSyberia2){
		// Syberia 2 is simpler, it always uses a lower-case version of the anim
		// file in the passed key.
		Common::String key = Common::Path(animKey).baseName();
		key.toLowercase();
		if (_callbacks.contains(key)) {
			_callbacks[key].push_back(c);
		} else {
			Common::Array<Callback *> callbacks;
			callbacks.push_back(c);
			_callbacks.setVal(key, callbacks);
		}
	} else {
		error("addCallback: Unsupported game type.");
	}
}

/*static*/
void Character::animCacheFreeAll() {
	/*
	if (_animCache) {
		for (const auto &entry : (*_animCache))
			_animCacheSize -= entry._size;
		delete _animCache;
		_animCache = nullptr;
	} */
	if (_animCacheMap) {
		delete _animCacheMap;
		_animCacheMap = nullptr;
	}
}

/*static*/
void Character::animCacheFreeOldest() {
	// Unused?
	//_animCacheSize -= _animCache[_animCache.size() - 1]._size;
	//_animCache.pop_back();
}

/*static*/
TeIntrusivePtr<TeModelAnimation> Character::animCacheLoad(const Common::Path &path) {
	const Common::String pathStr = path.toString();
	if (!_animCacheMap) {
		_animCacheMap = new Common::HashMap<Common::String, TeIntrusivePtr<TeModelAnimation>>();
	}
	if (_animCacheMap->contains(pathStr)) {
		// Copy from the cache (keep the cached instance clean)
		return new TeModelAnimation(*_animCacheMap->getVal(pathStr));
	}

	TeIntrusivePtr<TeModelAnimation> modelAnim = new TeModelAnimation();
	if (!modelAnim->load(path)) {
		warning("Failed to load anim %s", path.toString().c_str());
	}

	_animCacheMap->setVal(pathStr, modelAnim);
	return modelAnim;
}

float Character::animLength(const TeModelAnimation &modelanim, int bone, int lastframe) {
	int last = modelanim.lastFrame();
	if (lastframe > last)
		lastframe = last;
	int first = modelanim.firstFrame();
	const TeVector3f32 starttrans = translationVectorFromAnim(modelanim, bone, first);
	const TeVector3f32 endtrans = translationVectorFromAnim(modelanim, bone, lastframe);
	const TeVector3f32 secondtrans = translationVectorFromAnim(modelanim, bone, first + 1);
	return ((endtrans.z() - starttrans.z()) + secondtrans.z()) - starttrans.z();
}

float Character::animLengthFromFile(const Common::String &animname, uint32 *pframeCount, uint lastframe /* = 9999 */) {
	if (animname.empty()) {
		*pframeCount = 0;
		return 0.0f;
	}
	TeIntrusivePtr<TeModelAnimation> anim = _model->anim();
	if (!anim->loadedPath().toString().contains(animname)) {
		Common::Path animpath("models/Anims");
		animpath.joinInPlace(animname);
		anim = animCacheLoad(animpath);
		if (!anim)
			error("Character::animLengthFromFile couldn't load anim %s", animname.c_str());
	}

	// The "Pere" or "father" bone is the root.
	float animLen = animLength(*anim, anim->findBone(rootBone()), lastframe);
	int frameCount = anim->lastFrame() + 1 - anim->firstFrame();
	*pframeCount = frameCount;

	return animLen * _model->scale().z();
}

bool Character::blendAnimation(const Common::String &animname, float amount, bool repeat, bool returnToIdle) {
	Common::Path animpath("models/Anims");
	animpath.joinInPlace(animname);

	_notWalkAnim = !(animname.contains(_characterSettings._idleAnimFileName)
			|| animname.contains(walkAnim(WalkPart_Start))
			|| animname.contains(walkAnim(WalkPart_Loop))
			|| animname.contains(walkAnim(WalkPart_EndG))
			|| animname.contains(walkAnim(WalkPart_EndD)));

	if (_curModelAnim) {
		_curModelAnim->onFinished().remove(this, &Character::onModelAnimationFinished);
		_curModelAnim->unbind();
		_curModelAnim->reset();
	}

	_curModelAnim = animCacheLoad(animpath);
	assert(_curModelAnim);
	_curModelAnim->reset();
	_curModelAnim->onFinished().add(this, &Character::onModelAnimationFinished);

	_curModelAnim->bind(_model);
	_model->blendAnim(_curModelAnim, amount, repeat);
	_lastFrame = -1;
	_curModelAnim->play();
	_curAnimName = animname;
	_returnToIdleAnim = !repeat && returnToIdle;
	return true;
}

TeVector3f32 Character::correctPosition(const TeVector3f32 &pos) {
	bool flag;
	TeVector3f32 result = _freeMoveZone->correctCharacterPosition(pos, &flag, true);
	if (!flag)
		result.y() = _model->position().y();
	return result;
}

float Character::curveOffset() {
	return _walkCurveStart;
}

void Character::deleteAllCallback() {
	_callbacksChanged = true;
	for (auto &pair : _callbacks) {
		for (Callback *c : pair._value) {
			delete c;
		}
	}
	_callbacks.clear();
}

void Character::deleteAnim() {
	if (_curModelAnim) {
		_curModelAnim->onFinished().remove(this, &Character::onModelAnimationFinished);
		_curModelAnim->unbind();
		_curModelAnim->reset();
	}
	_model->removeAnim();
	_curModelAnim.release();
}

void Character::deleteCallback(const Common::String &key, const Common::String &fnName, float f) {
	_callbacksChanged = true;
	assert(_model->anim());
	Common::String animFile = _model->anim()->loadedPath().baseName();
	if (!_callbacks.contains(animFile))
		return;

	Common::Array<Callback *> &cbs = _callbacks.getVal(animFile);
	for (uint i = 0; i < cbs.size(); i++) {
		if (fnName.empty()) {
			delete cbs[i];
			// don't remove from array, clear at the end.
		} else if (cbs[i]->_luaFn == fnName) {
			if (f == -1 || cbs[i]->_triggerFrame == f) {
				delete cbs[i];
				cbs.remove_at(i);
				i--;
			}
		}
	}
	if (fnName.empty())
		cbs.clear();

	if (cbs.empty())
		_callbacks.erase(animFile);
}

void Character::endMove() {
	if (g_engine->getGame()->scene()._character == this)
		walkMode("Walk");

	_onFinishedSignal.call();
	stop();
}

const Character::WalkSettings *Character::getCurrentWalkFiles() {
	for (const auto & walkSettings : _characterSettings._walkSettings) {
		if (walkSettings._key == _walkModeStr)
			return &walkSettings._value;
	}
	return nullptr;
}

bool Character::isFramePassed(int frameno) {
	return (frameno > _lastAnimFrame && _model->anim()->curFrame2() >= frameno);
}

bool Character::isWalkEnd() {
	const Common::String animFile = _model->anim()->loadedPath().baseName();
	for (const auto & walkSettings : _characterSettings._walkSettings) {
		if (walkSettings._value._walkParts[WalkPart_EndD]._file.contains(animFile)
				|| walkSettings._value._walkParts[WalkPart_EndG]._file.contains(animFile))
			return true;
	}
	return false;
}

int Character::leftStepFrame(enum Character::WalkPart walkpart) {
	const Character::WalkSettings *settings = getCurrentWalkFiles();
	if (settings) {
		return settings->_walkParts[(int)walkpart]._stepLeft;
	}
	return -1;
}

int Character::rightStepFrame(enum Character::WalkPart walkpart) {
	const Character::WalkSettings *settings = getCurrentWalkFiles();
	if (settings) {
		return settings->_walkParts[(int)walkpart]._stepRight;
	}
	return -1;
}

bool Character::loadModel(const Common::String &mname, bool unused) {
	assert(_globalCharacterSettings);
	if (_model) {
		_model->bonesUpdatedSignal().remove(this, &Character::onBonesUpdate);
	}
	_model = new TeModel();
	_model->bonesUpdatedSignal().add(this, &Character::onBonesUpdate);

	if (!_globalCharacterSettings->contains(mname))
		return false;

	_characterSettings = _globalCharacterSettings->getVal(mname);
	_model->setTexturePath("models/Textures");
	_model->setEnableLights(true);
	if (!_model->load(Common::Path("models").join(_characterSettings._modelFileName))) {
		warning("Failed to load character model %s", _characterSettings._modelFileName.c_str());
		return false;
	}

	_model->setName(mname);
	_model->setScale(_characterSettings._defaultScale);
	if (_characterSettings._invertNormals)
		_model->invertNormals();

	for (auto &mesh : _model->meshes())
		mesh->setVisible(true);

	// Set all mouthes, eyes, etc not visible by default
	_model->setVisibleByName("_B_", false);
	_model->setVisibleByName("_Y_", false);
	_model->setVisibleByName("_M_", false);
	_model->setVisibleByName("_E_", false);

	// Note: game loops through "faces" here, but it only ever uses the default ones.
	_model->setVisibleByName(_characterSettings._defaultEyes, true);
	_model->setVisibleByName(_characterSettings._defaultMouth, true);
	_model->setVisibleByName(_characterSettings._defaultBody, true);

	setAnimation(_characterSettings._idleAnimFileName, true);

	_walkStartAnimLen = animLengthFromFile(walkAnim(WalkPart_Start), &_walkStartAnimFrameCount);
	_walkEndGAnimLen = animLengthFromFile(walkAnim(WalkPart_EndG), &_walkEndGAnimFrameCount);
	_walkLoopAnimLen = animLengthFromFile(walkAnim(WalkPart_Loop), &_walkLoopAnimFrameCount);

	if (g_engine->gameType() == TetraedgeEngine::kSyberia) {
		// Only Syberia 1 has the simple shadow.
		TeIntrusivePtr<Te3DTexture> shadow = Te3DTexture::makeInstance();
		TeCore *core = g_engine->getCore();
		shadow->load(core->findFile("models/Textures/simple_shadow_alpha.tga"));

		for (int i = 0; i < 2; i++) {
			TeModel *pmodel = new TeModel();
			_shadowModel[i] = pmodel;
			pmodel->setName("Shadow");
			Common::Array<TeVector3f32> arr;
			arr.resize(4);
			arr[0] = TeVector3f32(-60.0, 0.0, -60.0);
			arr[1] = TeVector3f32(-60.0, 0.0, 60.0);
			arr[2] = TeVector3f32(60.0, 0.0, -60.0);
			arr[3] = TeVector3f32(60.0, 0.0, 60.0);
			pmodel->setQuad(shadow, arr, TeColor(0xff, 0xff, 0xff, 0x50));
		}
	}
	return true;
}

/*static*/
bool Character::loadSettings(const Common::Path &path) {
	CharacterSettingsXmlParser parser;
	parser.setAllowText();
	if (_globalCharacterSettings)
		delete _globalCharacterSettings;
	_globalCharacterSettings = new Common::HashMap<Common::String, CharacterSettings>();
	parser.setCharacterSettings(_globalCharacterSettings);

	// WORKAROUND: This file contains invalid comments
	// eg, <!--------- and a comment-inside-a-comment.
	// patch them before parsing.
	Common::File xmlFile;
	if (!xmlFile.open(path))
		error("Character::loadSettings: Can't open %s", path.toString(Common::Path::kNativeSeparator).c_str());
	const int64 bufsize = xmlFile.size();
	char *buf = new char[bufsize+1];
	buf[bufsize] = '\0';
	xmlFile.read(buf, bufsize);
	Common::String fixedbuf(buf);
	delete [] buf;
	size_t offset = fixedbuf.find("------------");
	while (offset != Common::String::npos) {
		fixedbuf.replace(offset, 12, "--");
		offset = fixedbuf.find("------------");
	}

	// Big HACK: Remove the embedded comment in this config.
	offset = fixedbuf.find("<!--<walk>");
	if (offset != Common::String::npos) {
		size_t endOffset = fixedbuf.find(" -->", offset);
		if (endOffset != Common::String::npos) {
			size_t realEndOffset = fixedbuf.find("walk>-->", endOffset);
			if (realEndOffset  != Common::String::npos && realEndOffset > endOffset) {
				fixedbuf.replace(offset, endOffset - offset, "<!-- ");
			}
		}
	}

	if (!parser.loadBuffer((const byte *)fixedbuf.c_str(), bufsize))
		error("Character::loadSettings: Can't open %s", path.toString(Common::Path::kNativeSeparator).c_str());

	if (!parser.parse())
		error("Character::loadSettings: Can't parse %s", path.toString(Common::Path::kNativeSeparator).c_str());

	return true;
}

bool Character::onBonesUpdate(const Common::String &boneName, TeMatrix4x4 &boneMatrix) {
	if (!_model || !_model->anim())
		return false;

	Game *game = g_engine->getGame();
	if (boneName == rootBone()) {
		const Common::String animfile = _model->anim()->loadedPath().baseName();
		bool resetX = false;
		if (game->scene()._character == this) {
			for (const auto &walkSettings : _characterSettings._walkSettings) {
				if (walkSettings._key.contains("Walk") || walkSettings._key.contains("Jog")) {
					resetX |= (walkSettings._value._walkParts[0]._file.contains(animfile)
								|| walkSettings._value._walkParts[1]._file.contains(animfile)
								|| walkSettings._value._walkParts[2]._file.contains(animfile)
								|| walkSettings._value._walkParts[3]._file.contains(animfile));
				}
			}
			resetX |= _characterSettings._idleAnimFileName.contains(animfile);
		} else {
			resetX = (_characterSettings._idleAnimFileName.contains(animfile) ||
					  walkAnim(WalkPart_Start).contains(animfile) ||
					  walkAnim(WalkPart_Loop).contains(animfile) ||
					  walkAnim(WalkPart_EndD).contains(animfile) ||
					  walkAnim(WalkPart_EndG).contains(animfile));
		}
		if (resetX) {
			boneMatrix.setValue(0, 3, 0.0f);
			boneMatrix.setValue(2, 3, 0.0f);
		}
	}

	if (boneName.contains("Bip01 Head")) {
		if (_hasAnchor) {
			game->scene().currentCamera()->apply();
			_lastHeadRotation = _headRotation;
			TeQuaternion rot1 = TeQuaternion::fromAxisAndAngle(TeVector3f32(-1, 0, 0), _lastHeadRotation.getX());
			TeQuaternion rot2 = TeQuaternion::fromAxisAndAngle(TeVector3f32(0, 0, 1), _lastHeadRotation.getY());
			boneMatrix.rotate(rot1);
			boneMatrix.rotate(rot2);
		} else {
			// Return the head to the centerpoint if there is no anchor.
			const float lastHeadX = _lastHeadRotation.getX();
			const float headXAdjust = (lastHeadX > 0) ? -0.1 : 0.1;
			const float newX = (fabs(headXAdjust) > fabs(lastHeadX)) ? 0.0 : lastHeadX + headXAdjust;
			_lastHeadRotation.setX(newX);

			const float lastHeadY = _lastHeadRotation.getY();
			const float headYAdjust = (lastHeadY > 0) ? -0.1 : 0.1;
			const float newY = (fabs(headYAdjust) > fabs(lastHeadY)) ? 0.0 : lastHeadY + headYAdjust;
			_lastHeadRotation.setY(newY);

			_headRotation = _lastHeadRotation;

			const TeQuaternion rot1 = TeQuaternion::fromAxisAndAngle(TeVector3f32(-1, 0, 0), _lastHeadRotation.getX());
			const TeQuaternion rot2 = TeQuaternion::fromAxisAndAngle(TeVector3f32(0, 0, 1), _lastHeadRotation.getY());
			boneMatrix.rotate(rot1);
			boneMatrix.rotate(rot2);
			_lastHeadBoneTrans = boneMatrix.translation();
		}
	}

	if (boneName.contains("Bip01 L Foot") || boneName.contains("Bip01 R Foot")) {
		TeVector3f32 trans = boneMatrix.translation();
		trans.rotate(_model->rotation());
		const TeVector3f32 modelScale = _model->scale();
		trans.x() *= modelScale.x();
		trans.y() = 0.0;
		trans.z() *= modelScale.z();
		TeVector3f32 pos = _model->position() + trans;
		if (_freeMoveZone) {
			bool flag;
			pos = _freeMoveZone->correctCharacterPosition(pos, &flag, true);
		}
		int shadowNo = boneName.contains("Bip01 L Foot") ? 0 : 1;
		if (_shadowModel[shadowNo]) {
			_shadowModel[shadowNo]->setPosition(pos);
			_shadowModel[shadowNo]->setRotation(_model->rotation());
			_shadowModel[shadowNo]->setScale(_model->scale());
		}
	}

	// Move any objects attached to the bone
	for (Object3D *obj : game->scene().object3Ds()) {
		if (obj->_onCharName == _model->name() && boneName == obj->_onCharBone) {
			if (_model->anim()->curFrame2() >= obj->_startFrame
					&& _model->anim()->curFrame2() <= obj->_endFrame) {
				obj->model()->setVisible(true);

				if (!obj->_moveAnim._runTimer.running()) {
					obj->_lastMatrix = boneMatrix;
					obj->_lastMatrix.scale(obj->_objScale);
					obj->_lastMatrix.rotate(obj->_objRotation);
					obj->_lastMatrix.translate(obj->_objTranslation);
					obj->model()->forceMatrix(obj->_lastMatrix);
					obj->model()->setRotation(_model->rotation());
					obj->model()->setPosition(_model->position());
					obj->model()->setScale(_model->scale());
				} else {
					obj->model()->forceMatrix(obj->_lastMatrix);
					obj->model()->setRotation(_model->rotation());
					obj->model()->setPosition(_model->position() + obj->_curMovePos);
					obj->model()->setScale(_model->scale());
				}
			} else {
				obj->model()->setVisible(false);
			}
		}
	}

	return true;
}

bool Character::onModelAnimationFinished() {
	// this shouldn't happen but check to be sure..
	if (!_model || !_model->anim())
		return false;

	const Common::Path loadedPath = _model->anim()->loadedPath();
	const Common::String animfile = loadedPath.baseName();

	bool shouldAdjust = true;
	for (const auto &unrecal : g_engine->getApplication()->unrecalAnims()) {
		if (animfile.contains(unrecal))
			shouldAdjust = false;
	}

	Game *game = g_engine->getGame();
	bool isWalkAnim = false;
	if (game->scene()._character == this) {
		for (const auto &walkSettings : _characterSettings._walkSettings) {
			if (walkSettings._key.contains("Walk") || walkSettings._key.contains("Jog")) {
				isWalkAnim |= (walkSettings._value._walkParts[0]._file.contains(animfile)
							 || walkSettings._value._walkParts[1]._file.contains(animfile)
							 || walkSettings._value._walkParts[2]._file.contains(animfile)
							 || walkSettings._value._walkParts[3]._file.contains(animfile));
			}
		}
		isWalkAnim |= animfile.contains(_characterSettings._idleAnimFileName);
	} else {
		isWalkAnim = (_characterSettings._idleAnimFileName.contains(animfile)
					|| walkAnim(WalkPart_Start).contains(animfile)
					|| walkAnim(WalkPart_Loop).contains(animfile)
					|| walkAnim(WalkPart_EndD).contains(animfile)
					|| walkAnim(WalkPart_EndG).contains(animfile));
	}

	if (!isWalkAnim && shouldAdjust) {
		int pereBone = _curModelAnim->findBone(rootBone());
		const TeTRS endTRS = trsFromAnim(*_curModelAnim, pereBone, _curModelAnim->lastFrame());
		TeVector3f32 trans = endTRS.getTranslation();
		trans.x() = -trans.x();

		TeVector3f32 newpos;
		if (!_recallageY) {
			const TeTRS startTRS = trsFromAnim(*_curModelAnim, pereBone, _curModelAnim->firstFrame());
			trans = trans - startTRS.getTranslation();
			const TeTRS nearEndTRS = trsFromAnim(*_curModelAnim, pereBone, _curModelAnim->lastFrame() - 1);
			trans  = trans + (endTRS.getTranslation() - nearEndTRS.getTranslation());
			newpos = _model->worldTransformationMatrix() * trans;
		} else if (!_freeMoveZone) {
			trans.x() = -trans.x();
			trans.y() = 0.0;
			newpos = _model->worldTransformationMatrix() * trans;
		} else {
			newpos = correctPosition(_model->worldTransformationMatrix() * trans);
		}
		_model->setPosition(newpos);
	}

	if (game->scene()._character == this) {
		_characterAnimPlayerFinishedSignal.call(loadedPath.toString());
	} else {
		_onCharacterAnimFinishedSignal.call(_model->name());
	}

	Common::Path setAnimNamePath(_setAnimName);
	Common::String setAnimNameFile = setAnimNamePath.baseName();
	Common::String loadedPathFile = loadedPath.baseName();
	if (_returnToIdleAnim && loadedPathFile.contains(setAnimNameFile)) {
		_notWalkAnim = false;
		_returnToIdleAnim = false;
		setAnimation(_characterSettings._idleAnimFileName, true);
	}

	return false;
}

void Character::permanentUpdate() {
	assert(_model->anim());
	const Common::Path &animPath = _model->anim()->loadedPath();
	int curFrame = _model->anim()->curFrame2();
	Game *game = g_engine->getGame();
	_callbacksChanged = false;
	// Diverge from original - just use filename for anim callbacks as the
	// original does werid things with paths.
	Common::String animFile = animPath.baseName();
	if (g_engine->gameType() == TetraedgeEngine::kSyberia2)
		animFile.toLowercase();

	//if (!_callbacks.empty())
	//	debug("%s: check cbs for %s frame %d speed %.02f", _model->name().c_str(),
	//			animFile.c_str(), curFrame, _model->anim()->speed());

	if (_callbacks.contains(animFile)) {
		Common::Array<Callback *> &cbs = _callbacks.getVal(animFile);
		for (Callback *cb : cbs) {
			//debug("%s: check cb for %s frame %d against %d. speed %.02f", _model->name().c_str(),
			//		animFile.c_str(), curFrame, cb->_triggerFrame, _model->anim()->speed());
			if (cb->_triggerFrame > cb->_lastCheckFrame && curFrame >= cb->_triggerFrame){
				int callsMade = cb->_callsMade;
				cb->_callsMade++;
				if (callsMade >= cb->_maxCalls)
					continue;
				cb->_lastCheckFrame = curFrame;
				game->luaScript().execute(cb->_luaFn);
				if (_callbacksChanged)
					break;
			}
			cb->_lastCheckFrame = curFrame;
		}
	}

	if (animFile.contains("ka_esc_h")) {
		if (_lastAnimFrame < 7 && _model->anim()->curFrame2() > 6) {
			game->playSound("sounds/SFX/PAS_F_PAVE1.ogg", 1, 1.0f);
		} else if (_lastAnimFrame < 22 && _model->anim()->curFrame2() > 21) {
			game->playSound("sounds/SFX/PAS_F_PAVE2.ogg", 1, 1.0f);
		}
	} else if (animFile.contains("ka_esc_b")) {
		if (_lastAnimFrame < 12 && _model->anim()->curFrame2() > 11) {
			game->playSound("sounds/SFX/PAS_F_PAVE1.ogg", 1, 1.0f);
		} else if (_lastAnimFrame < 27 && _model->anim()->curFrame2() > 26) {
			game->playSound("sounds/SFX/PAS_F_PAVE2.ogg", 1, 1.0f);
		}
	}
	updateAnimFrame();
}

void Character::placeOnCurve(TeIntrusivePtr<TeBezierCurve> &curve) {
	_curve = curve;
	updatePosition(_walkCurveStart);
}

void Character::removeAnim() {
	if (_curModelAnim) {
		_curModelAnim->onFinished().remove(this, &Character::onModelAnimationFinished);
		_curModelAnim->unbind();
		_curModelAnim->reset();
	}
	_model->removeAnim();
	if (_curModelAnim) {
		_curModelAnim.release();
	}
}

void Character::removeFromCurve() {
	_curve.release();
}

Common::String Character::rootBone() const {
	if (g_engine->gameType() != TetraedgeEngine::kSyberia2 || _model->name() != "Youki")
		return "Pere";
	else
		return "Bip01";
}

bool Character::setAnimation(const Common::String &aname, bool repeat, bool returnToIdle, bool unused, int startFrame, int endFrame) {
	if (aname.empty())
		return false;

	Common::Path animPath("models/Anims");
	animPath.joinInPlace(aname);
	bool isWalkAnim = (aname.contains(_characterSettings._idleAnimFileName) ||
					  aname.contains(walkAnim(WalkPart_Start)) ||
					  aname.contains(walkAnim(WalkPart_Loop)) ||
					  aname.contains(walkAnim(WalkPart_EndD)) ||
					  aname.contains(walkAnim(WalkPart_EndG)));
	_notWalkAnim = !isWalkAnim;

	if (_curModelAnim) {
		_curModelAnim->onFinished().remove(this, &Character::onModelAnimationFinished);
		_curModelAnim->unbind();
		_curModelAnim->reset();
	}

	_curModelAnim = animCacheLoad(animPath);
	_curModelAnim->reset();
	_curModelAnim->onFinished().add(this, &Character::onModelAnimationFinished);
	_curModelAnim->bind(_model);
	_curModelAnim->setFrameLimits(startFrame, endFrame);
	_model->setAnim(_curModelAnim, repeat);
	_lastFrame = -1;
	_curModelAnim->play();
	_setAnimName = aname;
	_curAnimName = aname;
	_returnToIdleAnim = !repeat && returnToIdle;

	return true;
}

void Character::setAnimationSound(const Common::String &sname, uint offset) {
	warning("TODO: Set field 0x2f8 to 0 in Character::setAnimationSound.");
	_animSound = sname;
	_animSoundOffset = offset;
}

void Character::setCurveOffset(float offset) {
	_walkCurveStart = offset;
	updatePosition(offset);
}

void Character::setFreeMoveZone(TeFreeMoveZone *zone) {
	_freeMoveZone = zone;
}

void Character::setStepSound(const Common::String &stepSound1, const Common::String &stepSound2) {
	_stepSound1 = stepSound1;
	_stepSound2	= stepSound2;
}

bool Character::setShadowVisible(bool visible) {
	if (_shadowModel[0]) {
		_shadowModel[0]->setVisible(visible);
		_shadowModel[1]->setVisible(visible);
	}
	return false;
}

float Character::speedFromAnim(double msFromStart) {
	if (!_model)
		return 0.0f;

	TeIntrusivePtr<TeModelAnimation> modelAnim;
	if (_model->boneBlenders().empty()) {
		modelAnim = _model->anim();
	} else {
		modelAnim = _model->boneBlenders().back()->_anim;
	}

	if (!modelAnim)
		return 0.0f;

	const int pereBone = modelAnim->findBone(rootBone());
	int curFrame = modelAnim->calcCurrentFrame(msFromStart);

	float result;
	if (_lastFrame == -1) {
		const TeVector3f32 nowvec = translationVectorFromAnim(*modelAnim, pereBone, 0);
		const TeVector3f32 lastvec = translationVectorFromAnim(*modelAnim, pereBone, 1);
		result = lastvec.z() - nowvec.z();
	} else {
		const TeVector3f32 nowvec = translationVectorFromAnim(*modelAnim, pereBone, curFrame);
		const TeVector3f32 lastvec = translationVectorFromAnim(*modelAnim, pereBone, _lastFrame);
		result = nowvec.z() - lastvec.z();
		if (curFrame < _lastFrame) {
			result += animLength(*modelAnim, pereBone, 9999);
		}
	}
	_lastFrame = curFrame;
	result *= _model->scale().z();
	return result;
}

float Character::translationFromAnim(const TeModelAnimation &anim, int bone, int frame) {
	return translationVectorFromAnim(anim, bone, frame).z();
}

TeVector3f32 Character::translationVectorFromAnim(const TeModelAnimation &anim, int bone, int frame) {
	const TeTRS trs = trsFromAnim(anim, bone, frame);
	return trs.getTranslation();
}

TeTRS Character::trsFromAnim(const TeModelAnimation &anim, int bone, int frame) {
	if (bone == -1)
		return TeTRS();

	return anim.getTRS(bone, frame, false);
}

void Character::update(double msFromStart) {
	if (!_curve || !_runTimer.running())
		return;

	_walkCurveNextLength = speedFromAnim(msFromStart) * _walkCurveIncrement + _walkCurveNextLength;

	if (_curve->controlPoints().size() < 2) {
		blendAnimation(_characterSettings._idleAnimFileName, 0.0667f, true, false);
		endMove();
		return;
	}

	const float baseAngle = (_walkCurveStart > _walkCurveEnd ? M_PI : 0);
	const float sign = (_walkCurveStart > _walkCurveEnd ? -1 : 1);

	updatePosition(_walkCurveLast);

	float lastWalkedLength = _walkedLength;
	TeVector3f32 lastNextPos = _model->position();
	TeVector3f32 nextPos = _model->position();
	TeVector3f32 newPos = _model->position();
	float lastOffset = _walkCurveLast;

	// First do a coarse search for the position, then back up 1 step and do a finer
	// search.

	const float coarseStep = (4.0 / _curve->numIterations()) * sign;
	const float fineStep = (1.0 / _curve->numIterations()) * sign;

	float offset = _walkCurveLast;
	while (_walkedLength < _walkCurveNextLength) {
		lastOffset = offset;
		lastWalkedLength = _walkedLength;
		lastNextPos = nextPos;

		offset = CLIP(lastOffset + coarseStep, 0.0f, 1.0f);

		newPos = _curve->retrievePoint(offset) + _curveStartLocation;
		const TeVector2f32 dist = TeVector2f32(nextPos.x(), nextPos.z()) - TeVector2f32(newPos.x(), newPos.z());
		_walkedLength += dist.length();

		nextPos = newPos;
		if (offset == 1.0 || offset == 0.0)
			break;
	}

	_walkedLength = lastWalkedLength;
	nextPos = lastNextPos;
	offset = lastOffset;

	while (_walkedLength < _walkCurveNextLength) {
		offset = CLIP(offset + fineStep, 0.0f, 1.0f);

		newPos = _curve->retrievePoint(offset) + _curveStartLocation;
		const TeVector2f32 dist = TeVector2f32(nextPos.x(), nextPos.z()) - TeVector2f32(newPos.x(), newPos.z());
		_walkedLength += dist.length();

		nextPos = newPos;
		if (offset == 1.0 || offset == 0.0)
			break;
	}

	if (_freeMoveZone) {
		bool correctflag;
		newPos = _freeMoveZone->correctCharacterPosition(newPos, &correctflag, true);
	}

	//debug("Character::update %4d %.04f %s -> %s %.4f", (int)msFromStart, offset, _model->position().dump().c_str(),
	//		newPos.dump().c_str(), (newPos - _model->position()).length());

	_walkCurveLast = offset;
	_model->setPosition(newPos);

	TeVector3f32 t1;
	TeVector3f32 t2;
	_curve->pseudoTangent(offset, t1, t2);
	const TeVector3f32 normalizedTangent = (t2 - t1).getNormalized();
	float angle = TeVector3f32(0.0, 0.0, 1.0).dotProduct(normalizedTangent);
	angle = acos(angle);
	TeVector3f32 crossprod = TeVector3f32::crossProduct(TeVector3f32(0.0, 0.0, 1.0), normalizedTangent);
	if (crossprod.y() >= 0.0f) {
		angle = -angle;
	}
	//debug("update: curve offset %f - angle %f (base %f)", offset, angle, baseAngle);
	TeQuaternion rot = TeQuaternion::fromAxisAndAngle(TeVector3f32(0.0, 1.0, 0.0), baseAngle + angle);
	_model->setRotation(rot);

	const Common::String endGAnim = walkAnim(WalkPart_EndG);
	if (_walkCurveLast == _walkCurveEnd || fabs(_walkCurveEnd - _walkCurveStart) < fabs(_walkCurveLast - _walkCurveStart)) {
		if (_walkToFlag) {
			_walkToFlag = false;
			endMove();
		}
		if (endGAnim.empty()) {
			blendAnimation(_characterSettings._idleAnimFileName, 0.0667f, true, false);
			endMove();
		}
	}

	if (!endGAnim.empty() && _curAnimName == walkAnim(WalkPart_Loop) &&
		   ((_curModelAnim->speed() * (msFromStart / 1000.0)) >= _walkTotalFrames)) {
		if (_walkToFlag) {
			_walkToFlag = false;
			endMove();
		} else {
			if (_walkEndAnimG)
				setAnimation(walkAnim(WalkPart_EndG), false);
			else
				setAnimation(walkAnim(WalkPart_EndD), false);
		}
	}

	// Note:
	// The game does a bunch of extra things here (line 252 on)
	// that seem to have no actual effect??

	updateAnimFrame();
}

void Character::updateAnimFrame() {
	if (_model->anim()) {
		_lastAnimFrame = _model->anim()->curFrame2();
	}
}

void Character::updatePosition(float curveOffset) {
	assert(_curve);
	if (!_curve->controlPoints().empty()) {
		TeVector3f32 pt = _curve->retrievePoint(curveOffset) + _curveStartLocation;
		if (_freeMoveZone) {
			bool flag;
			pt = _freeMoveZone->correctCharacterPosition(pt, &flag, true);
		}
		_model->setPosition(pt);
	}
}

Common::String Character::walkAnim(Character::WalkPart part) {
	Common::String result;
	const Character::WalkSettings *settings = getCurrentWalkFiles();
	if (settings) {
		return settings->_walkParts[(int)part]._file;
	}
	return result;
}

void Character::walkMode(const Common::String &mode) {
	if (_walkModeStr != mode)
		_walkModeStr = mode;
	_walkStartAnimLen = animLengthFromFile(walkAnim(WalkPart_Start), &_walkStartAnimFrameCount);
	_walkEndGAnimLen = animLengthFromFile(walkAnim(WalkPart_EndG), &_walkEndGAnimFrameCount);
	_walkLoopAnimLen = animLengthFromFile(walkAnim(WalkPart_Loop), &_walkLoopAnimFrameCount);
}

void Character::walkTo(float curveEnd, bool walkFlag) {
	_walkToFlag = walkFlag;
	stop();
	_walkCurveEnd = curveEnd;
	_walkCurveLast = _walkCurveStart;
	_walkCurveNextLength = 0.0f;
	_walkedLength = 0.0f;
	if (!_curve)
		warning("_curve not set in Character::walkTo");
	if (_curve && _curve->controlPoints().size()) {
		const float walkEndLen = (walkFlag ? 0 : _walkEndGAnimLen);
		_walkCurveLen = _curve->length();
		_walkEndAnimG = false;
		const float nloops = (_walkCurveLen - (walkEndLen + _walkStartAnimLen)) / _walkLoopAnimLen;
		float animLen;
		if (nloops >= 0) {
			Game *game = g_engine->getGame();
			if (game->scene()._character == this && _walkModeStr == "Walk") {
				int looplen = (int)(nloops * _walkLoopAnimFrameCount);
				int repeats = looplen / _walkLoopAnimFrameCount;
				uint32 remainder = looplen % _walkLoopAnimFrameCount;

				uint framecounts[4];

				if (repeats == 0)
					framecounts[0] = INT_MAX;
				else
					framecounts[0] = (repeats - 1) * _walkLoopAnimFrameCount + 29;

				framecounts[1] = _walkLoopAnimFrameCount * repeats + 13;
				framecounts[2] = _walkLoopAnimFrameCount * repeats + 29;
				framecounts[3] = _walkLoopAnimFrameCount * (repeats + 1) + 13;

				for (int i = 0; i < 4; i++) {
					framecounts[i] = abs((int)(framecounts[i] - (int)(nloops * _walkLoopAnimFrameCount)));
				}

				int minoffset = 0;
				for (int i = 0; i < 4; i++) {
					if (framecounts[i] < framecounts[minoffset])
						minoffset = i;
				}

				switch(minoffset) {
				case 0:
					remainder = 29;
					_walkEndAnimG = true;
					repeats--;
					break;
				case 1:
					remainder = 13;
					break;
				case 2:
					remainder = 29;
					_walkEndAnimG = true;
					break;
				case 3:
					remainder = 13;
					repeats++;
					break;
				}
				_walkTotalFrames = _walkLoopAnimFrameCount * repeats + _walkStartAnimFrameCount + remainder;
				const float loopAnimLen = animLengthFromFile(walkAnim(WalkPart_Loop), &remainder, remainder);
				_walkCurveIncrement = _walkCurveLen / (repeats * _walkLoopAnimLen + walkEndLen + _walkStartAnimLen + loopAnimLen);
				play();
				return; // NOTE: early return here.
			} else {
				// Run or NPC walk
				double intpart;
				double remainder = modf(nloops, &intpart);
				if (remainder >= 0.5) {
					_walkEndAnimG = true;
					intpart += 0.75;
				} else {
					intpart += 0.25;
				}
				_walkTotalFrames = (int)(_walkLoopAnimFrameCount * intpart) + _walkStartAnimFrameCount;
				animLen = walkEndLen + (float)_walkStartAnimLen + intpart * _walkLoopAnimLen;
			}
		} else {
			_walkTotalFrames = _walkStartAnimFrameCount;
			animLen = (float)(_walkStartAnimLen + _walkEndGAnimLen);
		}
		_walkCurveIncrement = _walkCurveLen / animLen;
	}
	play();
}

Character::Water::Water() {
	_model = new TeModel();
	_model->setName("Water");
	TeIntrusivePtr<TeCamera> cam = g_engine->getGame()->scene().currentCamera();
	if (!cam)
		error("No active camera when constructing water");
	TeMatrix4x4 camMatrix = cam->worldTransformationMatrix();
	Common::Array<TeVector3f32> quad;
	quad.resize(4);
	quad[0] = camMatrix.mult3x3(TeVector3f32(-0.1f, 0.0f,  0.1f));
	quad[1] = camMatrix.mult3x3(TeVector3f32( 0.1f, 0.0f,  0.1f));
	quad[2] = camMatrix.mult3x3(TeVector3f32(-0.1f, 0.0f, -0.1f));
	quad[3] = camMatrix.mult3x3(TeVector3f32( 0.1f, 0.0f, -0.1f));
	const TeQuaternion noRot = TeQuaternion::fromEuler(TeVector3f32(0, 0, 0));
	TeIntrusivePtr<Te3DTexture> tex = Te3DTexture::makeInstance();
	tex->load(g_engine->getCore()->findFile("texturesIngame/EauOndine1.tga"));
	_model->setQuad(tex, quad, TeColor(255, 0, 0, 0));
	_model->setRotation(noRot);
	_model->setScale(TeVector3f32(0.5, 0.5, 0.5));
	_colorAnim._duration = 2000.0f;
	TeColor col = _model->color();
	col.a() = 100;
	_colorAnim._startVal = col;
	col.a() = 0;
	_colorAnim._endVal = col;
	Common::Array<float> curve;
	curve.push_back(0);
	curve.push_back(1);
	_colorAnim.setCurve(curve);
	_colorAnim._callbackObj = _model.get();
	_colorAnim._callbackMethod = &TeModel::setColor;
	_colorAnim.play();
	_scaleAnim._duration = 2000.0f;
	_scaleAnim._startVal = _model->scale();
	_scaleAnim._endVal = TeVector3f32(3.0f, 3.0f, 3.0f);
	_scaleAnim.setCurve(curve);
	_scaleAnim._callbackObj = _model.get();
	_scaleAnim._callbackMethod = &TeModel::setScale;
}

} // end namespace Tetraedge
