#pragma once

#include <psemek/math/vector.hpp>
#include <psemek/math/interval.hpp>
#include <psemek/math/math.hpp>

#include <cstdint>
#include <string_view>
#include <optional>

namespace psemek::gfx
{

	using color_3f = math::vector<float, 3>;
	using color_4f = math::vector<float, 4>;

	using color_rgb = math::vector<std::uint8_t, 3>;
	using color_rgba = math::vector<std::uint8_t, 4>;

	template <std::size_t N>
	auto to_colorf(math::vector<std::uint8_t, N> const & c)
	{
		math::vector<float, N> r;
		for (std::size_t i = 0; i < N; ++i)
			r[i] = c[i] / 255.f;
		return r;
	}

	template <std::size_t N>
	auto to_colorf(math::vector<std::uint16_t, N> const & c)
	{
		math::vector<float, N> r;
		for (std::size_t i = 0; i < N; ++i)
			r[i] = c[i] / 65535.f;
		return r;
	}

	template <std::size_t N>
	auto to_coloru8(math::vector<std::uint16_t, N> const & c)
	{
		math::vector<std::uint8_t, N> r;
		for (std::size_t i = 0; i < N; ++i)
			r[i] = (static_cast<std::uint32_t>(c[i]) * 255) / 65535;
		return r;
	}

	template <std::size_t N>
	auto to_coloru8(math::vector<float, N> const & c)
	{
		math::vector<std::uint8_t, N> r;
		for (std::size_t i = 0; i < N; ++i)
			r[i] = static_cast<std::uint8_t>(std::round(math::clamp(c[i] * 255.f, {0.f, 255.f})));
		return r;
	}

	template <std::size_t N>
	auto to_coloru16(math::vector<std::uint8_t, N> const & c)
	{
		math::vector<std::uint16_t, N> r;
		for (std::size_t i = 0; i < N; ++i)
			r[i] = (static_cast<std::uint32_t>(c[i]) * 65535) / 255;
		return r;
	}

	template <std::size_t N>
	auto to_coloru16(math::vector<float, N> const & c)
	{
		math::vector<std::uint16_t, N> r;
		for (std::size_t i = 0; i < N; ++i)
			r[i] = static_cast<std::uint16_t>(std::round(math::clamp(c[i] * 65535.f, {0.f, 65535.f})));
		return r;
	}

	template <std::size_t N>
	auto lerp(math::vector<float, N> const & c0, math::vector<float, N> const & c1, float t)
	{
		return math::lerp(c0, c1, t);
	}

	template <std::size_t N>
	auto lerp(math::vector<std::uint8_t, N> const & c0, math::vector<std::uint8_t, N> const & c1, float t)
	{
		return to_coloru8(lerp(to_colorf(c0), to_colorf(c1), t));
	}

	template <std::size_t N>
	auto to_srgb(math::vector<float, N> const & c, float g = 1.f / 2.2f)
	{
		math::vector<float, N> r = c;
		for (std::size_t i = 0; i < std::min<std::size_t>(3, N); ++i)
			r[i] = std::pow(r[i], g);
		return r;
	}

	template <std::size_t N>
	auto to_srgb(math::vector<std::uint8_t, N> const & c, float g = 1.f / 2.2f)
	{
		return to_coloru8(to_srgb(to_colorf(c), g));
	}

	template <std::size_t N>
	auto to_srgb(math::vector<std::uint16_t, N> const & c, float g = 1.f / 2.2f)
	{
		return to_coloru16(to_srgb(to_colorf(c), g));
	}

	template <typename T, std::size_t N>
	auto to_linear(math::vector<T, N> const & c, float g = 1.f / 2.2f)
	{
		return to_srgb(c, 1.f / g);
	}

	inline math::vector<float, 4> premult(math::vector<float, 4> const & c)
	{
		return {c[0] * c[3], c[1] * c[3], c[2] * c[3], c[3]};
	}

	inline math::vector<std::uint8_t, 4> premult(math::vector<std::uint8_t, 4> const & c)
	{
		return {
			(static_cast<std::uint16_t>(c[0]) * c[3]) / 255,
			(static_cast<std::uint16_t>(c[1]) * c[3]) / 255,
			(static_cast<std::uint16_t>(c[2]) * c[3]) / 255,
			c[3]
		};
	}

