#ifndef CAMERA_H
#define CAMERA_H

#include "boundingbox.h"
#include "render_shared.h"

#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>
#include <glm/gtx/euler_angles.hpp>
#include <glm/gtx/norm.hpp>

enum CameraProjection_t
{
	ORTHOGONAL,
	PERSPECTIVE,
};

enum FrustumSide_t
{
	FRUSTUM_TOP,
	FRUSTUM_BOTTOM,
	FRUSTUM_RIGHT,
	FRUSTUM_LEFT,
	FRUSTUM_FAR,
	FRUSTUM_NEAR,
	FRUSTUM_MAX
};

enum Halfspace_t
{
	HALFSPACE_NEGATIVE = -1,
	HALFSPACE_ONPLANE = 0,
	HALFSPACE_POSITIVE = 1
};

struct Plane
{
	glm::vec3 normal;
	float distance;

	Plane() : normal(0.0f, 1.0f, 0.0f), distance(0.0f)
	{
	}

	inline void Normalize();
	inline float ClassifyPoint(const glm::vec3& point) const;
};

inline void Plane::Normalize()
{
	// calculate vector magnitude
	float mag = glm::length(normal);

	// divide plane normal and distance
	normal /= mag;
	distance /= mag;
}

inline float Plane::ClassifyPoint(const glm::vec3& point) const
{
	return glm::dot(normal, point) + distance;
}

struct Sphere
{
	glm::vec3 center;
	float radius;

	Sphere() : center(0.0f), radius(0.0f)
	{
	}
};

class Frustum
{
public:
	Plane m_planes[FRUSTUM_MAX];

public:
	inline void Build(const glm::mat4& a_ViewProj);

	inline bool CullBoundingBox(const BoundingBox& a_Bounds);
	inline bool CullSphere(const Sphere& a_Sphere);
};

inline void Frustum::Build(const glm::mat4& a_ViewProj)
{
	// left
	m_planes[FRUSTUM_LEFT].normal.x = a_ViewProj[3][0] + a_ViewProj[0][0];
	m_planes[FRUSTUM_LEFT].normal.y = a_ViewProj[3][1] + a_ViewProj[0][1];
	m_planes[FRUSTUM_LEFT].normal.z = a_ViewProj[3][2] + a_ViewProj[0][2];
	m_planes[FRUSTUM_LEFT].distance = a_ViewProj[3][3] + a_ViewProj[0][3];
	m_planes[FRUSTUM_LEFT].Normalize();

	// right
	m_planes[FRUSTUM_RIGHT].normal.x = a_ViewProj[3][0] - a_ViewProj[0][0];
	m_planes[FRUSTUM_RIGHT].normal.y = a_ViewProj[3][1] - a_ViewProj[0][1];
	m_planes[FRUSTUM_RIGHT].normal.z = a_ViewProj[3][2] - a_ViewProj[0][2];
	m_planes[FRUSTUM_RIGHT].distance = a_ViewProj[3][3] - a_ViewProj[0][3];
	m_planes[FRUSTUM_RIGHT].Normalize();

	// bottom
	m_planes[FRUSTUM_BOTTOM].normal.x = a_ViewProj[3][0] + a_ViewProj[1][0];
	m_planes[FRUSTUM_BOTTOM].normal.y = a_ViewProj[3][1] + a_ViewProj[1][1];
	m_planes[FRUSTUM_BOTTOM].normal.z = a_ViewProj[3][2] + a_ViewProj[1][2];
	m_planes[FRUSTUM_BOTTOM].distance = a_ViewProj[3][3] + a_ViewProj[1][3];
	m_planes[FRUSTUM_BOTTOM].Normalize();

	// top
	m_planes[FRUSTUM_TOP].normal.x = a_ViewProj[3][0] - a_ViewProj[1][0];
	m_planes[FRUSTUM_TOP].normal.y = a_ViewProj[3][1] - a_ViewProj[1][1];
	m_planes[FRUSTUM_TOP].normal.z = a_ViewProj[3][2] - a_ViewProj[1][2];
	m_planes[FRUSTUM_TOP].distance = a_ViewProj[3][3] - a_ViewProj[1][3];
	m_planes[FRUSTUM_TOP].Normalize();

	// near
	m_planes[FRUSTUM_NEAR].normal.x = a_ViewProj[3][0] + a_ViewProj[2][0];
	m_planes[FRUSTUM_NEAR].normal.y = a_ViewProj[3][1] + a_ViewProj[2][1];
	m_planes[FRUSTUM_NEAR].normal.z = a_ViewProj[3][2] + a_ViewProj[2][2];
	m_planes[FRUSTUM_NEAR].distance = a_ViewProj[3][3] + a_ViewProj[2][3];
	m_planes[FRUSTUM_NEAR].Normalize();

	// far
	m_planes[FRUSTUM_FAR].normal.x = a_ViewProj[3][0] - a_ViewProj[2][0];
	m_planes[FRUSTUM_FAR].normal.y = a_ViewProj[3][1] - a_ViewProj[2][1];
	m_planes[FRUSTUM_FAR].normal.z = a_ViewProj[3][2] - a_ViewProj[2][2];
	m_planes[FRUSTUM_FAR].distance = a_ViewProj[3][3] - a_ViewProj[2][3];
	m_planes[FRUSTUM_FAR].Normalize();
}

