/*
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *
 *  Copyright (C) 2023-2023  The DOSBox Staging Team
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "bgrx8888.h"
#include "rgb.h"
#include "rgb565.h"
#include "rgb888.h"

#include <gtest/gtest.h>

constexpr uint8_t rgb5_to_8_reference(const uint8_t c)
{
	return check_cast<uint8_t>((c * 255 + 15) / 31);
}

constexpr uint8_t rgb6_to_8_reference(const uint8_t c)
{
	return check_cast<uint8_t>((c * 255 + 31) / 63);
}

constexpr auto AbsError = 0.000001f;

TEST(rgb, Rgb888_FromRgb444)
{
	EXPECT_EQ(Rgb888::FromRgb444(0x0, 0x1, 0x2), Rgb888(0x00, 0x11, 0x22));
	EXPECT_EQ(Rgb888::FromRgb444(0x8, 0xe, 0xf), Rgb888(0x88, 0xee, 0xff));
}

TEST(rgb, rgb5_to_8)
{
	for (auto c = 0; c <= Rgb5Max; ++c) {
		EXPECT_EQ(rgb5_to_8(c), rgb5_to_8_reference(c));
	}
}

TEST(rgb, rgb6_to_8)
{
	for (auto c = 0; c <= Rgb6Max; ++c) {
		EXPECT_EQ(rgb6_to_8(c), rgb6_to_8_reference(c));
	}
}

TEST(rgb, rgb8_to_5)
{
	for (auto c = 0; c <= Rgb5Max; ++c) {
		const auto c8 = rgb5_to_8_reference(c);
		EXPECT_EQ(rgb8_to_5(c8), c);
	}
}

TEST(rgb, rgb8_to_6)
{
	for (auto c = 0; c <= Rgb6Max; ++c) {
		const auto c8 = rgb6_to_8_reference(c);
		EXPECT_EQ(rgb8_to_6(c8), c);
	}
}

TEST(rgb, rgb5_to_8_lut)
{
	for (auto c = 0; c <= Rgb5Max; ++c) {
		EXPECT_EQ(rgb5_to_8_lut(c), rgb5_to_8_reference(c));
	}
}

TEST(rgb, rgb6_to_8_lut)
{
	for (auto c = 0; c <= Rgb6Max; ++c) {
		EXPECT_EQ(rgb6_to_8_lut(c), rgb6_to_8_reference(c));
	}
}

TEST(rgb, srgb_to_linear)
{
	EXPECT_NEAR(srgb_to_linear(0.0f), 0.0000000f, AbsError);
	EXPECT_NEAR(srgb_to_linear(0.2f), 0.0331048f, AbsError);
	EXPECT_NEAR(srgb_to_linear(0.8f), 0.6038270f, AbsError);
	EXPECT_NEAR(srgb_to_linear(1.0f), 1.0000000f, AbsError);
}

TEST(rgb, linear_to_srgb)
{
	EXPECT_NEAR(linear_to_srgb(0.0000000f), 0.0f, AbsError);
	EXPECT_NEAR(linear_to_srgb(0.0331048f), 0.2f, AbsError);
	EXPECT_NEAR(linear_to_srgb(0.6038270f), 0.8f, AbsError);
	EXPECT_NEAR(linear_to_srgb(1.0000000f), 1.0f, AbsError);
}

TEST(rgb, srgb_linear_roundtrip)
{
	constexpr auto NumIter = 1000;
	for (auto i = 0; i < NumIter; ++i) {
		const float srgb1 = i * (1.0f / NumIter);
		const auto lin    = srgb_to_linear(srgb1);
		const auto srgb2  = linear_to_srgb(lin);
		EXPECT_NEAR(srgb1, srgb2, AbsError);
	}
}

TEST(rgb, srgb8_to_linear_lut)
{
	for (auto c = 0; c <= Rgb8Max; ++c) {
		const auto expected = srgb_to_linear(c * (1.0f / Rgb8Max));
		EXPECT_NEAR(srgb8_to_linear_lut(c), expected, AbsError);
	}
}

TEST(rgb, linear_to_srgb8_lut)
{
	// This is good enough accuracy with a (16 * 1024) element LUT
	constexpr auto NumIter = 500;
	for (auto i = 0; i < NumIter; ++i) {
		const float lin     = i * (1.0f / NumIter);
		const auto expected = static_cast<std::uint8_t>(
		        std::round(linear_to_srgb(lin) * Rgb8Max));
		EXPECT_EQ(linear_to_srgb8_lut(lin), expected);
	}
}

TEST(rgb, type_sizes)
{
	auto as_bits = [](const size_t num_bytes) { return num_bytes * 8; };

	EXPECT_EQ(as_bits(sizeof(Rgb565)), 5 + 6 + 5);
	EXPECT_EQ(as_bits(sizeof(Rgb888)), 8 * 3);
	EXPECT_EQ(as_bits(sizeof(Bgrx8888)), 8 * 4);
}

TEST(rgb, rgb565_pixel_components)
{
	//                                  |  r5 |  g6  |  b5 |
	constexpr uint16_t rgb_as_uint16 = 0b00001'000011'00111;
	constexpr uint8_t r8             = 0b00001'000;
	constexpr uint8_t g8             = /* */ 0b000011'00;
	constexpr uint8_t b8             = /*        */ 0b00111'000;

	const Rgb565 rgb_object_from_uint16(rgb_as_uint16);
	const Rgb565 rgb_object_from_components(r8, g8, b8);

	EXPECT_EQ(rgb_object_from_uint16.pixel, rgb_object_from_components.pixel);
}

