#pragma once

#include "Utils.h"
#include "Isaac.h"

/// <summary>
/// Curves class.
/// </summary>

namespace EmberNs
{
/// <summary>
/// The Bezier curves used to adjust the colors during final accumulation.
/// This functionality was gotten directly from Apophysis.
/// </summary>
template <typename T>
class EMBER_API Curves
{
public:
	/// <summary>
	/// Constructor which sets the curve and weight values to their defaults.
	/// </summary>
	Curves(bool init = false)
	{
		if (init)
			Init();
		else
			Clear();
	}

	/// <summary>
	/// Default copy constructor.
	/// </summary>
	/// <param name="curves">The Curves object to copy</param>
	Curves(const Curves<T>& curves)
	{
		Curves<T>::operator=<T>(curves);
	}

	/// <summary>
	/// Copy constructor to copy a Curves object of type U.
	/// Special case that must be here in the header because it has
	/// a second template parameter.
	/// </summary>
	/// <param name="curves">The Curves object to copy</param>
	template <typename U>
	Curves(const Curves<U>& curves)
	{
		Curves<T>::operator=<U>(curves);
	}

	/// <summary>
	/// Default assignment operator.
	/// </summary>
	/// <param name="curves">The Curves object to copy</param>
	Curves<T>& operator = (const Curves<T>& curves)
	{
		if (this != &curves)
			Curves<T>::operator=<T>(curves);

		return *this;
	}

	/// <summary>
	/// Assignment operator to assign a Curves object of type U.
	/// </summary>
	/// <param name="curves">The Curves object to copy</param>
	/// <returns>Reference to updated self</returns>
	template <typename U>
	Curves<T>& operator = (const Curves<U>& curves)
	{
		for (size_t i = 0; i < 4; i++)
		{
			m_Points[i][0].x = T(curves.m_Points[i][0].x); m_Points[i][0].y = T(curves.m_Points[i][0].y); m_Weights[i].x = T(curves.m_Weights[i].x);
			m_Points[i][1].x = T(curves.m_Points[i][1].x); m_Points[i][1].y = T(curves.m_Points[i][1].y); m_Weights[i].y = T(curves.m_Weights[i].y);
			m_Points[i][2].x = T(curves.m_Points[i][2].x); m_Points[i][2].y = T(curves.m_Points[i][2].y); m_Weights[i].z = T(curves.m_Weights[i].z);
			m_Points[i][3].x = T(curves.m_Points[i][3].x); m_Points[i][3].y = T(curves.m_Points[i][3].y); m_Weights[i].w = T(curves.m_Weights[i].w);
		}

		return *this;
	}

	/// <summary>
	/// Unary addition operator to add a Curves<T> object to this one.
	/// </summary>
	/// <param name="curves">The Curves object to add</param>
	/// <returns>Reference to updated self</returns>
	Curves<T>& operator += (const Curves<T>& curves)
	{
		for (size_t i = 0; i < 4; i++)
		{
			m_Points[i][0] += curves.m_Points[i][0];
			m_Points[i][1] += curves.m_Points[i][1];
			m_Points[i][2] += curves.m_Points[i][2];
			m_Points[i][3] += curves.m_Points[i][3];

			m_Weights[i] += curves.m_Weights[i];
		}

		return *this;
	}

	/// <summary>
	/// Unary multiplication operator to multiply this object by another Curves<T> object.
	/// </summary>
	/// <param name="curves">The Curves object to multiply this one by</param>
	/// <returns>Reference to updated self</returns>
	Curves<T>& operator *= (const Curves<T>& curves)
	{
		for (size_t i = 0; i < 4; i++)
		{
			m_Points[i][0] *= curves.m_Points[i][0];
			m_Points[i][1] *= curves.m_Points[i][1];
			m_Points[i][2] *= curves.m_Points[i][2];
			m_Points[i][3] *= curves.m_Points[i][3];

			m_Weights[i] *= curves.m_Weights[i];
		}

		return *this;
	}

	/// <summary>
	/// Unary multiplication operator to multiply this object by a scalar of type T.
	/// </summary>
	/// <param name="t">The scalar to multiply this object by</param>
	/// <returns>Reference to updated self</returns>
	Curves<T>& operator *= (const T& t)
	{
		for (size_t i = 0; i < 4; i++)
		{
			m_Points[i][0] *= t;
			m_Points[i][1] *= t;
			m_Points[i][2] *= t;
			m_Points[i][3] *= t;

			m_Weights[i] *= t;
		}

		return *this;
	}

