#include "utils/sdl_bilinear_scale.hpp"

#include <cstdint>
#include <memory>

// Performs bilinear scaling using fixed-width integer math.

namespace devilution {

namespace {

int Frac(int fixedPoint)
{
	return fixedPoint & 0xffff;
}

int ToInt(int fixedPoint)
{
	return fixedPoint >> 16;
}

std::unique_ptr<int[]> CreateMixFactors(unsigned srcSize, unsigned dstSize)
{
	std::unique_ptr<int[]> result { new int[dstSize + 1] };
	const auto scale = static_cast<int>(65536.0 * static_cast<float>(srcSize - 1) / dstSize);
	int mix = 0;
	for (unsigned i = 0; i <= dstSize; ++i) {
		result[i] = mix;
		mix = Frac(mix) + scale;
	}
	return result;
};

uint8_t MixColors(uint8_t first, uint8_t second, int ratio)
{
	return ToInt((second - first) * ratio) + first;
}

uint8_t MixColorsWithAlpha(uint8_t first, uint8_t firstAlpha,
    uint8_t second, uint8_t secondAlpha,
    uint8_t mixedAlpha, int ratio)
{
	if (mixedAlpha == 0)
		return 0;
	if (mixedAlpha == 255)
		return MixColors(first, second, ratio);

	const int firstWithAlpha = first * firstAlpha;
	const int secondWithAlpha = second * secondAlpha;

	// We want to calculate:
	//
	//    (ToInt((secondWithAlpha - firstWithAlpha) * ratio) + firstWithAlpha) / mixedAlpha
	//
	// However, the above written as-is can overflow in the argument to `ToInt`.
	// To avoid the overflow we divide each term by `mixedAlpha` separately.
	//
	// This would be lower precision and could result in a negative overall result,
	// so we do the rounding-up integer division for each term (instead of a truncating one):
	//
	//    (a + (b - 1)) / b
	return ToInt((secondWithAlpha - firstWithAlpha) * ((ratio + (mixedAlpha - 1)) / mixedAlpha)) + (firstWithAlpha + (mixedAlpha - 1)) / mixedAlpha;
}

} // namespace

void BilinearScale32(SDL_Surface *src, SDL_Surface *dst)
{
	const std::unique_ptr<int[]> mixXs = CreateMixFactors(src->w, dst->w);
	const std::unique_ptr<int[]> mixYs = CreateMixFactors(src->h, dst->h);

	const unsigned dgap = dst->pitch - dst->w * 4;

	auto *srcPixels = static_cast<uint8_t *>(src->pixels);
	auto *dstPixels = static_cast<uint8_t *>(dst->pixels);

	int *curMixY = &mixYs[0];
	unsigned srcY = 0;
	for (unsigned y = 0; y < static_cast<unsigned>(dst->h); ++y) {
		uint8_t *s[4] = {
			srcPixels,                 // Self
			srcPixels + 4,             // Right
			srcPixels + src->pitch,    // Bottom
			srcPixels + src->pitch + 4 // Bottom right
		};

		int *curMixX = &mixXs[0];
		unsigned srcX = 0;
		for (unsigned x = 0; x < static_cast<unsigned>(dst->w); ++x) {
			const int mixX = Frac(*curMixX);
			const int mixY = Frac(*curMixY);

			const uint8_t alpha0 = MixColors(s[0][3], s[1][3], mixX);
			const uint8_t alpha1 = MixColors(s[2][3], s[3][3], mixX);
			const uint8_t finalAlpha = MixColors(alpha0, alpha1, mixY);

			if (finalAlpha == 0) {
				dstPixels[0] = 0;
				dstPixels[1] = 0;
				dstPixels[2] = 0;
				dstPixels[3] = 0;
			} else if (finalAlpha == 255) {
				for (unsigned channel = 0; channel < 3; ++channel) {
					dstPixels[channel] = MixColors(
					    MixColors(s[0][channel], s[1][channel], mixX),
					    MixColors(s[2][channel], s[3][channel], mixX),
					    mixY);
				}
				dstPixels[3] = 255;
			} else {
				for (unsigned channel = 0; channel < 3; ++channel) {
					dstPixels[channel] = MixColorsWithAlpha(
					    MixColorsWithAlpha(s[0][channel], s[0][3], s[1][channel], s[1][3], alpha0, mixX),
					    alpha0,
					    MixColorsWithAlpha(s[2][channel], s[2][3], s[3][channel], s[3][3], alpha1, mixX),
					    alpha1,
					    finalAlpha,
					    mixY);
				}
				dstPixels[3] = finalAlpha;
			}

			++curMixX;
			if (*curMixX > 0) {
				unsigned step = ToInt(*curMixX);
				srcX += step;
				if (srcX <= static_cast<unsigned>(src->w)) {
					step *= 4;
					for (auto &v : s) {
						v += step;
					}
				}
			}

			dstPixels += 4;
		}

		++curMixY;
		if (*curMixY > 0) {
			const unsigned step = ToInt(*curMixY);
			srcY += step;
			if (srcY < static_cast<unsigned>(src->h)) {
				srcPixels += step * src->pitch;
			}
		}

		dstPixels += dgap;
	}
}

void BilinearDownscaleByHalf8(const SDL_Surface *src, const std::array<std::array<Uint8, 256>, 256> &paletteBlendingTable, SDL_Surface *dst, uint8_t transparentIndex)
{
	const auto *const srcPixelsBegin = static_cast<const uint8_t *>(src->pixels)
	    + static_cast<size_t>(src->clip_rect.y * src->pitch + src->clip_rect.x);
	auto *const dstPixelsBegin = static_cast<uint8_t *>(dst->pixels)
	    + static_cast<size_t>(dst->clip_rect.y * dst->pitch + dst->clip_rect.x);
	for (unsigned y = 0, h = static_cast<unsigned>(dst->clip_rect.h); y < h; ++y) {
		const uint8_t *srcPixels = srcPixelsBegin + static_cast<size_t>(2 * y * src->pitch);
		uint8_t *dstPixels = dstPixelsBegin + static_cast<size_t>(y * dst->pitch);
		for (unsigned x = 0, w = static_cast<unsigned>(dst->clip_rect.w); x < w; ++x) {
			uint8_t quad[] = {
				srcPixels[0],
				srcPixels[1],
				srcPixels[src->pitch],
				srcPixels[src->pitch + 1]
			};
			// Attempt to avoid blending with transparent pixels
			if (quad[0] == transparentIndex)
				quad[0] = quad[1];
			if (quad[1] == transparentIndex)
				quad[1] = quad[0];
			if (quad[2] == transparentIndex)
				quad[2] = quad[3];
			if (quad[3] == transparentIndex)
				quad[3] = quad[2];
			uint8_t top = paletteBlendingTable[quad[0]][quad[1]];
			uint8_t bottom = paletteBlendingTable[quad[2]][quad[3]];
			if (top == transparentIndex)
				top = bottom;
			if (bottom == transparentIndex)
				bottom = top;
			*dstPixels++ = paletteBlendingTable[top][bottom];
			srcPixels += 2;
		}
	}
}

} // namespace devilution
