// F22 Texture Editor by Krishty
//
//  1. run from extracted EF2000 or F22 “PROGRAM” directory
//  2. converts all TM files of the game to PNGs in the subdirectory “!converted”
//  3. after editing the PNGs, run again to convert them back to TMs
//
// The last modification time is used to determine whether PNG or TM is newer, i.e. you can run this program
//  again and again to “synchronize” your work with the game.
//
// Errors are spit out to stderr. Invalid files are skipped.
//
// Currently limited to Windows because that has a built-in PNG encoder/decoder (GDI+).
//
// TODO: Command line options, return code, help, etc.



#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>



template <
	typename T
> [[nodiscard]] static T readBinaryFile(
	std::filesystem::path const & path
) {
	T result;
	std::ifstream file{ path, std::ios::binary };
	if(not file.is_open())
		throw std::runtime_error{ path.string() + ": Cannot open file" };
	file.read(reinterpret_cast<char *>(&result), sizeof result);
	if(not file.good())
		throw std::runtime_error{ path.string() + ": Cannot read data" };
	return result;
}

template <
	typename T
> static void writeBinaryFile(
	std::filesystem::path const & path,
	T const &                     data
) {
	std::ofstream file{ path, std::ios::binary };
	if(not file.is_open())
		throw std::runtime_error{ path.string() + ": Cannot open file" };
	file.write(reinterpret_cast<char const *>(&data), sizeof data);
	if(not file.good())
		throw std::runtime_error{ path.string() + ": Cannot write data" };
}


struct RGB {
	uint8_t r, g, b;
};


// A color palette.
//  • colors might be in 6-bit color depth due to Amiga legacy
struct Palette {
	RGB colors[256];
};


// A standard texture – 256×192 pixels, 256 colors.
struct Texture {
	uint8_t texels[256 * 192];
};
static constexpr auto tmWidth{ 256 };
static constexpr auto tmHeight{ 192 };


#define NOMINMAX
#include <Windows.h>
#include <gdiplus.h>
#include <gdiplusinit.h>
#pragma comment(lib, "gdiplus.lib")

static constexpr UUID CLSID_gdiPlusPngEncoder = { 0x557cf406, 0x1a04, 0x11d3, { 0x9a, 0x73, 0x00, 0x00, 0xf8, 0x1e, 0xf3, 0x2e } };

static void pngFrom(
	Texture const &               texture,
	Palette const &               palette,
	std::filesystem::path const & pngPath
) {
	using namespace Gdiplus;

	Bitmap bitmap{ tmWidth, tmHeight, PixelFormat8bppIndexed };

	// Assign the color palette:
	struct {
		ColorPalette header;
		ARGB         colors[255];
	} gdipPalette;
	gdipPalette.header.Flags = 0;
	gdipPalette.header.Count = 256;
	gdipPalette.header.Entries[0] = Color::MakeARGB(255, 255, 63, 255); // actually transparent black but few programs can handle that
	for(int i = 1; i < 256; ++i)
		gdipPalette.header.Entries[i] = Color::MakeARGB(255, palette.colors[i].r, palette.colors[i].g, palette.colors[i].b);
	if(Ok != bitmap.SetPalette(&gdipPalette.header))
		throw std::runtime_error{ "Cannot create GDI+ palette" };

	// Assign the indexed pixels:
	BitmapData bitmapData;
	Rect rect{ 0, 0, tmWidth, tmHeight };
	if(Ok != bitmap.LockBits(&rect, ImageLockModeWrite, PixelFormat8bppIndexed, &bitmapData))
		throw std::runtime_error{ "Cannot lock GDI+ bitmap" };
	for (int y = 0; y < tmHeight; ++y)
		std::copy(
			&texture.texels[y * tmWidth], &texture.texels[y * tmWidth + tmWidth],
			static_cast<uint8_t *>(bitmapData.Scan0) + y * bitmapData.Stride
		);
	if(Ok != bitmap.UnlockBits(&bitmapData))
		throw std::runtime_error{ "Cannot unlock GDI+ bitmap" };

	// Save as PNG:
	if(Ok != bitmap.Save(pngPath.wstring().c_str(), &CLSID_gdiPlusPngEncoder, nullptr))
		throw std::runtime_error{ pngPath.string() + ": Cannot save" };
}