	inline math::vector<std::uint16_t, 4> premult(math::vector<std::uint16_t, 4> const & c)
	{
		return {
			(static_cast<std::uint32_t>(c[0]) * c[3]) / 65535,
			(static_cast<std::uint32_t>(c[1]) * c[3]) / 65535,
			(static_cast<std::uint32_t>(c[2]) * c[3]) / 65535,
			c[3]
		};
	}

	inline math::vector<float, 4> unpremult(math::vector<float, 4> const & c)
	{
		if (c[3] == 0.f)
			return math::vector<float, 4>::zero();

		return {c[0] / c[3], c[1] / c[3], c[2] / c[3], c[3]};
	}

	inline math::vector<std::uint8_t, 4> unpremult(math::vector<std::uint8_t, 4> const & c)
	{
		if (c[3] == 0)
			return math::vector<std::uint8_t, 4>::zero();

		return {
			(static_cast<std::uint16_t>(c[0]) * 255) / c[3],
			(static_cast<std::uint16_t>(c[1]) * 255) / c[3],
			(static_cast<std::uint16_t>(c[2]) * 255) / c[3],
			c[3]
		};
	}

	inline math::vector<std::uint16_t, 4> unpremult(math::vector<std::uint16_t, 4> const & c)
	{
		if (c[3] == 0)
			return math::vector<std::uint16_t, 4>::zero();

		return {
			(static_cast<std::uint32_t>(c[0]) * 65535) / c[3],
			(static_cast<std::uint32_t>(c[1]) * 65535) / c[3],
			(static_cast<std::uint32_t>(c[2]) * 65535) / c[3],
			c[3]
		};
	}

	inline color_4f blend(color_4f const & c0, color_4f const & c1)
	{
		color_4f result = c0 * c0[3] * (1.f - c1[3]) + c1 * c1[3];
		result[3] = c0[3] + c1[3] - c0[3] * c1[3];
		return unpremult(result);
	}

	inline color_rgba blend(color_rgba const & c0, color_rgba const & c1)
	{
		return to_coloru8(blend(to_colorf(c0), to_colorf(c1)));
	}

	template <std::size_t N>
	auto blend(math::vector<std::uint8_t, N> const & c0, math::vector<std::uint8_t, N> const & c1, float t)
	{
		return to_coloru8(blend(to_colorf(c0), to_colorf(c1), t));
	}

	template <std::size_t N>
	struct as_hex
	{
		math::vector<std::uint8_t, N> color;

		as_hex(math::vector<std::uint8_t, N> const & color)
			: color{color}
		{}
	};

	template <typename OStream, std::size_t N>
	OStream & operator << (OStream & os, as_hex<N> const & c)
	{
		static auto const hex_v = [](std::uint8_t value) -> char
		{
			if (value < 10)
				return '0' + value;
			else
				return 'a' + (value - 10);
		};

		for (auto v : c.color.coords)
			os << hex_v(v >> 4) << hex_v(v & 15);

		return os;
	}

	struct generic_color
	{
		color_4f c;

		auto as_color_rgb() const
		{
			return to_coloru8(math::vector{c[0], c[1], c[2]});
		}

		auto as_color_rgba() const
		{
			return to_coloru8(c);
		}

		auto as_color_3f() const
		{
			return color_3f{c[0], c[1], c[2]};
		}

		auto as_color_4f() const
		{
			return c;
		}

		operator color_rgb() const
		{
			return as_color_rgb();
		}

		operator color_rgba() const
		{
			return as_color_rgba();
		}

		operator color_3f() const
		{
			return as_color_3f();
		}

		operator color_4f() const
		{
			return c;
		}
	};

	inline generic_color lerp(generic_color const & c0, generic_color const & c1, float t)
	{
		return generic_color{lerp(c0.c, c1.c, t)};
	}

	static const generic_color white  {{1.f, 1.f, 1.f, 1.f}};
	static const generic_color black  {{0.f, 0.f, 0.f, 1.f}};

