/*
 * Copyright 2025 Vsevolod Stakhov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "css_value.hxx"
#include "css_colors_list.hxx"
#include "frozen/unordered_map.h"
#include "frozen/string.h"
#include "libutil/util.h"
#include "contrib/ankerl/unordered_dense.h"
#include "contrib/fmt/include/fmt/base.h"

#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
#include "doctest/doctest.h"

/* Helper for unit test stringification */
namespace doctest {
template<>
struct StringMaker<rspamd::css::css_color> {
	static String convert(const rspamd::css::css_color &value)
	{
		return fmt::format("r={};g={};b={};alpha={}",
						   value.r, value.g, value.b, value.alpha)
			.c_str();
	}
};

}// namespace doctest

namespace rspamd::css {

auto css_value::maybe_color_from_string(const std::string_view &input)
	-> std::optional<css_value>
{

	if (input.size() > 1 && input.front() == '#') {
		return css_value::maybe_color_from_hex(input.substr(1));
	}
	else {
		auto found_it = css_colors_map.find(input);

		if (found_it != css_colors_map.end()) {
			return css_value{found_it->second};
		}
	}

	return std::nullopt;
}

constexpr static inline auto hexpair_decode(char c1, char c2) -> std::uint8_t
{
	std::uint8_t ret = 0;

	if (c1 >= '0' && c1 <= '9') ret = c1 - '0';
	else if (c1 >= 'A' && c1 <= 'F')
		ret = c1 - 'A' + 10;
	else if (c1 >= 'a' && c1 <= 'f')
		ret = c1 - 'a' + 10;

	ret *= 16;

	if (c2 >= '0' && c2 <= '9') ret += c2 - '0';
	else if (c2 >= 'A' && c2 <= 'F')
		ret += c2 - 'A' + 10;
	else if (c2 >= 'a' && c2 <= 'f')
		ret += c2 - 'a' + 10;

	return ret;
}

auto css_value::maybe_color_from_hex(const std::string_view &input)
	-> std::optional<css_value>
{
	if (input.length() == 6) {
		/* Plain RGB */
		css_color col(hexpair_decode(input[0], input[1]),
					  hexpair_decode(input[2], input[3]),
					  hexpair_decode(input[4], input[5]));
		return css_value(col);
	}
	else if (input.length() == 3) {
		/* Rgb as 3 hex digests */
		css_color col(hexpair_decode(input[0], input[0]),
					  hexpair_decode(input[1], input[1]),
					  hexpair_decode(input[2], input[2]));
		return css_value(col);
	}
	else if (input.length() == 8) {
		/* RGBA */
		css_color col(hexpair_decode(input[0], input[1]),
					  hexpair_decode(input[2], input[3]),
					  hexpair_decode(input[4], input[5]),
					  hexpair_decode(input[6], input[7]));
		return css_value(col);
	}

	return std::nullopt;
}

constexpr static inline auto rgb_color_component_convert(const css_parser_token &tok)
	-> std::uint8_t
{
	std::uint8_t ret = 0;

	if (tok.type == css_parser_token::token_type::number_token) {
		auto dbl = std::get<float>(tok.value);

		if (tok.flags & css_parser_token::number_percent) {
			if (dbl > 100) {
				dbl = 100;
			}
			else if (dbl < 0) {
				dbl = 0;
			}
			ret = (std::uint8_t) (dbl / 100.0 * 255.0);
		}
		else {
			if (dbl > 255) {
				dbl = 255;
			}
			else if (dbl < 0) {
				dbl = 0;
			}

			ret = (std::uint8_t) (dbl);
		}
	}

	return ret;
}

constexpr static inline auto alpha_component_convert(const css_parser_token &tok)
	-> std::uint8_t
{
	double ret = 1.0;

	if (tok.type == css_parser_token::token_type::number_token) {
		auto dbl = std::get<float>(tok.value);

		if (tok.flags & css_parser_token::number_percent) {
			if (dbl > 100) {
				dbl = 100;
			}
			else if (dbl < 0) {
				dbl = 0;
			}
			ret = (dbl / 100.0);
		}
		else {
			if (dbl > 1.0) {
				dbl = 1.0;
			}
			else if (dbl < 0) {
				dbl = 0;
			}

			ret = dbl;
		}
	}

	return (std::uint8_t) (ret * 255.0);
}

constexpr static inline auto h_component_convert(const css_parser_token &tok)
	-> double
{
	double ret = 0.0;

	if (tok.type == css_parser_token::token_type::number_token) {
		auto dbl = std::get<float>(tok.value);

		if (tok.flags & css_parser_token::number_percent) {
			if (dbl > 100) {
				dbl = 100;
			}
			else if (dbl < 0) {
				dbl = 0;
			}
			ret = (dbl / 100.0);
		}
		else {
			dbl = ((((int) dbl % 360) + 360) % 360); /* Deal with rotations */
			ret = dbl / 360.0;                       /* Normalize to 0..1 */
		}
	}

	return ret;
}

constexpr static inline auto sl_component_convert(const css_parser_token &tok)
	-> double
{
	double ret = 0.0;

	if (tok.type == css_parser_token::token_type::number_token) {
		ret = tok.get_normal_number_or_default(ret);
	}

	return ret;
}

static inline auto hsl_to_rgb(double h, double s, double l)
	-> css_color
{
	css_color ret;

	constexpr auto hue2rgb = [](auto p, auto q, auto t) -> auto {
		if (t < 0.0) {
			t += 1.0;
		}
		if (t > 1.0) {
			t -= 1.0;
		}
		if (t * 6. < 1.0) {
			return p + (q - p) * 6.0 * t;
		}
		if (t * 2. < 1) {
			return q;
		}
		if (t * 3. < 2.) {
			return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
		}
		return p;
	};

	if (s == 0) {
		/* Achromatic */
		ret.r = l;
		ret.g = l;
		ret.b = l;
	}
	else {
		auto q = l <= 0.5 ? l * (1.0 + s) : l + s - l * s;
		auto p = 2.0 * l - q;
		ret.r = (std::uint8_t) (hue2rgb(p, q, h + 1.0 / 3.0) * 255);
		ret.g = (std::uint8_t) (hue2rgb(p, q, h) * 255);
		ret.b = (std::uint8_t) (hue2rgb(p, q, h - 1.0 / 3.0) * 255);
	}

	ret.alpha = 255;

	return ret;
}

auto css_value::maybe_color_from_function(const css_consumed_block::css_function_block &func)
	-> std::optional<css_value>
{

	if (func.as_string() == "rgb" && func.args.size() == 3) {
		css_color col{rgb_color_component_convert(func.args[0]->get_token_or_empty()),
					  rgb_color_component_convert(func.args[1]->get_token_or_empty()),
					  rgb_color_component_convert(func.args[2]->get_token_or_empty())};

		return css_value(col);
	}
	else if (func.as_string() == "rgba" && func.args.size() == 4) {
		css_color col{rgb_color_component_convert(func.args[0]->get_token_or_empty()),
					  rgb_color_component_convert(func.args[1]->get_token_or_empty()),
					  rgb_color_component_convert(func.args[2]->get_token_or_empty()),
					  alpha_component_convert(func.args[3]->get_token_or_empty())};

		return css_value(col);
	}
	else if (func.as_string() == "hsl" && func.args.size() == 3) {
		auto h = h_component_convert(func.args[0]->get_token_or_empty());
		auto s = sl_component_convert(func.args[1]->get_token_or_empty());
		auto l = sl_component_convert(func.args[2]->get_token_or_empty());

		auto col = hsl_to_rgb(h, s, l);

		return css_value(col);
	}
	else if (func.as_string() == "hsla" && func.args.size() == 4) {
		auto h = h_component_convert(func.args[0]->get_token_or_empty());
		auto s = sl_component_convert(func.args[1]->get_token_or_empty());
		auto l = sl_component_convert(func.args[2]->get_token_or_empty());

		auto col = hsl_to_rgb(h, s, l);
		col.alpha = alpha_component_convert(func.args[3]->get_token_or_empty());

		return css_value(col);
	}

	return std::nullopt;
}

auto css_value::maybe_dimension_from_number(const css_parser_token &tok)
	-> std::optional<css_value>
{
	if (std::holds_alternative<float>(tok.value)) {
		auto dbl = std::get<float>(tok.value);
		css_dimension dim;

		dim.dim = dbl;

		if (tok.flags & css_parser_token::number_percent) {
			dim.is_percent = true;
		}
		else {
			dim.is_percent = false;
		}

		return css_value{dim};
	}

	return std::nullopt;
}

constexpr const auto display_names_map = frozen::make_unordered_map<frozen::string, css_display_value>({
	{"hidden", css_display_value::DISPLAY_HIDDEN},
	{"collapse", css_display_value::DISPLAY_HIDDEN},
	{"none", css_display_value::DISPLAY_HIDDEN},
	{"inline", css_display_value::DISPLAY_INLINE},
	{"block", css_display_value::DISPLAY_BLOCK},
	{"content", css_display_value::DISPLAY_INLINE},
	{"flex", css_display_value::DISPLAY_BLOCK},
	{"grid", css_display_value::DISPLAY_BLOCK},
	{"inline-block", css_display_value::DISPLAY_INLINE},
	{"inline-flex", css_display_value::DISPLAY_INLINE},
	{"inline-grid", css_display_value::DISPLAY_INLINE},
	{"inline-table", css_display_value::DISPLAY_INLINE},
	{"list-item", css_display_value::DISPLAY_BLOCK},
	{"run-in", css_display_value::DISPLAY_INLINE},
	{"table", css_display_value::DISPLAY_BLOCK},
	{"table-caption", css_display_value::DISPLAY_TABLE_ROW},
	{"table-column-group", css_display_value::DISPLAY_TABLE_ROW},
	{"table-header-group", css_display_value::DISPLAY_TABLE_ROW},
	{"table-footer-group", css_display_value::DISPLAY_TABLE_ROW},
	{"table-row-group", css_display_value::DISPLAY_TABLE_ROW},
	{"table-cell", css_display_value::DISPLAY_TABLE_ROW},
	{"table-column", css_display_value::DISPLAY_TABLE_ROW},
	{"table-row", css_display_value::DISPLAY_TABLE_ROW},
	{"initial", css_display_value::DISPLAY_INLINE},
});

auto css_value::maybe_display_from_string(const std::string_view &input)
	-> std::optional<css_value>
{
	auto f = display_names_map.find(input);

	if (f != display_names_map.end()) {
		return css_value{f->second};
	}

	return std::nullopt;
}


auto css_value::debug_str() const -> std::string
{
	std::string ret;

	std::visit([&](const auto &arg) {
		using T = std::decay_t<decltype(arg)>;

		if constexpr (std::is_same_v<T, css_color>) {
			ret += fmt::format("color: r={};g={};b={};alpha={}",
							   arg.r, arg.g, arg.b, arg.alpha);
		}
		else if constexpr (std::is_same_v<T, double>) {
			ret += "size: " + std::to_string(arg);
		}
		else if constexpr (std::is_same_v<T, css_dimension>) {
			ret += "dimension: " + std::to_string(arg.dim);
			if (arg.is_percent) {
				ret += "%";
			}
		}
		else if constexpr (std::is_same_v<T, css_display_value>) {
			ret += "display: ";
			switch (arg) {
			case css_display_value::DISPLAY_HIDDEN:
				ret += "hidden";
				break;
			case css_display_value::DISPLAY_BLOCK:
				ret += "block";
				break;
			case css_display_value::DISPLAY_INLINE:
				ret += "inline";
				break;
			case css_display_value::DISPLAY_TABLE_ROW:
				ret += "table_row";
				break;
			}
		}
		else if constexpr (std::is_integral_v<T>) {
			ret += "integral: " + std::to_string(static_cast<int>(arg));
		}
		else {
			ret += "nyi";
		}
	},
			   value);

	return ret;
}

TEST_SUITE("css"){
	TEST_CASE("css hex colors"){
		const std::pair<const char *, css_color> hex_tests[] = {
			{"000", css_color(0, 0, 0)},
			{"000000", css_color(0, 0, 0)},
			{"f00", css_color(255, 0, 0)},
			{"FEDCBA", css_color(254, 220, 186)},
			{"234", css_color(34, 51, 68)},
		};

for (const auto &p: hex_tests) {
	SUBCASE((std::string("parse hex color: ") + p.first).c_str())
	{
		auto col_parsed = css_value::maybe_color_from_hex(p.first);
		//CHECK_UNARY(col_parsed);
		//CHECK_UNARY(col_parsed.value().to_color());
		auto final_col = col_parsed.value().to_color().value();
		CHECK(final_col == p.second);
	}
}
}// namespace rspamd::css
TEST_CASE("css colors strings")
{
	auto passed = 0;
	for (const auto &p: css_colors_map) {
		/* Match some of the colors selected randomly */
		if (rspamd_random_double_fast() > 0.9) {
			auto col_parsed = css_value::maybe_color_from_string(p.first);
			auto final_col = col_parsed.value().to_color().value();
			CHECK_MESSAGE(final_col == p.second, p.first.data());
			passed++;

			if (passed > 20) {
				break;
			}
		}
	}
}
}
;
}
