#include "VideoIOFormat.h"

#include <algorithm>
#include <cmath>

#ifdef min
#undef min
#endif
#ifdef max
#undef max
#endif

namespace
{
uint16_t Clamp10(int value, int minimum, int maximum)
{
	return static_cast<uint16_t>(std::max(minimum, std::min(maximum, value)));
}

uint32_t MakeV210Word(uint16_t a, uint16_t b, uint16_t c)
{
	return (static_cast<uint32_t>(a) & 0x3ffu)
		| ((static_cast<uint32_t>(b) & 0x3ffu) << 10)
		| ((static_cast<uint32_t>(c) & 0x3ffu) << 20);
}

void StoreWord(std::array<uint8_t, 16>& bytes, std::size_t wordIndex, uint32_t word)
{
	const std::size_t offset = wordIndex * 4;
	bytes[offset + 0] = static_cast<uint8_t>(word & 0xffu);
	bytes[offset + 1] = static_cast<uint8_t>((word >> 8) & 0xffu);
	bytes[offset + 2] = static_cast<uint8_t>((word >> 16) & 0xffu);
	bytes[offset + 3] = static_cast<uint8_t>((word >> 24) & 0xffu);
}

uint32_t LoadWord(const std::array<uint8_t, 16>& bytes, std::size_t wordIndex)
{
	const std::size_t offset = wordIndex * 4;
	return static_cast<uint32_t>(bytes[offset + 0])
		| (static_cast<uint32_t>(bytes[offset + 1]) << 8)
		| (static_cast<uint32_t>(bytes[offset + 2]) << 16)
		| (static_cast<uint32_t>(bytes[offset + 3]) << 24);
}

uint16_t Component(uint32_t word, unsigned index)
{
	return static_cast<uint16_t>((word >> (index * 10)) & 0x3ffu);
}
}

const char* VideoIOPixelFormatName(VideoIOPixelFormat format)
{
	switch (format)
	{
	case VideoIOPixelFormat::V210:
		return "10-bit YUV v210";
	case VideoIOPixelFormat::Yuva10:
		return "10-bit YUVA Ay10";
	case VideoIOPixelFormat::Bgra8:
		return "8-bit BGRA";
	case VideoIOPixelFormat::Uyvy8:
	default:
		return "8-bit YUV UYVY";
	}
}

bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format)
{
	return format == VideoIOPixelFormat::V210 || format == VideoIOPixelFormat::Yuva10;
}

VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported)
{
	return tenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Uyvy8;
}

unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format)
{
	switch (format)
	{
	case VideoIOPixelFormat::Uyvy8:
		return 2u;
	case VideoIOPixelFormat::Bgra8:
		return 4u;
	case VideoIOPixelFormat::Yuva10:
		return 4u;
	case VideoIOPixelFormat::V210:
	default:
		return 0u;
	}
}

unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth)
{
	if (format == VideoIOPixelFormat::V210)
		return MinimumV210RowBytes(frameWidth);
	if (format == VideoIOPixelFormat::Yuva10)
		return MinimumYuva10RowBytes(frameWidth);
	return frameWidth * VideoIOBytesPerPixel(format);
}

unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes)
{
	return (rowBytes + 3u) / 4u;
}

unsigned MinimumV210RowBytes(unsigned frameWidth)
{
	return ((frameWidth + 5u) / 6u) * 16u;
}

unsigned MinimumYuva10RowBytes(unsigned frameWidth)
{
	return ((frameWidth + 63u) / 64u) * 256u;
}

unsigned ActiveV210WordsForWidth(unsigned frameWidth)
{
	return ((frameWidth + 5u) / 6u) * 4u;
}

V210CodeValues Rec709RgbToLegalV210(float red, float green, float blue)
{
	red = std::max(0.0f, std::min(1.0f, red));
	green = std::max(0.0f, std::min(1.0f, green));
	blue = std::max(0.0f, std::min(1.0f, blue));

	const float y = 0.2126f * red + 0.7152f * green + 0.0722f * blue;
	const float cb = (blue - y) / 1.8556f + 0.5f;
	const float cr = (red - y) / 1.5748f + 0.5f;

	V210CodeValues values;
	values.y = Clamp10(static_cast<int>(std::lround(64.0f + y * 876.0f)), 64, 940);
	values.cb = Clamp10(static_cast<int>(std::lround(64.0f + cb * 896.0f)), 64, 960);
	values.cr = Clamp10(static_cast<int>(std::lround(64.0f + cr * 896.0f)), 64, 960);
	return values;
}

std::array<uint8_t, 16> PackV210Block(const V210SixPixelBlock& block)
{
	std::array<uint8_t, 16> bytes = {};
	StoreWord(bytes, 0, MakeV210Word(block.cb[0], block.y[0], block.cr[0]));
	StoreWord(bytes, 1, MakeV210Word(block.y[1], block.cb[1], block.y[2]));
	StoreWord(bytes, 2, MakeV210Word(block.cr[1], block.y[3], block.cb[2]));
	StoreWord(bytes, 3, MakeV210Word(block.y[4], block.cr[2], block.y[5]));
	return bytes;
}

V210SixPixelBlock UnpackV210Block(const std::array<uint8_t, 16>& bytes)
{
	const uint32_t word0 = LoadWord(bytes, 0);
	const uint32_t word1 = LoadWord(bytes, 1);
	const uint32_t word2 = LoadWord(bytes, 2);
	const uint32_t word3 = LoadWord(bytes, 3);

	V210SixPixelBlock block;
	block.cb[0] = Component(word0, 0);
	block.y[0] = Component(word0, 1);
	block.cr[0] = Component(word0, 2);
	block.y[1] = Component(word1, 0);
	block.cb[1] = Component(word1, 1);
	block.y[2] = Component(word1, 2);
	block.cr[1] = Component(word2, 0);
	block.y[3] = Component(word2, 1);
	block.cb[2] = Component(word2, 2);
	block.y[4] = Component(word3, 0);
	block.cr[2] = Component(word3, 1);
	block.y[5] = Component(word3, 2);
	return block;
}