	/// <summary>
	/// Set the curve and weight values to their default state.
	/// </summary>
	void Init()
	{
		for (size_t i = 0; i < 4; i++)
		{
			m_Points[i][0] = v2T(0);//0,0 -> 0,0 -> 1,1 -> 1,1.
			m_Points[i][1] = v2T(0);
			m_Points[i][2] = v2T(1);
			m_Points[i][3] = v2T(1);

			m_Weights[i] = v4T(1);
		}
	}

	/// <summary>
	/// Set the curve and weight values to an empty state.
	/// </summary>
	void Clear()
	{
		memset(&m_Points, 0, sizeof(m_Points));
		memset(&m_Weights, 0, sizeof(m_Weights));
	}

	/// <summary>
	/// Whether any points are not the default.
	/// </summary>
	/// <returns>True if any point has been set to a value other than the default, else false.</returns>
	bool CurvesSet()
	{
		bool set = false;

		for (size_t i = 0; i < 4; i++)
		{
			if ((m_Points[i][0] != v2T(0)) ||
				(m_Points[i][1] != v2T(0)) ||
				(m_Points[i][2] != v2T(1)) ||
				(m_Points[i][3] != v2T(1)))
			{
				set = true;
				break;
			}
		}

		return set;
	}

	/// <summary>
	/// Wrapper around calling BezierSolve() on each of the 4 weight and point vectors.
	/// </summary>
	/// <param name="t">The position to apply</param>
	/// <returns>vec4<T> that contains the y component of the solution for each vector in each component</returns>
	v4T BezierFunc(const T& t)
	{
		v4T result;
		v2T solution(0, 0);

		BezierSolve(t, m_Points[0], &m_Weights[0], solution); result.x = solution.y;
		BezierSolve(t, m_Points[1], &m_Weights[1], solution); result.y = solution.y;
		BezierSolve(t, m_Points[2], &m_Weights[2], solution); result.z = solution.y;
		BezierSolve(t, m_Points[3], &m_Weights[3], solution); result.w = solution.y;

		return result;
	}

private:
	/// <summary>
	/// Solve the given point and weight vectors for the given position and store
	/// the output in the solution vec2 passed in.
	/// </summary>
	/// <param name="t">The position to apply</param>
	/// <param name="src">A pointer to an array of 4 vec2</param>
	/// <param name="w">A pointer to an array of 4 weights</param>
	/// <param name="solution">The vec2 to store the solution in</param>
	void BezierSolve(const T& t, v2T* src, v4T* w, v2T& solution)
	{
		T s, s2, s3, t2, t3, nom_x, nom_y, denom;

		s = 1 - t;
		s2 = s * s;
		s3 = s * s * s;
		t2 = t * t;
		t3 = t * t * t;

		nom_x = (w->x * s3 * src->x) + (w->y * s2 * 3 * t * src[1].x) + (w->z * s * 3 * t2 * src[2].x) + (w->w * t3 * src[3].x);

		nom_y = (w->x * s3 * src->y) + (w->y * s2 * 3 * t * src[1].y) + (w->z * s * 3 * t2 * src[2].y) + (w->w * t3 * src[3].y);

		denom = (w->x * s3) + (w->y * s2 * 3 * t) + (w->z * s * 3 * t2) + (w->w * t3);


		if (std::isnan(nom_x) || std::isnan(nom_y) || std::isnan(denom) || denom == 0)
			return;

		solution.x = nom_x / denom;
		solution.y = nom_y / denom;
	}

public:
	v2T m_Points[4][4];
	v4T m_Weights[4];
};

//Must declare this outside of the class to provide for both orders of parameters.

/// <summary>
/// Multiplication operator to multiply a Curves<T> object by a scalar of type T.
/// </summary>
/// <param name="curves">The curves object to multiply</param>
/// <param name="t">The scalar to multiply curves by by</param>
/// <returns>Copy of new Curves<T></returns>
template<typename T>
Curves<T> operator * (const Curves<T>& curves, const T& t)
{
	Curves<T> c(curves);

	for (size_t i = 0; i < 4; i++)
	{
		c.m_Points[i][0] *= t;
		c.m_Points[i][1] *= t;
		c.m_Points[i][2] *= t;
		c.m_Points[i][3] *= t;

		c.m_Weights[i] *= t;
	}

	return c;
}

/// <summary>
/// Multiplication operator for reverse order.
/// </summary>
/// <param name="t">The scalar to multiply curves by by</param>
/// <param name="curves">The curves object to multiply</param>
/// <returns>Copy of new Curves<T></returns>
template<typename T>
Curves<T> operator * (const T& t, const Curves<T>& curves)
{
	return curves * t;
}
}