#include "TextureAssetLoader.h"

#include <windows.h>
#include <wincodec.h>

#include <atlbase.h>
#include <algorithm>
#include <cctype>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>

#ifndef GL_RGBA32F
#define GL_RGBA32F 0x8814
#endif

namespace
{
std::string LowercaseExtension(const std::filesystem::path& path)
{
	std::string extension = path.extension().string();
	std::transform(extension.begin(), extension.end(), extension.begin(),
		[](unsigned char value) { return static_cast<char>(std::tolower(value)); });
	return extension;
}

bool LoadCubeTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
{
	std::ifstream file(textureAsset.path);
	if (!file)
	{
		error = "Could not open shader LUT asset: " + textureAsset.path.string();
		return false;
	}

	unsigned lutSize = 0;
	std::vector<float> values;
	std::string line;
	while (std::getline(file, line))
	{
		const std::size_t commentStart = line.find('#');
		if (commentStart != std::string::npos)
			line.resize(commentStart);

		std::istringstream stream(line);
		std::string firstToken;
		if (!(stream >> firstToken))
			continue;

		if (firstToken == "TITLE" || firstToken == "DOMAIN_MIN" || firstToken == "DOMAIN_MAX")
			continue;
		if (firstToken == "LUT_3D_SIZE")
		{
			stream >> lutSize;
			continue;
		}
		if (firstToken == "LUT_1D_SIZE")
		{
			error = "Only 3D .cube LUT assets are supported: " + textureAsset.path.string();
			return false;
		}

		float red = 0.0f;
		float green = 0.0f;
		float blue = 0.0f;
		try
		{
			red = std::stof(firstToken);
		}
		catch (...)
		{
			error = "Unsupported .cube directive in shader LUT asset: " + firstToken;
			return false;
		}
		if (!(stream >> green >> blue))
		{
			error = "Malformed RGB entry in shader LUT asset: " + textureAsset.path.string();
			return false;
		}
		values.push_back(red);
		values.push_back(green);
		values.push_back(blue);
		values.push_back(1.0f);
	}

	if (lutSize == 0)
	{
		error = "Shader LUT asset is missing LUT_3D_SIZE: " + textureAsset.path.string();
		return false;
	}

	const std::size_t expectedFloats = static_cast<std::size_t>(lutSize) * lutSize * lutSize * 4;
	if (values.size() != expectedFloats)
	{
		error = "Shader LUT asset entry count does not match LUT_3D_SIZE: " + textureAsset.path.string();
		return false;
	}

	const GLsizei atlasWidth = static_cast<GLsizei>(lutSize * lutSize);
	const GLsizei atlasHeight = static_cast<GLsizei>(lutSize);
	glGenTextures(1, &textureId);
	glBindTexture(GL_TEXTURE_2D, textureId);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, atlasWidth, atlasHeight, 0, GL_RGBA, GL_FLOAT, values.data());
	glBindTexture(GL_TEXTURE_2D, 0);
	return true;
}
}

bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
{
	textureId = 0;
	if (LowercaseExtension(textureAsset.path) == ".cube")
		return LoadCubeTextureAsset(textureAsset, textureId, error);

	HRESULT comInitResult = CoInitializeEx(NULL, COINIT_MULTITHREADED);
	const bool shouldUninitializeCom = (comInitResult == S_OK || comInitResult == S_FALSE);
	if (FAILED(comInitResult) && comInitResult != RPC_E_CHANGED_MODE)
	{
		error = "Could not initialize COM to load shader texture assets.";
		return false;
	}

	CComPtr<IWICImagingFactory> imagingFactory;
	HRESULT result = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&imagingFactory));
	if (FAILED(result) || !imagingFactory)
	{
		if (shouldUninitializeCom)
			CoUninitialize();
		error = "Could not create a WIC imaging factory to load shader texture assets.";
		return false;
	}

	CComPtr<IWICBitmapDecoder> bitmapDecoder;
	result = imagingFactory->CreateDecoderFromFilename(textureAsset.path.wstring().c_str(), NULL, GENERIC_READ, WICDecodeMetadataCacheOnLoad, &bitmapDecoder);
	if (FAILED(result) || !bitmapDecoder)
	{
		if (shouldUninitializeCom)
			CoUninitialize();
		error = "Could not open shader texture asset: " + textureAsset.path.string();
		return false;
	}

	CComPtr<IWICBitmapFrameDecode> bitmapFrame;
	result = bitmapDecoder->GetFrame(0, &bitmapFrame);
	if (FAILED(result) || !bitmapFrame)
	{
		if (shouldUninitializeCom)
			CoUninitialize();
		error = "Could not decode the first frame of shader texture asset: " + textureAsset.path.string();
		return false;
	}

	CComPtr<IWICFormatConverter> formatConverter;
	result = imagingFactory->CreateFormatConverter(&formatConverter);
	if (FAILED(result) || !formatConverter)
	{
		if (shouldUninitializeCom)
			CoUninitialize();
		error = "Could not create a WIC format converter for shader texture asset: " + textureAsset.path.string();
		return false;
	}

	result = formatConverter->Initialize(bitmapFrame, GUID_WICPixelFormat32bppBGRA, WICBitmapDitherTypeNone, NULL, 0.0, WICBitmapPaletteTypeCustom);
	if (FAILED(result))
	{
		if (shouldUninitializeCom)
			CoUninitialize();
		error = "Could not convert shader texture asset to BGRA: " + textureAsset.path.string();
		return false;
	}

	UINT width = 0;
	UINT height = 0;
	result = formatConverter->GetSize(&width, &height);
	if (FAILED(result) || width == 0 || height == 0)
	{
		if (shouldUninitializeCom)
			CoUninitialize();
		error = "Shader texture asset has an invalid size: " + textureAsset.path.string();
		return false;
	}

	const UINT stride = width * 4;
	std::vector<unsigned char> pixels(static_cast<std::size_t>(stride) * static_cast<std::size_t>(height));
	result = formatConverter->CopyPixels(NULL, stride, static_cast<UINT>(pixels.size()), pixels.data());
	if (FAILED(result))
	{
		if (shouldUninitializeCom)
			CoUninitialize();
		error = "Could not read shader texture pixels: " + textureAsset.path.string();
		return false;
	}

	std::vector<unsigned char> flippedPixels(pixels.size());
	for (UINT row = 0; row < height; ++row)
	{
		const std::size_t srcOffset = static_cast<std::size_t>(row) * stride;
		const std::size_t dstOffset = static_cast<std::size_t>(height - 1 - row) * stride;
		std::memcpy(flippedPixels.data() + dstOffset, pixels.data() + srcOffset, stride);
	}

	glGenTextures(1, &textureId);
	glBindTexture(GL_TEXTURE_2D, textureId);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast<GLsizei>(width), static_cast<GLsizei>(height), 0, GL_BGRA, GL_UNSIGNED_BYTE, flippedPixels.data());
	glBindTexture(GL_TEXTURE_2D, 0);

	if (shouldUninitializeCom)
		CoUninitialize();

	return true;
}
