#include "Base.h"
#include "Camera.h"
#include "Game.h"
#include "Node.h"
#include "Game.h"
#include "PhysicsController.h"

// Camera dirty bits
#define CAMERA_DIRTY_VIEW 1
#define CAMERA_DIRTY_PROJ 2
#define CAMERA_DIRTY_VIEW_PROJ 4
#define CAMERA_DIRTY_INV_VIEW 8
#define CAMERA_DIRTY_INV_VIEW_PROJ 16
#define CAMERA_DIRTY_BOUNDS 32
#define CAMERA_DIRTY_ALL (CAMERA_DIRTY_VIEW | CAMERA_DIRTY_PROJ | CAMERA_DIRTY_VIEW_PROJ | CAMERA_DIRTY_INV_VIEW | CAMERA_DIRTY_INV_VIEW_PROJ | CAMERA_DIRTY_BOUNDS)

// Other misc camera bits
#define CAMERA_CUSTOM_PROJECTION 64

namespace gameplay
{

Camera::Camera(float fieldOfView, float aspectRatio, float nearPlane, float farPlane)
    : _type(PERSPECTIVE), _fieldOfView(fieldOfView), _aspectRatio(aspectRatio), _nearPlane(nearPlane), _farPlane(farPlane),
      _bits(CAMERA_DIRTY_ALL), _node(NULL)
{
}

Camera::Camera(float zoomX, float zoomY, float aspectRatio, float nearPlane, float farPlane)
    : _type(ORTHOGRAPHIC), _aspectRatio(aspectRatio), _nearPlane(nearPlane), _farPlane(farPlane),
      _bits(CAMERA_DIRTY_ALL), _node(NULL)
{
    // Orthographic camera.
    _zoom[0] = zoomX;
    _zoom[1] = zoomY;
}

Camera::~Camera()
{
}

Camera* Camera::createPerspective(float fieldOfView, float aspectRatio, float nearPlane, float farPlane)
{
    return new Camera(fieldOfView, aspectRatio, nearPlane, farPlane);
}

Camera* Camera::createOrthographic(float zoomX, float zoomY, float aspectRatio, float nearPlane, float farPlane)
{
    return new Camera(zoomX, zoomY, aspectRatio, nearPlane, farPlane);
}

Camera* Camera::create(Properties* properties)
{
    GP_ASSERT(properties);

    // Read camera type
    std::string typeStr;
    if (properties->exists("type"))
        typeStr = properties->getString("type");
    Camera::Type type;
    if (typeStr == "PERSPECTIVE")
    {
        type = Camera::PERSPECTIVE;
    }
    else if (typeStr == "ORTHOGRAPHIC")
    {
        type = Camera::ORTHOGRAPHIC;
    }
    else
    {
        GP_ERROR("Invalid 'type' parameter for camera definition.");
        return NULL;
    }

    // Read common parameters
    float aspectRatio, nearPlane, farPlane;
    if (properties->exists("aspectRatio"))
    {
        aspectRatio = properties->getFloat("aspectRatio");
    }
    else
    {
        // Use default aspect ratio
        aspectRatio = (float)Game::getInstance()->getWidth() / Game::getInstance()->getHeight();
    }

    if (properties->exists("nearPlane"))
        nearPlane = properties->getFloat("nearPlane");
    else
        nearPlane = 0.2f; // use some reasonable default value

    if (properties->exists("farPlane"))
        farPlane = properties->getFloat("farPlane");
    else
        farPlane = 100; // use some reasonable default value

    Camera* camera = NULL;

    switch (type)
    {
    case Camera::PERSPECTIVE:
        // If field of view is not specified, use a default of 60 degrees
        camera = createPerspective(
            properties->exists("fieldOfView") ? properties->getFloat("fieldOfView") : 60.0f,
            aspectRatio, nearPlane, farPlane);
        break;

    case Camera::ORTHOGRAPHIC:
        // If zoomX and zoomY are not specified, use screen width/height
        camera = createOrthographic(
            properties->exists("zoomX") ? properties->getFloat("zoomX") : Game::getInstance()->getWidth(),
            properties->exists("zoomY") ? properties->getFloat("zoomY") : Game::getInstance()->getHeight(),
            aspectRatio, nearPlane, farPlane);
        break;
    }

    return camera;
}

Camera::Type Camera::getCameraType() const
{
    return _type;
}

float Camera::getFieldOfView() const
{
    GP_ASSERT(_type == Camera::PERSPECTIVE);

    return _fieldOfView;
}

void Camera::setFieldOfView(float fieldOfView)
{
    GP_ASSERT(_type == Camera::PERSPECTIVE);

    _fieldOfView = fieldOfView;
    _bits |= CAMERA_DIRTY_PROJ | CAMERA_DIRTY_VIEW_PROJ | CAMERA_DIRTY_INV_VIEW_PROJ | CAMERA_DIRTY_BOUNDS;
}

float Camera::getZoomX() const
{
    GP_ASSERT(_type == Camera::ORTHOGRAPHIC);

    return _zoom[0];
}

void Camera::setZoomX(float zoomX)
{
    GP_ASSERT(_type == Camera::ORTHOGRAPHIC);

    _zoom[0] = zoomX;
    _bits |= CAMERA_DIRTY_PROJ | CAMERA_DIRTY_VIEW_PROJ | CAMERA_DIRTY_INV_VIEW_PROJ | CAMERA_DIRTY_BOUNDS;
}

float Camera::getZoomY() const
{
    GP_ASSERT(_type == Camera::ORTHOGRAPHIC);

    return _zoom[1];
}

void Camera::setZoomY(float zoomY)
{
    GP_ASSERT(_type == Camera::ORTHOGRAPHIC);

    _zoom[1] = zoomY;
    _bits |= CAMERA_DIRTY_PROJ | CAMERA_DIRTY_VIEW_PROJ | CAMERA_DIRTY_INV_VIEW_PROJ | CAMERA_DIRTY_BOUNDS;
}

float Camera::getAspectRatio() const
{
    return _aspectRatio;
}

void Camera::setAspectRatio(float aspectRatio)
{
    _aspectRatio = aspectRatio;
    _bits |= CAMERA_DIRTY_PROJ | CAMERA_DIRTY_VIEW_PROJ | CAMERA_DIRTY_INV_VIEW_PROJ | CAMERA_DIRTY_BOUNDS;
}

float Camera::getNearPlane() const
{
    return _nearPlane;
}

void Camera::setNearPlane(float nearPlane)
{
    _nearPlane = nearPlane;
    _bits |= CAMERA_DIRTY_PROJ | CAMERA_DIRTY_VIEW_PROJ | CAMERA_DIRTY_INV_VIEW_PROJ | CAMERA_DIRTY_BOUNDS;
}

float Camera::getFarPlane() const
{
    return _farPlane;
}

void Camera::setFarPlane(float farPlane)
{
    _farPlane = farPlane;
    _bits |= CAMERA_DIRTY_PROJ | CAMERA_DIRTY_VIEW_PROJ | CAMERA_DIRTY_INV_VIEW_PROJ | CAMERA_DIRTY_BOUNDS;
}

Node* Camera::getNode() const
{
    return _node;
}

void Camera::setNode(Node* node)
{
    if (_node != node)
    {
        if (_node)
        {
            _node->removeListener(this);
        }

        // Connect the new node.
        _node = node;

        if (_node)
        {
            _node->addListener(this);
        }

        _bits |= CAMERA_DIRTY_VIEW | CAMERA_DIRTY_VIEW_PROJ | CAMERA_DIRTY_INV_VIEW | CAMERA_DIRTY_INV_VIEW_PROJ | CAMERA_DIRTY_BOUNDS;
    }
}

const Matrix& Camera::getViewMatrix() const
{
    if (_bits & CAMERA_DIRTY_VIEW)
    {
        if (_node)
        {
            // The view matrix is the inverse of our transform matrix.
            _node->getWorldMatrix().invert(&_view);
        }
        else
        {
            _view.setIdentity();
        }

        _bits &= ~CAMERA_DIRTY_VIEW;
    }

    return _view;
}

const Matrix& Camera::getInverseViewMatrix() const
{
    if (_bits & CAMERA_DIRTY_INV_VIEW)
    {
        getViewMatrix().invert(&_inverseView);

        _bits &= ~CAMERA_DIRTY_INV_VIEW;
    }

    return _inverseView;
}

const Matrix& Camera::getProjectionMatrix() const
{
    if (!(_bits & CAMERA_CUSTOM_PROJECTION) && (_bits & CAMERA_DIRTY_PROJ))
    {
        if (_type == PERSPECTIVE)
        {
            Matrix::createPerspective(_fieldOfView, _aspectRatio, _nearPlane, _farPlane, &_projection);
        }
        else
        {
            Matrix::createOrthographic(_zoom[0], _zoom[1], _nearPlane, _farPlane, &_projection);
        }

        _bits &= ~CAMERA_DIRTY_PROJ;
    }

    return _projection;
}

void Camera::setProjectionMatrix(const Matrix& matrix)
{
    _projection = matrix;
    _bits |= CAMERA_CUSTOM_PROJECTION;
    _bits |= CAMERA_DIRTY_PROJ | CAMERA_DIRTY_VIEW_PROJ | CAMERA_DIRTY_INV_VIEW_PROJ | CAMERA_DIRTY_BOUNDS;
}

void Camera::resetProjectionMatrix()
{
    if (_bits & CAMERA_CUSTOM_PROJECTION)
    {
        _bits &= ~CAMERA_CUSTOM_PROJECTION;
        _bits |= CAMERA_DIRTY_PROJ | CAMERA_DIRTY_VIEW_PROJ | CAMERA_DIRTY_INV_VIEW_PROJ | CAMERA_DIRTY_BOUNDS;
    }
}

const Matrix& Camera::getViewProjectionMatrix() const
{
    if (_bits & CAMERA_DIRTY_VIEW_PROJ)
    {
        Matrix::multiply(getProjectionMatrix(), getViewMatrix(), &_viewProjection);

        _bits &= ~CAMERA_DIRTY_VIEW_PROJ;
    }

    return _viewProjection;
}

const Matrix& Camera::getInverseViewProjectionMatrix() const
{
    if (_bits & CAMERA_DIRTY_INV_VIEW_PROJ)
    {
        getViewProjectionMatrix().invert(&_inverseViewProjection);

        _bits &= ~CAMERA_DIRTY_INV_VIEW_PROJ;
    }

    return _inverseViewProjection;
}

const Frustum& Camera::getFrustum() const
{
    if (_bits & CAMERA_DIRTY_BOUNDS)
    {
        // Update our bounding frustum from our view projection matrix.
        _bounds.set(getViewProjectionMatrix());

        _bits &= ~CAMERA_DIRTY_BOUNDS;
    }

    return _bounds;
}

void Camera::project(const Rectangle& viewport, const Vector3& position, float* x, float* y, float* depth) const
{
    GP_ASSERT(x);
    GP_ASSERT(y);

    // Transform the point to clip-space.
    Vector4 clipPos;
    getViewProjectionMatrix().transformVector(Vector4(position.x, position.y, position.z, 1.0f), &clipPos);

    // Compute normalized device coordinates.
    GP_ASSERT(clipPos.w != 0.0f);
    float ndcX = clipPos.x / clipPos.w;
    float ndcY = clipPos.y / clipPos.w;

    // Compute screen coordinates by applying our viewport transformation.
    *x = viewport.x + (ndcX + 1.0f) * 0.5f * viewport.width;
    *y = viewport.y + (1.0f - (ndcY + 1.0f) * 0.5f) * viewport.height;
    if (depth)
    {
        float ndcZ = clipPos.z / clipPos.w;
        *depth = ndcZ + 1.0f / 2.0f;
    }
}

void Camera::project(const Rectangle& viewport, const Vector3& position, Vector2* out) const
{
    GP_ASSERT(out);
    float x, y;
    project(viewport, position, &x, &y);
    out->set(x, y);
}

void Camera::project(const Rectangle& viewport, const Vector3& position, Vector3* out) const
{
    GP_ASSERT(out);
    float x, y, depth;
    project(viewport, position, &x, &y, &depth);
    out->set(x, y, depth);
}

void Camera::unproject(const Rectangle& viewport, float x, float y, float depth, Vector3* dst) const
{
    GP_ASSERT(dst);
    
    // Create our screen space position in NDC.
    GP_ASSERT(viewport.width != 0.0f && viewport.height != 0.0f);
    Vector4 screen((x - viewport.x) / viewport.width, ((viewport.height - y) - viewport.y) / viewport.height, depth, 1.0f);

    // Map to range -1 to 1.
    screen.x = screen.x * 2.0f - 1.0f;
    screen.y = screen.y * 2.0f - 1.0f;
    screen.z = screen.z * 2.0f - 1.0f;

    // Transform the screen-space NDC by our inverse view projection matrix.
    getInverseViewProjectionMatrix().transformVector(screen, &screen);

    // Divide by our W coordinate.
    if (screen.w != 0.0f)
    {
        screen.x /= screen.w;
        screen.y /= screen.w;
        screen.z /= screen.w;
    }

    dst->set(screen.x, screen.y, screen.z);
}

void Camera::pickRay(const Rectangle& viewport, float x, float y, Ray* dst) const
{
    GP_ASSERT(dst);

    // Get the world-space position at the near clip plane.
    Vector3 nearPoint;
    unproject(viewport, x, y, 0.0f, &nearPoint);

    // Get the world-space position at the far clip plane.
    Vector3 farPoint;
    unproject(viewport, x, y, 1.0f, &farPoint);

    // Set the direction of the ray.
    Vector3 direction;
    Vector3::subtract(farPoint, nearPoint, &direction);
    direction.normalize();

    dst->set(nearPoint, direction);
}

Camera* Camera::clone(NodeCloneContext &context) const
{
    Camera* cameraClone = NULL;
    if (getCameraType() == PERSPECTIVE)
    {
        cameraClone = createPerspective(_fieldOfView, _aspectRatio, _nearPlane, _farPlane);
    }
    else if (getCameraType() == ORTHOGRAPHIC)
    {
        cameraClone = createOrthographic(getZoomX(), getZoomY(), getAspectRatio(), _nearPlane, _farPlane);
    }
    GP_ASSERT(cameraClone);

    if (Node* node = context.findClonedNode(getNode()))
    {
        cameraClone->setNode(node);
    }
    return cameraClone;
}

void Camera::transformChanged(Transform* transform, long cookie)
{
    _bits |= CAMERA_DIRTY_VIEW | CAMERA_DIRTY_INV_VIEW | CAMERA_DIRTY_INV_VIEW_PROJ | CAMERA_DIRTY_VIEW_PROJ | CAMERA_DIRTY_BOUNDS;
}

}