	static const generic_color red    {{1.f, 0.f, 0.f, 1.f}};
	static const generic_color green  {{0.f, 1.f, 0.f, 1.f}};
	static const generic_color blue   {{0.f, 0.f, 1.f, 1.f}};

	static const generic_color cyan   {{0.f, 1.f, 1.f, 1.f}};
	static const generic_color magenta{{1.f, 0.f, 1.f, 1.f}};
	static const generic_color yellow {{1.f, 1.f, 0.f, 1.f}};

	static const generic_color gray {{0.50f, 0.50f, 0.50f, 1.f}};

	template <std::size_t N>
	math::vector<float, N> light(math::vector<float, N> c, float lightness = 0.5f)
	{
		static_assert(N == 3 || N == 4);
		for (std::size_t i = 0; i < 3; ++i)
			c[i] = math::lerp(c[i], 1.f, lightness);
		return c;
	}

	template <std::size_t N>
	math::vector<float, N> dark(math::vector<float, N> c, float darkness = 0.5f)
	{
		static_assert(N == 3 || N == 4);
		for (std::size_t i = 0; i < 3; ++i)
			c[i] = math::lerp(c[i], 0.f, darkness);
		return c;
	}

	template <std::size_t N>
	math::vector<std::uint8_t, N> light(math::vector<std::uint8_t, N> c, float lightness = 0.5f)
	{
		return to_coloru8(light(to_colorf(c), lightness));
	}

	template <std::size_t N>
	math::vector<std::uint8_t, N> dark(math::vector<std::uint8_t, N> c, float darkness = 0.5f)
	{
		return to_coloru8(dark(to_colorf(c), darkness));
	}

	inline generic_color light(generic_color const & c, float lightness = 0.5f)
	{
		return generic_color{light(c.c, lightness)};
	}

	inline generic_color dark(generic_color const & c, float darkness = 0.5f)
	{
		return generic_color{dark(c.c, darkness)};
	}

	template <typename T>
	math::vector<T, 3> rgb_to_hsv(math::vector<T, 3> const & rgb)
	{
		T max = std::max({rgb[0], rgb[1], rgb[2]});
		T min = std::min({rgb[0], rgb[1], rgb[2]});
		T delta = max - min;

		T hue = T{0};

		if (delta > T{0})
		{
			if (rgb[0] > rgb[1] && rgb[0] > rgb[2])
			{
				hue = (rgb[1] - rgb[2]) / delta;
			}
			else if (rgb[1] > rgb[2])
			{
				hue = 2.f + (rgb[2] - rgb[0]) / delta;
			}
			else
			{
				hue = 4.f + (rgb[0] - rgb[1]) / delta;
			}
		}

		hue *= math::rad<T>(60);

		if (hue < 0)
			hue += math::rad<T>(360);

		math::vector<T, 3> result;
		result[0] = hue;
		result[1] = (max > T{0}) ? (delta / max) : T{0};
		result[2] = max;

		return result;
	}

	template <typename T>
	math::vector<T, 3> hsv_to_rgb(math::vector<T, 3> const & hsv)
	{
		auto c = hsv[1] * hsv[2];
		auto x = c * (T{1} - std::abs(std::fmod(hsv[0] / math::rad<T>(60), 2.f) - 1.f));
		auto m = hsv[2] - c;

		math::vector<T, 3> result;

		if (hsv[0] < math::rad<T>(60))
			result = {c, x, T{0}};
		else if (hsv[0] < math::rad<T>(120))
			result = {x, c, T{0}};
		else if (hsv[0] < math::rad<T>(180))
			result = {T{0}, c, x};
		else if (hsv[0] < math::rad<T>(240))
			result = {T{0}, x, c};
		else if (hsv[0] < math::rad<T>(300))
			result = {x, T{0}, c};
		else
			result = {c, T{0}, x};

		result += math::vector{m, m, m};

		return result;
	}

	template <std::floating_point T, std::size_t N>
	T luminance(math::vector<T, N> const & c)
	{
		return c[0] * T(0.299) + c[1] * T(0.587) + c[2] * T(0.114);
	}

	std::optional<color_rgba> parse_color(std::string_view const & text);

}
