#pragma once

#include <psemek/util/hash.hpp>

#include <psemek/math/detail/array.hpp>
#include <psemek/math/vector.hpp>

#include <iostream>
#include <format>

namespace psemek::math
{

	template <typename T, std::size_t N>
	struct point
	{
		using scalar_type = T;
		static constexpr std::size_t static_dimension = N;

		typename detail::array<T, N>::type coords;

		using dynamic_dimensions_type = detail::dynamic_dimensions<N>;

		// Internal
		point(dynamic_dimensions_type dimensions)
			: coords(dimensions)
		{}

		point() = default;

		explicit point(std::size_t size) requires (N == dynamic)
			: coords({.size = size})
		{}

		template <typename ... Args>
		requires(N != dynamic && sizeof...(Args) == N && detail::all_convertible_to<T, Args...>::value)
		point(Args && ... args)
		{
			auto out = values().begin();
			((*out++ = args), ...);
		}

		template <typename ... Args>
		requires(N == dynamic && detail::all_convertible_to<T, Args...>::value)
		point(Args && ... args)
			: coords({.size = sizeof...(Args)})
		{
			auto out = values().begin();
			((*out++ = args), ...);
		}

		std::size_t dimension() const
		{
			return coords.size;
		}

		T & operator[](std::size_t i)
		{
			return coords[i];
		}

		T const & operator[](std::size_t i) const
		{
			return coords[i];
		}

		util::span<T> values()
		{
			return {&coords[0], dimension()};
		}

		util::span<T const> values() const
		{
			return {&coords[0], dimension()};
		}

		point copy() const
		{
			if constexpr (N == dynamic)
			{
				point result;
				result.coords = coords.copy();
				return result;
			}
			else
			{
				return *this;
			}
		}

		point & operator += (vector<T, N> const & v);
		point & operator -= (vector<T, N> const & v);

		static point zero(dynamic_dimensions_type dimensions = {});
	};

	template <typename ... Args>
	point(Args && ...) -> point<std::common_type_t<Args...>, sizeof...(Args)>;

	template <typename T, std::size_t N>
	void check_dynamic_size(point<T, N> const & p1, point<T, N> const & p2)
	{
		check_dynamic_size(p1.coords.dimensions(), p2.coords.dimensions());
	}

	template <typename T, std::size_t N>
	void check_dynamic_size(point<T, N> const & p, vector<T, N> const & v)
	{
		check_dynamic_size(p.coords.dimensions(), v.coords.dimensions());
	}

	template <typename T, std::size_t N>
	point<T, N> point<T, N>::zero(dynamic_dimensions_type dimensions)
	{
		point<T, N> p(dimensions);
		for (std::size_t i = 0; i < p.dimension(); ++i)
			p[i] = T(0);
		return p;
	}

	template <typename T1, typename T, std::size_t N>
	point<T1, N> cast(point<T, N> const & p)
	{
		point<T1, N> r(p.coords.dimensions());
		for (std::size_t i = 0; i < r.dimension(); ++i)
			r[i] = T1(p[i]);
		return r;
	}

	template <typename T, std::size_t N>
	bool operator == (point<T, N> const & p1, point<T, N> const & p2)
	{
		return (p1 <=> p2) == std::strong_ordering::equal;
	}

	template <typename T, std::size_t N>
	bool operator != (point<T, N> const & p1, point<T, N> const & p2)
	{
		return !(p1 == p2);
	}

	template <typename T, std::size_t N>
	bool operator < (point<T, N> const & p1, point<T, N> const & p2)
	{
		return (p1 <=> p2) == std::strong_ordering::less;
	}

	template <typename T, std::size_t N>
	bool operator > (point<T, N> const & p1, point<T, N> const & p2)
	{
		return (p2 < p1);
	}

	template <typename T, std::size_t N>
	bool operator <= (point<T, N> const & p1, point<T, N> const & p2)
	{
		return !(p2 < p1);
	}

	template <typename T, std::size_t N>
	bool operator >= (point<T, N> const & p1, point<T, N> const & p2)
	{
		return !(p1 < p2);
	}

	template <typename T, std::size_t N>
	std::strong_ordering operator <=> (point<T, N> const & p1, point<T, N> const & p2)
	{
		check_dynamic_size(p1, p2);

		for (std::size_t i = 0; i < p1.dimension(); ++i)
		{
			if (p1[i] < p2[i])
				return std::strong_ordering::less;
			else if (p1[i] > p2[i])
				return std::strong_ordering::greater;
		}
		return std::strong_ordering::equal;
	}

