/* Universidade Federal Fluminense
 * Programa de Pós-Graduação em Computação
 *
 * Tópicos Avançados em Computação Visual e Interfaces II (Técnicas Avançadas de Rendering)
 * Professor: Leandro Augusto Frata Fernandes
 *
 * Version: 2012.05.05-001
 */

#include "camera.h"
#include <cmath>

AbstractCamera::AbstractCamera() {
	setPosition(coord3f(0.0f, 0.0f, 0.0f), coord3f(0.0f, 0.0f, -1.0f), coord3f(0.0f, 1.0f, 0.0f));
}

AbstractCamera::AbstractCamera(const coord3f& location_, const coord3f& forwardDirection_, const coord3f& upDirection_) {
	setPosition(location_, forwardDirection_, upDirection_);
}

coord3f AbstractCamera::getForwardDirection() const {
	return coord3f(-m_viewMatrix[2], -m_viewMatrix[6], -m_viewMatrix[10]);
}

coord3f AbstractCamera::getLocation() const {
	const matrix4x4f& viewMatrixInverse = getViewMatrixInverse();
	return coord3f(viewMatrixInverse[12], viewMatrixInverse[13], viewMatrixInverse[14]);
}

coord3f AbstractCamera::getRightDirection() const {
	return coord3f(m_viewMatrix[0], m_viewMatrix[4], m_viewMatrix[8]);
}

coord3f AbstractCamera::getUpDirection() const {
	return coord3f(m_viewMatrix[1], m_viewMatrix[5], m_viewMatrix[9]);
}

void AbstractCamera::rotateInViewSpace(const coord3f& axis, const GLfloat& angleInRadians) {
	m_viewMatrix = multiply(inverse(rotation(axis, angleInRadians)), m_viewMatrix);
}

void AbstractCamera::rotateInWorldSpace(const coord3f& axis, const GLfloat& angleInRadians) {
	m_viewMatrix = multiply(m_viewMatrix, inverse(rotation(axis, angleInRadians)));
}

void AbstractCamera::setPosition(const coord3f& location_, const coord3f& forwardDirection_, const coord3f& upDirection_) {
	const coord3f& unitForward = normalize(forwardDirection_);
	const coord3f& unitUp = normalize(upDirection_);
	const coord3f& unitRight = normalize(cross(forwardDirection_, upDirection_));

	m_viewMatrix[ 0] = unitRight.x;
	m_viewMatrix[ 1] = unitUp.x;
	m_viewMatrix[ 2] = -unitForward.x;
	m_viewMatrix[ 3] = 0.0f;

	m_viewMatrix[ 4] = unitRight.y;
	m_viewMatrix[ 5] = unitUp.y;
	m_viewMatrix[ 6] = -unitForward.y;
	m_viewMatrix[ 7] = 0.0f;

	m_viewMatrix[ 8] = unitRight.z;
	m_viewMatrix[ 9] = unitUp.z;
	m_viewMatrix[10] = -unitForward.z;
	m_viewMatrix[11] = 0.0f;

	m_viewMatrix[12] = -dot(unitRight, location_);
	m_viewMatrix[13] = -dot(unitUp, location_);
	m_viewMatrix[14] = dot(unitForward, location_);
	m_viewMatrix[15] = 1.0f;
}

void AbstractCamera::translateInViewSpace(const coord3f& delta) {
	m_viewMatrix = multiply(inverse(translation(delta)), m_viewMatrix);
}

void AbstractCamera::translateInWorldSpace(const coord3f& delta) {
	m_viewMatrix = multiply(m_viewMatrix, inverse(translation(delta)));
}

PinholeCamera::PinholeCamera() :
	AbstractCamera(),
	m_aspectRatio(1.0f),
	m_fovYInRadians(degreesToRadians(60.0f)),
	m_zFar(15.0f),
	m_zNear(1.0f) {

	updateProjectionMatrix();
}

PinholeCamera::PinholeCamera(const coord3f& location_, const coord3f& forwardDirection_, const coord3f& upDirection_, const GLfloat& fovYInRadians_, const GLfloat& aspectRatio_, const GLfloat& zNear_, const GLfloat& zFar_) :
	AbstractCamera(location_, forwardDirection_, upDirection_),
	m_aspectRatio(aspectRatio_),
	m_fovYInRadians(fovYInRadians_),
	m_zFar(zFar_),
	m_zNear(zNear_) {

	updateProjectionMatrix();
}

void PinholeCamera::setAspectRatio(const GLfloat& aspectRatio_) {
	m_aspectRatio = aspectRatio_;
	updateProjectionMatrix();
}

void PinholeCamera::setFovY(const GLfloat& fovYInRadians_) {
	m_fovYInRadians = fovYInRadians_;
	updateProjectionMatrix();
}

void PinholeCamera::setProjection(const GLfloat& fovYInRadians_, const GLfloat& aspectRatio_, const GLfloat& zNear_, const GLfloat& zFar_) {
	m_fovYInRadians = fovYInRadians_;
	m_aspectRatio = aspectRatio_;
	m_zNear = zNear_;
	m_zFar = zFar_;
	updateProjectionMatrix();
}

void PinholeCamera::setZFar(const GLfloat& zFar_) {
	m_zFar = zFar_;
	updateProjectionMatrix();
}

void PinholeCamera::setZNear(const GLfloat& zNear_) {
	m_zNear = zNear_;
	updateProjectionMatrix();
}

void PinholeCamera::updateProjectionMatrix() {
    const GLfloat yMax = m_zNear * tanf(0.5f * m_fovYInRadians);
    const GLfloat xMax = yMax * m_aspectRatio;

    m_projectionMatrix[ 0] = m_zNear / xMax;
    m_projectionMatrix[ 1] = 0.0f;
    m_projectionMatrix[ 2] = 0.0f;
    m_projectionMatrix[ 3] = 0.0f;

    m_projectionMatrix[ 4] = 0.0f;
    m_projectionMatrix[ 5] = m_zNear / yMax;
    m_projectionMatrix[ 6] = 0.0f;
    m_projectionMatrix[ 7] = 0.0f;

    m_projectionMatrix[ 8] = 0.0f;
    m_projectionMatrix[ 9] = 0.0f;
    m_projectionMatrix[10] = -(m_zFar + m_zNear) / (m_zFar - m_zNear);
    m_projectionMatrix[11] = -1.0f;

    m_projectionMatrix[12] = 0.0f;
    m_projectionMatrix[13] = 0.0f;
    m_projectionMatrix[14] = (-2.0f * m_zNear * m_zFar) / (m_zFar - m_zNear);
    m_projectionMatrix[15] = 0.0f;
}
