#pragma once

#include <psemek/math/simplex.hpp>
#include <psemek/math/vector.hpp>
#include <psemek/math/point.hpp>

#include <vector>
#include <array>
#include <tuple>
#include <algorithm>

namespace psemek::cg
{

	/* The basic interface of a 2D body is
	 *   body_traits<Body>::static dimension -> 2
	 *   vertices(body) -> [point]
	 *   edges(body) -> [(index, index)]
	 *
	 * The basic interface of a 3D body is
	 *   body_traits<Body>::static dimension -> 3
	 *   vertices(body) -> [point]
	 *   edges(body) -> [(index, index)]
	 *   faces(body) -> [[index]]
	 *   triangles(body) -> [(index, index, index)]
	 *   face_normals(body) -> [vector]
	 *   edge_directions(body) -> [vector]
	 *
	 * Note that faces(body).size() == face_normals(body).size
	 * However, it is possible that edges(body).size() != edge_directions(body).size,
	 *   if there are many collinear edges.
	 *
	 * Minimal set of required functions for 2D body:
	 *   vertices
	 *   edges
	 *
	 * Minimal set of required functions for 3D body:
	 *   vertices
	 *   edges
	 *   faces/triangles
	 */

	namespace detail
	{

		template <typename Container>
		struct has_static_size
			: std::false_type
		{};

		template <typename T, std::size_t N>
		struct has_static_size<std::array<T, N>>
			: std::true_type
		{};

		template <typename Container>
		constexpr bool has_static_size_v = has_static_size<Container>::value;

		template <typename Container>
		struct static_size;

		template <typename T, std::size_t N>
		struct static_size<std::array<T, N>>
		{
			static constexpr std::size_t value = N;
		};

		template <typename Container>
		constexpr std::size_t static_size_v = static_size<Container>::value;

	}

	template <typename Body>
	auto vertices(Body const & b)
	{
		return b.vertices;
	}

	template <typename Body>
	constexpr std::size_t dimension = std::decay_t<decltype(vertices(std::declval<Body>())[0])>::static_dimension;

	template <typename Body>
	auto faces(Body const & b)
	{
		return triangles(b);
	}

	template <typename Body>
	auto edges(Body const & b)
	{
		auto const & fs = faces(b);

		using index_type = std::remove_cvref_t<decltype((*std::begin(fs))[0])>;

		using std::begin;
		using std::end;

		std::vector<math::segment<index_type>> result;
		for (auto const & f : fs)
		{
			for (auto it = begin(f), jt = std::prev(end(f)); it != end(f); jt = it++)
			{
				math::segment<index_type> s{*jt, *it};
				if (s[0] > s[1]) std::swap(s[0], s[1]);
				result.push_back(s);
			}
		}

		auto compare = [](auto const & s1, auto const & s2)
		{
			return std::tie(s1[0], s1[1]) < std::tie(s2[0], s2[1]);
		};

		std::sort(result.begin(), result.end(), compare);
		result.erase(std::unique(result.begin(), result.end()), result.end());
		return result;
	}

	template <typename Body>
	auto triangles(Body const & b)
	{
		auto const & fs = faces(b);

		using faces_type = std::remove_cvref_t<decltype(fs)>;
		using face_type = std::remove_cvref_t<decltype(*std::begin(fs))>;
		using index_type = std::remove_cvref_t<decltype((*std::begin(fs))[0])>;

		auto impl = [&fs](auto out)
		{
			for (auto const & f : fs)
			{
				auto it0 = std::begin(f);

				for (auto it = std::next(it0), jt = std::next(it); jt < std::end(f); it = jt++)
				{
					*out++ = {*it0, *it, *jt};
				}
			}
		};

		if constexpr (detail::has_static_size_v<faces_type> && detail::has_static_size_v<face_type>)
		{
			constexpr std::size_t faces_count = detail::static_size_v<faces_type>;
			constexpr std::size_t face_size = detail::static_size_v<face_type>;
			std::array<math::triangle<index_type>, faces_count * (face_size - 2)> result;
			impl(result.begin());
			return result;
		}
		else
		{
			std::vector<math::triangle<index_type>> result;
			if constexpr (detail::has_static_size_v<face_type>)
			{
				constexpr std::size_t face_size = detail::static_size_v<face_type>;
				result.reserve(fs.size() * (face_size - 2));
			}
			else
			{
				result.reserve(fs.size());
			}
			impl(std::back_inserter(result));
			return result;
		}
	}

	template <typename Body>
	auto edge_directions(Body const & b)
	{
		auto const & vs = vertices(b);
		auto const & es = edges(b);

		using edges_type = std::remove_cvref_t<decltype(es)>;
		using vector_type = std::remove_cvref_t<decltype(vs[0] - vs[0])>;

		auto impl = [&es, &vs](auto out)
		{
			for (auto const & e : es)
			{
				*out++ = math::normalized(vs[e[1]] - vs[e[0]]);
			}
		};

		if constexpr (detail::has_static_size_v<edges_type>)
		{
			constexpr std::size_t edges_count = detail::static_size_v<edges_type>;
			std::array<vector_type, edges_count> result;
			impl(result.begin());
			return result;
		}
		else
		{
			std::vector<vector_type> result;
			result.reserve(es.size());
			impl(std::back_inserter(result));
			return result;
		}
	}

	template <typename Body>
	auto face_normals(Body const & b)
	{
		auto const & vs = vertices(b);
		auto const & fs = faces(b);

		using faces_type = std::remove_cvref_t<decltype(fs)>;
		using vector_type = std::remove_cvref_t<decltype(vs[0] - vs[0])>;

		auto impl = [&fs, &vs](auto out)
		{
			for (auto const & f : fs)
			{
				*out++ = math::normal(vs[f[0]], vs[f[1]], vs[f[2]]);
			}
		};

		if constexpr (detail::has_static_size_v<faces_type>)
		{
			constexpr std::size_t faces_count = detail::static_size_v<faces_type>;
			std::array<vector_type, faces_count> result;
			impl(result.begin());
			return result;
		}
		else
		{
			std::vector<vector_type> result;
			result.reserve(fs.size());
			impl(std::back_inserter(result));
			return result;
		}
	}

}