	template <typename T, std::size_t N>
	point<T, N> operator + (point<T, N> const & p, vector<T, N> const & v)
	{
		check_dynamic_size(p, v);
		point<T, N> r(p.coords.dimensions());
		for (std::size_t i = 0; i < N; ++i)
			r[i] = p[i] + v[i];
		return r;
	}

	template <typename T, std::size_t N>
	point<T, N> operator + (vector<T, N> const & v, point<T, N> const & p)
	{
		check_dynamic_size(p, v);
		point<T, N> r(p.coords.dimensions());
		for (std::size_t i = 0; i < N; ++i)
			r[i] = v[i] + p[i];
		return r;
	}

	template <typename T, std::size_t N>
	point<T, N> operator - (point<T, N> const & p, vector<T, N> const & v)
	{
		check_dynamic_size(p, v);
		point<T, N> r(p.coords.dimensions());
		for (std::size_t i = 0; i < N; ++i)
			r[i] = p[i] - v[i];
		return r;
	}

	template <typename T, std::size_t N>
	vector<T, N> operator - (point<T, N> const & p1, point<T, N> const & p2)
	{
		check_dynamic_size(p1, p2);
		vector<T, N> r(p1.coords.dimensions());
		for (std::size_t i = 0; i < N; ++i)
			r[i] = p1[i] - p2[i];
		return r;
	}

	template <typename T, std::size_t N>
	point<T, N> & point<T, N>::operator += (vector<T, N> const & v)
	{
		return (*this) = (*this) + v;
	}

	template <typename T, std::size_t N>
	point<T, N> & point<T, N>::operator -= (vector<T, N> const & v)
	{
		return (*this) = (*this) - v;
	}

	template <typename T, std::size_t N>
	point<T, N> lerp(point<T, N> const & p0, point<T, N> const & p1, T const & t)
	{
		return p0 + t * (p1 - p0);
	}

	template <typename T, std::size_t N>
	T distance_sqr(point<T, N> const & p1, point<T, N> const & p2)
	{
		return length_sqr(p2 - p1);
	}

	template <typename T, std::size_t N>
	T distance(point<T, N> const & p1, point<T, N> const & p2)
	{
		return length(p2 - p1);
	}

	template <typename P, typename ... Points>
	auto volume(P const & p0, Points const & ... ps)
	{
		return det((ps - p0)...);
	}

	template <typename P, typename ... Points>
	auto normal(P const & p0, Points const & ... ps)
	{
		return normalized(ort((ps - p0)...));
	}

	template <typename T, std::size_t N>
	std::ostream & operator << (std::ostream & os, point<T, N> const & p)
	{
		if constexpr (N == 0)
		{
			os << "()";
			return os;
		}

		os << '(' << p[0];
		for (std::size_t i = 1; i < p.dimension(); ++i)
			os << ", " << p[i];
		os << ')';
		return os;
	}

	template <typename T, std::size_t N>
	bool isfinite(point<T, N> const & p)
	{
		for (std::size_t i = 0; i < p.dimension(); ++i)
			if (!std::isfinite(p[i]))
				return false;
		return true;
	}

}

namespace std
{

	template <typename T, std::size_t N>
	struct hash<::psemek::math::point<T, N>>
	{
		std::uint64_t operator()(::psemek::math::point<T, N> const & p) const noexcept
		{
			hash<T> h;
			std::uint64_t r = 0;
			for (std::size_t i = 0; i < N; ++i)
				::psemek::util::hash_combine(r, h(p[i]));
			return r;
		}
	};

	template <typename T, std::size_t N, typename Char>
	struct formatter<::psemek::math::point<T, N>, Char>
		: formatter<T, Char>
	{
		using formatter<T, Char>::parse;

		template <typename FormatContext>
		auto format(::psemek::math::point<T, N> const & p, FormatContext & ctx) const
		{
			if constexpr (N == 0)
			{
				return std::format_to(ctx.out(), "()");
			}
			else
			{
				ctx.advance_to(std::format_to(ctx.out(), "("));
				for (std::size_t i = 0; i < p.dimension(); ++i)
				{
					if (i > 0) ctx.advance_to(std::format_to(ctx.out(), ", "));
					ctx.advance_to(formatter<T, Char>::format(p[i], ctx));
				}
				return std::format_to(ctx.out(), ")");
			}
		}
	};

}
