// ==============================================================
//	This file is part of Glest (www.glest.org)
//
//	Copyright (C) 2001-2008 Martiño Figueroa
//
//	You can redistribute this code 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
// ==============================================================

#include "game_camera.h"

#include <cstdlib>

#include "config.h"
#include "game_constants.h"
#include "xml_parser.h"
#include "conversion.h"
#include "platform_common.h"
#include "leak_dumper.h"
#include "randomgen.h"


using namespace Shared::Graphics;
using Shared::Xml::XmlNode;
using namespace Shared::Util;

namespace Glest { namespace Game {

// =====================================================
// 	class GameCamera
// =====================================================

//static std::map<float, std::map<float, std::map<Vec3f, Quad2i> > > cacheVisibleQuad;

// ================== PUBLIC =====================

const float GameCamera::startingVAng= -60.f;
const float GameCamera::startingHAng= 0.f;
const float GameCamera::vTransitionMult= 0.125f;
const float GameCamera::hTransitionMult= 0.125f;
const float GameCamera::defaultHeight= 20.f;
const float GameCamera::centerOffsetZ= 8.0f;
const float GameCamera::shakeDist= 50.f;

// ================= Constructor =================

GameCamera::GameCamera() : pos(0.f, defaultHeight, 0.f),
		destPos(0.f, defaultHeight, 0.f), destAng(startingVAng, startingHAng) {
	//Config &config = Config::getInstance();
	calculatedDefault=defaultHeight;
    state= sGame;

    cacheVisibleQuad.clear();
    //MaxVisibleQuadItemCache = config.getInt("MaxVisibleQuadItemCache",intToStr(-1).c_str());
    MaxVisibleQuadItemCache = -1;
    //if(Config::getInstance().getBool("DisableCaching","false") == true) {
    //	MaxVisibleQuadItemCache = 0;
    //}

	//config
	speed= Config::getInstance().getFloat("CameraMoveSpeed","15") / GameConstants::cameraFps;
	clampBounds= !Config::getInstance().getBool("PhotoMode");
	clampDisable = false;

	vAng= startingVAng;
    hAng= startingHAng;

    rotate=0;

	move= Vec3f(0.f);

	shakeDecrement=0.f;
	currentShakeIntensity=0;
	shakeOffset= Vec2f(0.f);

	//maxRenderDistance = Config::getInstance().getFloat("RenderDistanceMax","64");
	maxHeight = Config::getInstance().getFloat("CameraMaxDistance","20");
	minHeight = Config::getInstance().getFloat("CameraMinDistance","7");
	//maxCameraDist = maxHeight;
	//minCameraDist = minHeight;
	minVAng = -Config::getInstance().getFloat("CameraMaxYaw","77.5");
	maxVAng = -Config::getInstance().getFloat("CameraMinYaw","20");
	fov = Config::getInstance().getFloat("CameraFov","45");

	lastHAng=0;
	lastVAng=0;
	limitX=0;
	limitY=0;
}

GameCamera::~GameCamera() {
	cacheVisibleQuad.clear();
}

std::string GameCamera::getCameraMovementKey() const {
	char szBuf[8096]="";
	snprintf(szBuf,8096,"%s_%f_%f_%f_%s,%f",pos.getString().c_str(),hAng,vAng,rotate,move.getString().c_str(),fov);
	return szBuf;
}

void GameCamera::setMaxHeight(float value) {
	if(value < 0) {
		maxHeight = Config::getInstance().getFloat("CameraMaxDistance","20");
	}
	else {
		maxHeight = value;
	}
}

void GameCamera::setCalculatedDefault(float calculatedDefault){
	this->calculatedDefault= calculatedDefault;
	if(maxHeight>0 && maxHeight<calculatedDefault){
		setMaxHeight(calculatedDefault);
	}
	resetPosition();
}

void GameCamera::init(int limitX, int limitY){
	this->limitX= limitX;
	this->limitY= limitY;
}

// ==================== Misc =====================

void GameCamera::setPos(Vec2f pos){
	this->pos= Vec3f(pos.x, this->pos.y, pos.y);
	clampPosXZ(0.0f, (float)limitX, 0.0f, (float)limitY);
	destPos.x = pos.x;
	destPos.z = pos.y;
}

void GameCamera::setPos(Vec3f pos){
	this->pos= pos;
	//clampPosXZ(0.0f, (float)limitX, 0.0f, (float)limitY);
	destPos.x = pos.x;
	destPos.y = pos.y;
	destPos.z = pos.z;
}

void GameCamera::shake(int shakeDuration, int shakeStartIntensity , bool cameraDistanceAffected, Vec3f unitVector) {
	float currentDurationLeft=0;
	float incomingShakeIntensity=((float) shakeStartIntensity)/1000;

	// calculate the shake duration which is left
	if(this->currentShakeIntensity > 0.f && this->shakeDecrement != 0.f){
		currentDurationLeft=this->currentShakeIntensity / this->shakeDecrement;
	}

	// reduce new shake effect camera distance related
	if (cameraDistanceAffected) {
		incomingShakeIntensity=incomingShakeIntensity*(1.f-unitVector.dist(getPos())/GameCamera::shakeDist);
	}

	// add camera shake effect to current one ( if exsists ).
	if(this->currentShakeIntensity>0){
		this->currentShakeIntensity = this->currentShakeIntensity+incomingShakeIntensity ;
	}
	else {
		this->currentShakeIntensity = incomingShakeIntensity ;
	}

	// use bigger shakeDuration to calculate new shakeDecrement
	if(currentDurationLeft < shakeDuration){
		this->shakeDecrement = currentShakeIntensity / ((float) shakeDuration);
	}
	else if(currentDurationLeft!=0.0f)
	{
		this->shakeDecrement = currentShakeIntensity / ((float) currentDurationLeft);
	}
}

void GameCamera::shakeCamera(){
	//RandomGen random;
	if(currentShakeIntensity > 0.f) {
//		pos.x += (((float) (rand() % 50)) / 50.f - 0.5f) * currentShakeIntensity;
//		pos.z += (((float) (rand() % 50)) / 50.f - 0.5f) * currentShakeIntensity;

		shakeOffset.x = (((float) (rand() % 50)) / 50.f - 0.5f) * currentShakeIntensity;
		shakeOffset.y = (((float) (rand() % 50)) / 50.f - 0.5f) * currentShakeIntensity;
		currentShakeIntensity -= shakeDecrement;
	}
}

void GameCamera::update(){
	//move XZ
	if(move.z){
        moveForwardH(speed * move.z, 0.9f);
	}
	if(move.x){
        moveSideH(speed * move.x, 0.9f);
	}

	//free state
	if(state==sFree ){
		if(std::fabs(rotate) == 1){
			rotateHV(speed*5*rotate, 0);
		}
		if(move.y>0){
			moveUp(speed * move.y);
			if(clampDisable == false && clampBounds && pos.y<maxHeight){
				rotateHV(0.f, -speed * 1.7f * move.y);
			}
		}
		if(move.y<0){
			moveUp(speed * move.y);
			if(clampDisable == false && clampBounds && pos.y>minHeight){
				rotateHV(0.f, -speed * 1.7f * move.y);
			}
		}
	}

	//game state
	if(abs(destAng.x - vAng) > 0.01f) {
		vAng+= (destAng.x - vAng) * hTransitionMult;
	}
	if(abs(destAng.y - hAng) > 0.01f) {
		if(abs(destAng.y - hAng) > 180) {
			if(destAng.y > hAng) {
				hAng+= (destAng.y - hAng - 360) * vTransitionMult;
			} else {
				hAng+= (destAng.y - hAng + 360) * vTransitionMult;
			}
		} else {
			hAng+= (destAng.y - hAng) * vTransitionMult;
		}
	}
	if(abs(destPos.x - pos.x) > 0.01f) {
		pos.x += (destPos.x - pos.x) / 32.0f;
	}
	if(abs(destPos.y - pos.y) > 0.01f) {
		pos.y += (destPos.y - pos.y) / 32.0f;
	}
	if(abs(destPos.z - pos.z) > 0.01f) {
		pos.z += (destPos.z - pos.z) / 32.0f;
	}
	clampAng();
	shakeCamera();

	if(clampDisable == false && clampBounds){
		clampPosXYZ(0.0f, (float)limitX, minHeight, maxHeight, 0.0f, (float)limitY);
	}
}

Quad2i GameCamera::computeVisibleQuad() {
	//printf("\n@@@ hAng [%f] vAng [%f] fov [%f]\n",hAng,vAng,fov);

	if(MaxVisibleQuadItemCache != 0) {
		std::map<float, std::map<float, std::map<Vec3f, Quad2i> > >::const_iterator iterFind = cacheVisibleQuad.find(fov);
		if(iterFind != cacheVisibleQuad.end()) {
			std::map<float, std::map<Vec3f, Quad2i> >::const_iterator iterFind2 = iterFind->second.find(hAng);
			if(iterFind2 != iterFind->second.end()) {
				std::map<Vec3f, Quad2i>::const_iterator iterFind3 = iterFind2->second.find(pos);
				if(iterFind3 != iterFind2->second.end()) {
					return iterFind3->second;
				}
			}
		}
	}

	float nearDist = 15.f;
	float dist = pos.y > nearDist ? pos.y * 1.2f : nearDist;
	float farDist = 90.f * (pos.y > nearDist ? pos.y / 15.f : 1.f);
	const float viewDegree = 180.f;

	Vec2f v(std::sin(degToRad(viewDegree - hAng)), std::cos(degToRad(viewDegree - hAng)));
	Vec2f v1(std::sin(degToRad(viewDegree - hAng - fov)), std::cos(degToRad(viewDegree - hAng - fov)));
	Vec2f v2(std::sin(degToRad(viewDegree - hAng + fov)), std::cos(degToRad(viewDegree - hAng + fov)));

	v.normalize();
	v1.normalize();
	v2.normalize();

	Vec2f p = Vec2f(pos.x, pos.z) - v * dist;
	Vec2i p1(static_cast<int>(p.x + v1.x * nearDist), static_cast<int>(p.y + v1.y * nearDist));
	Vec2i p2(static_cast<int>(p.x + v1.x * farDist), static_cast<int>(p.y + v1.y * farDist));
	Vec2i p3(static_cast<int>(p.x + v2.x * nearDist), static_cast<int>(p.y + v2.y * nearDist));
	Vec2i p4(static_cast<int>(p.x + v2.x * farDist), static_cast<int>(p.y + v2.y * farDist));

	const bool debug = false;

	Quad2i result;
	if (hAng >= 135 && hAng <= 225) {
		if(debug) printf("Line %d  hAng [%f] fov [%f]\n",__LINE__,hAng,fov);

		result = Quad2i(p1, p2, p3, p4);
		if(MaxVisibleQuadItemCache != 0 &&
		   (MaxVisibleQuadItemCache < 0 || (int)cacheVisibleQuad[fov][hAng].size() <= MaxVisibleQuadItemCache)) {
			cacheVisibleQuad[fov][hAng][pos] = result;
		}
	}
	else if (hAng >= 45 && hAng <= 135) {
		if(debug) printf("Line %d  hAng [%f] fov [%f]\n",__LINE__,hAng,fov);

		result = Quad2i(p3, p1, p4, p2);
		if(MaxVisibleQuadItemCache != 0 &&
		   (MaxVisibleQuadItemCache < 0 || (int)cacheVisibleQuad[fov][hAng].size() <= MaxVisibleQuadItemCache)) {
			cacheVisibleQuad[fov][hAng][pos] = result;
		}
	}
	else if (hAng >= 225 && hAng <= 315) {
		if(debug) printf("Line %d  hAng [%f] fov [%f]\n",__LINE__,hAng,fov);

		result = Quad2i(p2, p4, p1, p3);
		if(MaxVisibleQuadItemCache != 0 &&
		   (MaxVisibleQuadItemCache < 0 || (int)cacheVisibleQuad[fov][hAng].size() <= MaxVisibleQuadItemCache)) {
			cacheVisibleQuad[fov][hAng][pos] = result;
		}
	}
	else {
		if(debug) printf("Line %d  hAng [%f] fov [%f]\n",__LINE__,hAng,fov);

		result = Quad2i(p4, p3, p2, p1);
		if(MaxVisibleQuadItemCache != 0 &&
		   (MaxVisibleQuadItemCache < 0 || (int)cacheVisibleQuad[fov][hAng].size() <= MaxVisibleQuadItemCache)) {
			cacheVisibleQuad[fov][hAng][pos] = Quad2i(p4, p3, p2, p1);
		}
	}

	return result;
}

void GameCamera::setState(State s){
	if(s==sGame){
		state = sGame;
		setClampDisabled(false);
		resetPosition();
	}
	else if(s==sUnit){
		state = sUnit;
		setClampDisabled(true);
	}
	else if(s==sFree){
		state = sFree;
		setClampDisabled(false);
		resetPosition();
	}
	else {
		abort();//"unknown camera state"
	}
}

void GameCamera::resetPosition(){
	destAng.x = startingVAng;
	destAng.y = startingHAng;
	destPos.y = calculatedDefault;
}

void GameCamera::centerXZ(float x, float z){
	destPos.x = pos.x= x;
	destPos.z = pos.z= z+centerOffsetZ;
}

void GameCamera::transitionXYZ(float x, float y, float z) {
	destPos.x += x;
	destPos.y += y;
	destPos.z += z;
	clampPosXYZ(0.0f, (float)limitX, minHeight, maxHeight, 0.0f, (float)limitY);
}

void GameCamera::transitionVH(float v, float h) {
	destAng.x -= v;
	//destPos.y -= v * destPos.y / 100.f;
	destAng.y -= h;
	clampAng();
}

void GameCamera::rotateToVH(float v, float h) {
	destAng.x = v;
	destAng.y = h;
	clampAng();
}

void GameCamera::zoom(float dist) {
	float flatDist = dist * std::cos(degToRad(vAng));
	Vec3f offset(flatDist * std::sin(degToRad(hAng)), dist * std::sin(degToRad(vAng)), flatDist  * -std::cos(degToRad(hAng)));

	destPos += offset;
}

void GameCamera::load(const XmlNode *node) {
	//destPos = node->getChildVec3fValue("pos");
	//destAng = node->getChildVec2fValue("angle");
}

void GameCamera::save(XmlNode *node) const {
	//node->addChild("pos", pos);
	//node->addChild("angle", Vec2f(vAng, hAng));
}

// ==================== PRIVATE ====================

void GameCamera::clampPosXZ(float x1, float x2, float z1, float z2){
	if(clampDisable == true) {
		return;
	}

	if(pos.x < x1)		pos.x = x1;
	if(destPos.x < x1)	destPos.x = x1;
	if(pos.z < z1)		pos.z = z1;
	if(destPos.z < z1)	destPos.z = z1;
	if(pos.x > x2)		pos.x = x2;
	if(destPos.x > x2)	destPos.x = x2;
	if(pos.z > z2)		pos.z = z2;
	if(destPos.z > z2)	destPos.z = z2;
}

void GameCamera::clampPosXYZ(float x1, float x2, float y1, float y2, float z1, float z2){
	if(clampDisable == true) {
		return;
	}

	if(pos.x < x1)		pos.x = x1;
	if(destPos.x < x1)	destPos.x = x1;
	if(pos.y < y1)		pos.y = y1;
	if(destPos.y < y1)	destPos.y = y1;
	if(pos.z < z1)		pos.z = z1;
	if(destPos.z < z1)	destPos.z = z1;
	if(pos.x > x2)		pos.x = x2;
	if(destPos.x > x2)	destPos.x = x2;
	if(pos.y > y2)		pos.y = y2;
	if(destPos.y > y2)	destPos.y = y2;
	if(pos.z > z2)		pos.z = z2;
	if(destPos.z > z2)	destPos.z = z2;
}

void GameCamera::rotateHV(float h, float v){
	destAng.x = vAng += v;
	destAng.y = hAng += h;
	clampAng();
}

void GameCamera::clampAng() {
	if(clampDisable == true && state != sUnit ) {
		return;
	}

	if(vAng > maxVAng)		vAng = maxVAng;
	if(destAng.x > maxVAng)	destAng.x = maxVAng;
	if(vAng < minVAng)		vAng = minVAng;
	if(destAng.x < minVAng)	destAng.x = minVAng;
	if(hAng > 360.f)		hAng -= 360.f;
	if(destAng.y > 360.f)	destAng.y -= 360.f;
	if(hAng < 0.f)			hAng += 360.f;
	if(destAng.y < 0.f)		destAng.y = 360.f;
}

//move camera forwad but never change heightFactor
void GameCamera::moveForwardH(float d, float response) {
	Vec3f offset(std::sin(degToRad(hAng)) * d, 0.f, -std::cos(degToRad(hAng)) * d);
	destPos += offset;
	pos.x += offset.x * response;
	pos.z += offset.z * response;
}

//move camera to a side but never change heightFactor
void GameCamera::moveSideH(float d, float response){
	Vec3f offset(std::sin(degToRad(hAng+90)) * d, 0.f, -std::cos(degToRad(hAng+90)) * d);
	destPos += offset;
	pos.x += (destPos.x - pos.x) * response;
	pos.z += (destPos.z - pos.z) * response;
}

void GameCamera::moveUp(float d){
//	pos.y+= d;
	destPos.y += d;
}

void GameCamera::saveGame(XmlNode *rootNode) {
	std::map<string,string> mapTagReplacements;
	XmlNode *gamecameraNode = rootNode->addChild("GameCamera");

//	Vec3f pos;
	gamecameraNode->addAttribute("pos",pos.getString(), mapTagReplacements);
//	Vec3f destPos;
	gamecameraNode->addAttribute("destPos",destPos.getString(), mapTagReplacements);
//
//    float hAng;	//YZ plane positive -Z axis
	gamecameraNode->addAttribute("hAng",floatToStr(hAng,6), mapTagReplacements);
//    float vAng;	//XZ plane positive +Z axis
	gamecameraNode->addAttribute("vAng",floatToStr(vAng,6), mapTagReplacements);
//	float lastHAng;
	gamecameraNode->addAttribute("lastHAng",floatToStr(lastHAng,6), mapTagReplacements);

//    float lastVAng;
	gamecameraNode->addAttribute("lastVAng",floatToStr(lastVAng,6), mapTagReplacements);
//	Vec2f destAng;
	gamecameraNode->addAttribute("destAng",destAng.getString(), mapTagReplacements);
//	float rotate;
	gamecameraNode->addAttribute("rotate",floatToStr(rotate,6), mapTagReplacements);
//	Vec3f move;
	gamecameraNode->addAttribute("move",move.getString(), mapTagReplacements);
//	State state;
	gamecameraNode->addAttribute("state",intToStr(state), mapTagReplacements);
//	int limitX;
	gamecameraNode->addAttribute("limitX",intToStr(limitX), mapTagReplacements);
//	int limitY;
	gamecameraNode->addAttribute("limitY",intToStr(limitY), mapTagReplacements);
//	//config
//	float speed;
	gamecameraNode->addAttribute("speed",floatToStr(speed,6), mapTagReplacements);
//	bool clampBounds;
	gamecameraNode->addAttribute("clampBounds",intToStr(clampBounds), mapTagReplacements);
//	//float maxRenderDistance;
//	float maxHeight;
	gamecameraNode->addAttribute("maxHeight",floatToStr(maxHeight,6), mapTagReplacements);
//	float minHeight;
	gamecameraNode->addAttribute("minHeight",floatToStr(minHeight,6), mapTagReplacements);
//	//float maxCameraDist;
//	//float minCameraDist;
//	float minVAng;
	gamecameraNode->addAttribute("minVAng",floatToStr(minVAng,6), mapTagReplacements);
//	float maxVAng;
	gamecameraNode->addAttribute("maxVAng",floatToStr(maxVAng,6), mapTagReplacements);
//	float fov;
	gamecameraNode->addAttribute("fov",floatToStr(fov,6), mapTagReplacements);
//	float calculatedDefault;
	gamecameraNode->addAttribute("calculatedDefault",floatToStr(calculatedDefault,6), mapTagReplacements);
//	std::map<float, std::map<float, std::map<Vec3f, Quad2i> > > cacheVisibleQuad;
//	int MaxVisibleQuadItemCache;
	gamecameraNode->addAttribute("MaxVisibleQuadItemCache",intToStr(MaxVisibleQuadItemCache), mapTagReplacements);
}

void GameCamera::loadGame(const XmlNode *rootNode) {
	const XmlNode *gamecameraNode = rootNode->getChild("GameCamera");

	//firstTime = timeflowNode->getAttribute("firstTime")->getFloatValue();

	//	Vec3f pos;
	pos = Vec3f::strToVec3(gamecameraNode->getAttribute("pos")->getValue());
	//	Vec3f destPos;
	destPos = Vec3f::strToVec3(gamecameraNode->getAttribute("destPos")->getValue());
	//
	//    float hAng;	//YZ plane positive -Z axis
	hAng = gamecameraNode->getAttribute("hAng")->getFloatValue();
	//    float vAng;	//XZ plane positive +Z axis
	vAng = gamecameraNode->getAttribute("vAng")->getFloatValue();
	//	float lastHAng;
	lastHAng = gamecameraNode->getAttribute("lastHAng")->getFloatValue();

	//    float lastVAng;
	lastVAng = gamecameraNode->getAttribute("lastVAng")->getFloatValue();
	//	Vec2f destAng;
	destAng = Vec2f::strToVec2(gamecameraNode->getAttribute("destAng")->getValue());
	//	float rotate;
	rotate = gamecameraNode->getAttribute("rotate")->getFloatValue();
	//	Vec3f move;
	move = Vec3f::strToVec3(gamecameraNode->getAttribute("move")->getValue());
	//	State state;
	state = static_cast<State>(gamecameraNode->getAttribute("state")->getIntValue());
	//	int limitX;
	limitX = gamecameraNode->getAttribute("limitX")->getIntValue();
	//	int limitY;
	limitY = gamecameraNode->getAttribute("limitY")->getIntValue();
	//	//config
	//	float speed;
	speed = gamecameraNode->getAttribute("speed")->getFloatValue();
	//	bool clampBounds;
	clampBounds = gamecameraNode->getAttribute("clampBounds")->getIntValue() != 0;
	//	//float maxRenderDistance;
	//	float maxHeight;
	maxHeight = gamecameraNode->getAttribute("maxHeight")->getFloatValue();
	//	float minHeight;
	minHeight = gamecameraNode->getAttribute("minHeight")->getFloatValue();
	//	//float maxCameraDist;
	//	//float minCameraDist;
	//	float minVAng;
	minVAng = gamecameraNode->getAttribute("minVAng")->getFloatValue();
	//	float maxVAng;
	maxVAng = gamecameraNode->getAttribute("maxVAng")->getFloatValue();
	//	float fov;
	fov = gamecameraNode->getAttribute("fov")->getFloatValue();
	//	float calculatedDefault;
	calculatedDefault = gamecameraNode->getAttribute("calculatedDefault")->getFloatValue();
	//	std::map<float, std::map<float, std::map<Vec3f, Quad2i> > > cacheVisibleQuad;
	//	int MaxVisibleQuadItemCache;
	MaxVisibleQuadItemCache = gamecameraNode->getAttribute("MaxVisibleQuadItemCache")->getIntValue();

}

}}//end namespace
