#include "utils/surface_to_clx.hpp"

#include <cstdint>
#include <cstring>
#include <vector>

#include "utils/clx_encode.hpp"
#include "utils/endian.hpp"

#ifdef DEBUG_SURFACE_TO_CLX_SIZE
#include <iomanip>
#include <iostream>
#endif

namespace devilution {

OwnedClxSpriteList SurfaceToClx(const Surface &surface, unsigned numFrames,
    std::optional<uint8_t> transparentColor)
{
	// CLX header: frame count, frame offset for each frame, file size
	std::vector<uint8_t> clxData(4 * (2 + static_cast<size_t>(numFrames)));
	WriteLE32(clxData.data(), numFrames);

	const auto height = static_cast<unsigned>(surface.h());
	const auto width = static_cast<unsigned>(surface.w());
	const auto pitch = static_cast<unsigned>(surface.pitch());
	const unsigned frameHeight = height / numFrames;

	// We process the surface a whole frame at a time because the lines are reversed in CEL.
	const uint8_t *dataPtr = surface.begin();
	for (unsigned frame = 1; frame <= numFrames; ++frame) {
		WriteLE32(&clxData[4 * static_cast<size_t>(frame)], static_cast<uint32_t>(clxData.size()));

		// Frame header: 5 16-bit values:
		// 1. Offset to start of the pixel data.
		// 2. Width
		// 3. Height
		// 4..5. Unused (0)
		const size_t frameHeaderPos = clxData.size();
		constexpr size_t FrameHeaderSize = 10;
		clxData.resize(clxData.size() + FrameHeaderSize);

		// Frame header:
		WriteLE16(&clxData[frameHeaderPos], FrameHeaderSize);
		WriteLE16(&clxData[frameHeaderPos + 2], static_cast<uint16_t>(width));
		WriteLE16(&clxData[frameHeaderPos + 4], static_cast<uint16_t>(frameHeight));
		memset(&clxData[frameHeaderPos + 6], 0, 4);

		unsigned transparentRunWidth = 0;
		size_t line = 0;
		while (line != frameHeight) {
			// Process line:
			const uint8_t *src = &dataPtr[(frameHeight - (line + 1)) * pitch];
			if (transparentColor) {
				unsigned solidRunWidth = 0;
				for (const uint8_t *srcEnd = src + width; src != srcEnd; ++src) {
					if (*src == *transparentColor) {
						if (solidRunWidth != 0) {
							AppendClxPixelsOrFillRun(src - transparentRunWidth - solidRunWidth, solidRunWidth, clxData);
							solidRunWidth = 0;
						}
						++transparentRunWidth;
					} else {
						AppendClxTransparentRun(transparentRunWidth, clxData);
						transparentRunWidth = 0;
						++solidRunWidth;
					}
				}
				if (solidRunWidth != 0) {
					AppendClxPixelsOrFillRun(src - solidRunWidth, solidRunWidth, clxData);
				}
			} else {
				AppendClxPixelsOrFillRun(src, width, clxData);
			}
			++line;
		}
		AppendClxTransparentRun(transparentRunWidth, clxData);

		dataPtr += static_cast<unsigned>(pitch * frameHeight);
	}

	WriteLE32(&clxData[4 * (1 + static_cast<size_t>(numFrames))], static_cast<uint32_t>(clxData.size()));

	auto out = std::unique_ptr<uint8_t[]>(new uint8_t[clxData.size()]);
	memcpy(&out[0], clxData.data(), clxData.size());

#ifdef DEBUG_SURFACE_TO_CLX_SIZE
	const int surfaceSize = surface.h() * surface.pitch();
	std::cout << "Surface(" << surface.w() << ", " << surface.h() << ") -> CLX\t"
	          << surfaceSize << " -> " << clxData.size() << "\t"
	          << std::setprecision(1) << std::fixed << (static_cast<int>(clxData.size()) - surfaceSize) / ((float)surfaceSize) * 100 << "%" << std::endl;
#endif

	return OwnedClxSpriteList { std::move(out) };
}

} // namespace devilution
