#pragma once

#include <psemek/math/vector.hpp>
#include <psemek/math/point.hpp>
#include <psemek/math/matrix.hpp>
#include <psemek/math/box.hpp>

#include <array>

namespace psemek::math
{

	struct camera
	{
		virtual matrix<float, 4, 4> projection() const = 0;
		virtual matrix<float, 4, 4> view() const = 0;

		virtual matrix<float, 4, 4> transform() const;
		virtual point<float, 3> position() const;

		// View direction is -axis_z(), or equivalently direction(0,0)
		// NB: these vectors are not necessarily normalized
		virtual vector<float, 3> axis_x() const;
		virtual vector<float, 3> axis_y() const;
		virtual vector<float, 3> axis_z() const;

		virtual vector<float, 3> direction() const;

		// x, y are in clip-space [-1, 1]
		// NB: this vector is not necessarily normalized
		virtual vector<float, 3> direction(float x, float y) const;

		// view frustum is the intersection of half-spaces defined by dot(clip_plane, x) >= 0
		virtual std::array<vector<float, 4>, 6> clip_planes() const;

		virtual ~camera() = default;
	};

	struct window_camera
		: camera
	{
		float width, height;

		window_camera() = default;

		window_camera(float width, float height)
			: width{width}
			, height{height}
		{}

		matrix<float, 4, 4> view() const override;
		matrix<float, 4, 4> projection() const override;
	};

	struct orthographic_camera
		: camera
	{
		math::box<float, 3> box;

		orthographic_camera() = default;

		orthographic_camera(math::box<float, 2> const & box)
		{
			this->box[0] = box[0];
			this->box[1] = box[1];
			this->box[2] = {-1.f, 1.f};
		}

		orthographic_camera(math::box<float, 3> const & box)
			: box{box}
		{}

		matrix<float, 4, 4> view() const override;
		matrix<float, 4, 4> projection() const override;
	};

	struct perspective_camera
		: camera
	{
		float fov_x;
		float fov_y;

		float near_clip;
		float far_clip;

		perspective_camera() = default;

		perspective_camera(float fov_x, float fov_y, float near_clip, float far_clip)
			: fov_x{fov_x}
			, fov_y{fov_y}
			, near_clip{near_clip}
			, far_clip{far_clip}
		{}

		matrix<float, 4, 4> projection() const override;

		// aspect_ratio = width / height
		void set_fov(float fov_y, float aspect_ratio);
	};

	template <typename BaseCamera>
	struct matrix_camera
		: BaseCamera
	{
		matrix<float, 4, 4> view_matrix;

		matrix<float, 4, 4> view() const override { return view_matrix; }
	};

	struct spherical_camera
		: perspective_camera
	{
		// assumes up is +Z
		// azimuthal angle is in XY plane
		// both angles zero gives X right, Y forward, and Z up

		float azimuthal_angle;
		float elevation_angle;
		float distance;
		point<float, 3> target;

		spherical_camera() = default;

		spherical_camera(float fov_x, float fov_y, float near_clip, float far_clip, float azimuthal_angle, float elevation_angle, float distance, point<float, 3> const & target)
			: perspective_camera(fov_x, fov_y, near_clip, far_clip)
			, azimuthal_angle{azimuthal_angle}
			, elevation_angle{elevation_angle}
			, distance{distance}
			, target{target}
		{}

		matrix<float, 4, 4> view() const override;
	};

	struct free_camera
		: perspective_camera
	{
		point<float, 3> pos;
		vector<float, 3> axes[3];

		free_camera()
		{
			axes[0] = {1.f, 0.f, 0.f};
			axes[1] = {0.f, 1.f, 0.f};
			axes[2] = {0.f, 0.f, 1.f};
		}

		free_camera(float fov_x, float fov_y, float near_clip, float far_clip, point<float, 3> const & pos)
			: perspective_camera(fov_x, fov_y, near_clip, far_clip)
			, pos{pos}
		{
			axes[0] = {1.f, 0.f, 0.f};
			axes[1] = {0.f, 1.f, 0.f};
			axes[2] = {0.f, 0.f, 1.f};
		}

		void rotateXY(float angle);
		void rotateYZ(float angle);
		void rotateZX(float angle);

		matrix<float, 4, 4> view() const override;
	};
}