TEST(rgb, rgb888_byte_order)
{
	constexpr uint8_t r8 = 0b1000'0011;
	constexpr uint8_t g8 = 0b1000'0111;
	constexpr uint8_t b8 = 0b1000'1111;

	// Create a sequential array of objects
	constexpr Rgb888 rgb_array[3] = {
	        {r8, g8, b8},
	        {r8, g8, b8},
	        {r8, g8, b8},
	};

	// Treat the array as if it were video memory, i.e., a sequence of bytes.
	const auto byte_array = reinterpret_cast<const uint8_t*>(rgb_array);

	// If Rgb888 works properly, the colour values will always be in the
	// same place regardless of the hosts's endianness.
	//
	constexpr auto first_r8_offset  = sizeof(Rgb888) * 0 + 0;
	constexpr auto second_g8_offset = sizeof(Rgb888) * 1 + 1;
	constexpr auto third_b8_offset  = sizeof(Rgb888) * 2 + 2;

	EXPECT_EQ(byte_array[first_r8_offset], r8);
	EXPECT_EQ(byte_array[second_g8_offset], g8);
	EXPECT_EQ(byte_array[third_b8_offset], b8);
}

TEST(rgb, bgrx8888_byte_order)
{
	constexpr uint8_t b8 = 0b1000'1111;
	constexpr uint8_t g8 = 0b1000'0111;
	constexpr uint8_t r8 = 0b1000'0011;

	// Create a sequential array of objects
	const Bgrx8888 bgrx_array[3] = {
	        {b8, g8, r8},
	        {b8, g8, r8},
	        {b8, g8, r8},
	};

	// Treat the array as if it were video memory, i.e., a sequence of bytes.
	const auto byte_array = reinterpret_cast<const uint8_t*>(bgrx_array);

	// If Bgrx8888 works properly, the colour values will always be in the
	// same place regardless of the hosts's endianness.
	//
	constexpr auto first_b8_offset  = sizeof(Bgrx8888) * 0 + 0;
	constexpr auto second_g8_offset = sizeof(Bgrx8888) * 1 + 1;
	constexpr auto third_r8_offset  = sizeof(Bgrx8888) * 2 + 2;

	EXPECT_EQ(byte_array[first_b8_offset], b8);
	EXPECT_EQ(byte_array[second_g8_offset], g8);
	EXPECT_EQ(byte_array[third_r8_offset], r8);
}

TEST(rgb, bgrx8888_cast_byte_order)
{
	constexpr uint8_t b8 = 0b1000'1111;
	constexpr uint8_t g8 = 0b1000'0111;
	constexpr uint8_t r8 = 0b1000'0011;

	const Bgrx8888 bgrx_object(b8, g8, r8);

	// Cast the object to a uint32_t and copy it byte-by-byte into an array.
	// Bgrx8888 claims that its uint32_t cast is just another 'view' into
	// the fixed BGRx byte-ordered colour values.
	//
	const uint32_t bgrx_as_uint32 = bgrx_object;
	constexpr auto array_size     = sizeof(bgrx_as_uint32);

	uint8_t byte_array[array_size] = {};
	memcpy(byte_array, &bgrx_as_uint32, array_size);

	// If Bgrx8888 works properly, the colour values will always be in the
	// same place regardless of the hosts's endianness. So the uint32 is
	// merely a different view into the struct's byte-order values.
	//
	EXPECT_EQ(byte_array[0], b8);
	EXPECT_EQ(byte_array[1], g8);
	EXPECT_EQ(byte_array[2], r8);
}
