//
// Lol Engine
//
// Copyright: (c) 2010-2013 Sam Hocevar <sam@hocevar.net>
//   This program is free software; you can redistribute it and/or
//   modify it under the terms of the Do What The Fuck You Want To
//   Public License, Version 2, as published by Sam Hocevar. See
//   http://www.wtfpl.net/ for more details.
//

#if defined HAVE_CONFIG_H
#   include "config.h"
#endif

#include <cstring>
#include <cstdlib>

#include "core.h"

#if defined _WIN32 || defined _XBOX
#   define strcasecmp _stricmp
#   undef near
#   undef far
#endif

namespace lol
{

Camera::Camera()
{
    m_gamegroup = GAMEGROUP_BEFORE;
    m_drawgroup = DRAWGROUP_CAMERA;

    ivec2 screen_size = (g_renderer)?(Video::GetSize()):(ivec2(0));
    m_fov = 45.f;
    m_near = -1000.f;
    m_far = 1000.f;
    m_screen_size = (float)screen_size.x;
    m_screen_ratio = (float)screen_size.x / (float)screen_size.y;
    m_is_shifted = false;

    /* Create a default perspective */
    SetProjection(mat4::perspective(45.f, 800.f, 600.f, -1000.f, 1000.f));
    SetView(mat4::lookat(vec3(0.f, 50.f, 50.f),
                         vec3(0.f),
                         vec3(0.f, 1.f, 0.f)));
}

Camera::~Camera()
{
}

//-----------------------------------------------------------------------------
//View functions
//--
void Camera::SetView(mat4 const &view)
{
    m_view_matrix = view;
    m_position = inverse(view)[3].xyz;
}

void Camera::SetView(vec3 eye, vec3 target, vec3 up)
{
    m_view_matrix = mat4::lookat(eye, target, up);
    m_position = eye;
    m_target_distance = length(target - m_position);
}

void Camera::SetView(vec3 pos, vec3 rot)
{
    SetView(pos, quat::fromeuler_xyz(rot));
}

void Camera::SetView(vec3 pos, quat rot)
{
    m_view_matrix = mat4::lookat(pos,
                                 pos + rot.transform(vec3(0.f, 0.f, -max(m_target_distance, 1.f))),
                                 rot.transform(vec3(0.f, 1.f, 0.f)));
    m_position = pos;
}

mat4 Camera::GetView()
{
    return m_view_matrix;
}

//-----------------------------------------------------------------------------
//Projections functions
//--
void Camera::SetProjection(mat4 const &proj)
{
    m_proj_matrix = proj;
}

void Camera::SetProjection(float fov, float near, float far)
{
    SetProjection(fov, near, far, m_screen_size, m_screen_ratio);
}

void Camera::SetProjection(float fov, float near, float far, float screen_size, float screen_ratio)
{
    m_fov = fov;
    m_near = near;
    m_far = far;
    m_screen_size = screen_size;
    m_screen_ratio = screen_ratio;

    if (m_fov > .0f)
    {
        if (m_is_shifted)
            SetProjection(mat4::shifted_perspective(m_fov, screen_size, screen_ratio, m_far - m_near));
        else
            SetProjection(mat4::perspective(m_fov, screen_size, screen_size * screen_ratio, m_near, m_far));
    }
    else
        SetProjection(mat4::ortho(screen_size, screen_size * screen_ratio, m_near, m_far));
}

mat4 Camera::GetProjection()
{
    return m_proj_matrix;
}

//-----------------------------------------------------------------------------
//Projections manipulation functions
//--
void Camera::SetFov(float fov)
{
    SetProjection(fov, m_near, m_far, m_screen_size, m_screen_ratio);
}

void Camera::SetScreenInfos(float screen_size)
{
    SetScreenInfos(screen_size, m_screen_ratio);
}

void Camera::SetScreenInfos(float screen_size, float screen_ratio)
{
    SetProjection(m_fov, m_near, m_far, screen_size, screen_ratio);
}

void Camera::SetDrawInfos(float far)
{
    SetDrawInfos(m_near, far);
}

void Camera::SetDrawInfos(float near, float far)
{
    SetProjection(m_fov, near, far, m_screen_size, m_screen_ratio);
}

void Camera::UseShift(bool should_shift)
{
    m_is_shifted = should_shift;
    SetProjection(m_fov, m_near, m_far, m_screen_size, m_screen_ratio);
}

void Camera::UseTarget(bool use_target)
{
    m_target_distance = ((use_target)?(max(m_target_distance, 1.f)):(.0f));
}

//-----------------------------------------------------------------------------
//camera manipulation Functions
//--
void Camera::SetPosition(vec3 pos)
{
    if (m_target_distance > .0f)
        SetView(m_position, m_position + GetTarget(), GetUp());
    else
        SetView(GetView() * mat4::translate(pos - m_position));
    m_position = pos;
}

void Camera::SetTarget(vec3 target)
{
    SetView(m_position, target, GetUp());
}

void Camera::SetUp(vec3 up)
{
    SetView(m_position, GetTarget(), up);
}

void Camera::SetRotation(vec3 rot)
{
    SetRotation(quat::fromeuler_xyz(rot));
}

void Camera::SetRotation(quat rot)
{
    SetView(m_position, rot);
}

//--
vec3 Camera::GetPosition()
{
    return m_position;
}

vec3 Camera::GetTarget()
{
    return m_position + (inverse(m_view_matrix) * vec4(0.f, 0.f, -max(m_target_distance, 1.f), 0.f)).xyz;
}

vec3 Camera::GetUp()
{
    return (inverse(m_view_matrix) * vec4(0.f, 1.f, 0.f, 0.f)).xyz;
}

vec3 Camera::GetRotationEuler()
{
    return vec3::toeuler_zyx(GetRotation());
}

quat Camera::GetRotation()
{
    return quat(inverse(m_view_matrix));
}

// Calculate the frustum height at a given distance from the camera.
float Camera::GetFrustumHeightAtDistance(float distance, float fov)
{
    return 2.f * distance * lol::tan(fov * .5f * (F_PI / 180.f));
}

// Calculate the FOV needed to get a given frustum height at a given distance.
float Camera::GetFOVForHeightAndDistance(float distance, float height)
{
    return 2.f * lol::atan(height * .5f / distance) * (180.f / F_PI);
}

void Camera::TickGame(float seconds)
{
    WorldEntity::TickGame(seconds);
}

void Camera::TickDraw(float seconds)
{
    WorldEntity::TickDraw(seconds);
}

} /* namespace lol */