inline bool Frustum::CullBoundingBox(const BoundingBox& a_Bounds)
{
	glm::vec3 center = a_Bounds.GetCenter();
	glm::vec3 magnitude = (a_Bounds.m_max - a_Bounds.m_min) * 0.5f;

	for (int i = 0; i < FRUSTUM_MAX; i++)
	{
		const Plane& plane = m_planes[i];
		float dist = plane.ClassifyPoint(center);

		dist += fabsf(plane.normal.z) * magnitude.z;
		dist += fabsf(plane.normal.y) * magnitude.y;
		dist += fabsf(plane.normal.x) * magnitude.x;
		if (dist < 0.0f)
			return true;
	}

	return false;
}

inline bool Frustum::CullSphere(const Sphere& a_Sphere)
{
	return m_planes[FRUSTUM_LEFT].ClassifyPoint(a_Sphere.center) >= -a_Sphere.radius &&
		m_planes[FRUSTUM_RIGHT].ClassifyPoint(a_Sphere.center) >= -a_Sphere.radius &&
		m_planes[FRUSTUM_TOP].ClassifyPoint(a_Sphere.center) >= -a_Sphere.radius &&
		m_planes[FRUSTUM_BOTTOM].ClassifyPoint(a_Sphere.center) >= -a_Sphere.radius &&
		m_planes[FRUSTUM_NEAR].ClassifyPoint(a_Sphere.center) >= -a_Sphere.radius &&
		m_planes[FRUSTUM_FAR].ClassifyPoint(a_Sphere.center) >= -a_Sphere.radius;

}

class Camera
{
public:
	Camera();
	~Camera();

	inline void LookAt(const glm::vec3& direction) { m_direction = direction /*- m_position*/; }

	inline void SetProjection(CameraProjection_t projection) { m_projectionType = projection; }
	inline CameraProjection_t GetProjection() const { return m_projectionType; }

	// In degrees

	inline void SetFOV(float fov) { m_fov = fov; }
	inline float GetFOV() const { return m_fov; }
	inline void SetPosition(const glm::vec3& position) { m_position = position; }

	inline const glm::vec3& GetPosition() const { return m_position; }

	void SetYawPitch(float yaw, float pitch);

	inline const glm::vec3& GetDirection() const { return m_direction; }
	inline const glm::vec3& GetFront() const { return m_front; }
	inline const glm::vec3& GetRight() const { return m_right; }
	inline const glm::vec3& GetUp() const { return m_up; }

	inline float GetYaw() { return m_yaw; }
	inline float GetPitch() { return m_pitch; }

	glm::quat GetOrientation();

	// FIXME: Control zNear/zFar in perspective projection
	glm::mat4 GetProjectionMatrix(const Viewport& viewport) const;
	glm::mat4 GetViewMatrix() const;
	glm::mat4 GetTranslationMatrix() const;

	Frustum& GetFrustum();

private:
	Frustum m_frustum;

	CameraProjection_t m_projectionType;

	glm::vec3 m_position;
	glm::vec3 m_direction;
	glm::vec3 m_front;
	glm::vec3 m_right;
	glm::vec3 m_up;

	float m_yaw;
	float m_pitch;

	float m_fov;
};

//////////////////////////////////////////////////////////////////////////
// Camera Manager

class CameraManager
{
public:
	void SetActiveCamera(Camera* p_camera);
	Camera* GetActiveCamera() const;

private:
	Camera* m_activeCamera;
};

extern CameraManager g_cameraManager;

#endif // !CAMERA_H