static void didTMFrom(std::filesystem::path const & pngPath, std::filesystem::path const & path) {
	using namespace Gdiplus;

	// Load the PNG:
	Bitmap bitmap{ pngPath.wstring().c_str() };
	if(bitmap.GetWidth() != tmWidth || bitmap.GetHeight() != tmHeight)
		throw std::runtime_error{ pngPath.string() + ": Not 256x192" };
	if(bitmap.GetPixelFormat() != PixelFormat8bppIndexed)
		throw std::runtime_error{ pngPath.string() + ": Not 8-bit indexed" };

	// Extract the indexed pixels:
	Texture tm;
	BitmapData bitmapData;
	Rect rect{ 0, 0, tmWidth, tmHeight };
	if(Ok != bitmap.LockBits(&rect, ImageLockModeWrite, PixelFormat8bppIndexed, &bitmapData))
		throw std::runtime_error{ "Cannot lock GDI+ bitmap" };
	for (int y = 0; y < tmHeight; ++y)
		std::copy(
			static_cast<uint8_t *>(bitmapData.Scan0) + y * bitmapData.Stride,
			static_cast<uint8_t *>(bitmapData.Scan0) + y * bitmapData.Stride + tmWidth,
			&tm.texels[y * tmWidth]
		);
	if(Ok != bitmap.UnlockBits(&bitmapData))
		throw std::runtime_error{ "Cannot unlock GDI+ bitmap" };

	// Save as TM:
	writeBinaryFile(path, tm);
}

static std::filesystem::file_time_type lastModifiedTimeOf(std::filesystem::path const & path) {
	if(exists(path))
		return std::filesystem::directory_entry{ path }.last_write_time();
	return std::filesystem::file_time_type::min();
}

int main() {
	auto const inBasePath{ std::filesystem::current_path() };
	auto const outBasePath{ inBasePath / "!converted" };

	Gdiplus::GdiplusStartupInput gdiplusStartupInput;
	ULONG_PTR gdiplusToken;
	Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr);

	// Convert all texture sets:

	struct TextureSet {
		char const * palName;
		char const * textureDir;
	};

	static constexpr TextureSet ef2000TextureSets[] = {
		{ "pd_cr.col", "tm" },
	};

	static constexpr TextureSet f22TextureSets[] = {
		{ "moonpal.col", "red0200" },
		{ "0600pal.col", "red0600" },
		{ "1000pal.col", "red1000" },
		{ "1400pal.col", "red1400" },
		{ "1800pal.col", "red1800" },
		{ "nomopal.col", "red2200" },
	};

	// Is this EF2000 or F22?
	auto set    { std::begin(ef2000TextureSets) };
	auto setsEnd{ std::end  (ef2000TextureSets) };
	if(is_directory(inBasePath / "red1000")) {
		set     = std::begin(f22TextureSets);
		setsEnd = std::end  (f22TextureSets);
	}
	for(; set < setsEnd; ++set) {
		auto const inTexDir{ inBasePath / set->textureDir };
		auto const outTexDir{ outBasePath / set->textureDir };
		create_directories(outTexDir);
		auto const palette{ readBinaryFile<Palette>(inBasePath / "colours" / set->palName) };

		for(auto const & tmPath : std::filesystem::directory_iterator{ inTexDir }) {
			if(not tmPath.is_regular_file())
				continue;
			try {
				auto const pngPath{ (outTexDir / tmPath.path().filename()).replace_extension(".png") };
				auto const pngWriteTime{ lastModifiedTimeOf(pngPath) };
				auto const tmWriteTime{ lastModifiedTimeOf(tmPath) };
				if(pngWriteTime < tmWriteTime) {
					pngFrom(readBinaryFile<Texture>(tmPath.path()), palette, pngPath);
					last_write_time(pngPath, tmWriteTime);
				} else if(tmWriteTime < pngWriteTime) {
					didTMFrom(pngPath, tmPath);
					last_write_time(tmPath, pngWriteTime);
				}
			} catch(std::exception const & e) {
				std::cerr << e.what() << std::endl;
			}
		}
	}

}
