/*
 * ScalableImage.cpp, part of VCMI engine
 *
 * Authors: listed in file AUTHORS in main folder
 *
 * License: GNU General Public License v2.0 or later
 * Full text of license available in license.txt file, in main folder
 *
 */
#include "StdInc.h"
#include "ScalableImage.h"

#include "SDLImage.h"
#include "SDL_Extensions.h"

#include "../gui/CGuiHandler.h"

#include "../render/ColorFilter.h"
#include "../render/Colors.h"
#include "../render/Graphics.h"
#include "../render/IRenderHandler.h"
#include "../render/IScreenHandler.h"
#include "../render/CanvasImage.h"

#include "../../lib/constants/EntityIdentifiers.h"

#include <SDL_surface.h>

//First 8 colors in def palette used for transparency
static constexpr std::array<SDL_Color, 8> sourcePalette = {{
	{0,   255, 255, SDL_ALPHA_OPAQUE},
	{255, 150, 255, SDL_ALPHA_OPAQUE},
	{255, 100, 255, SDL_ALPHA_OPAQUE},
	{255, 50,  255, SDL_ALPHA_OPAQUE},
	{255, 0,   255, SDL_ALPHA_OPAQUE},
	{255, 255, 0,   SDL_ALPHA_OPAQUE},
	{180, 0,   255, SDL_ALPHA_OPAQUE},
	{0,   255, 0,   SDL_ALPHA_OPAQUE}
}};

static constexpr std::array<ColorRGBA, 8> targetPalette = {{
	{0, 0, 0, 0  }, // 0 - transparency                  ( used in most images )
	{0, 0, 0, 64 }, // 1 - shadow border                 ( used in battle, adventure map def's )
	{0, 0, 0, 64 }, // 2 - shadow border                 ( used in fog-of-war def's )
	{0, 0, 0, 128}, // 3 - shadow body                   ( used in fog-of-war def's )
	{0, 0, 0, 128}, // 4 - shadow body                   ( used in battle, adventure map def's )
	{0, 0, 0, 0  }, // 5 - selection / owner flag        ( used in battle, adventure map def's )
	{0, 0, 0, 128}, // 6 - shadow body   below selection ( used in battle def's )
	{0, 0, 0, 64 }  // 7 - shadow border below selection ( used in battle def's )
}};

static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2)
{
	return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256;
}

static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over)
{
	return ColorRGBA(
		mixChannels(over.r, base.r, over.a, base.a),
		mixChannels(over.g, base.g, over.a, base.a),
		mixChannels(over.b, base.b, over.a, base.a),
		static_cast<ui8>(over.a + base.a * (255 - over.a) / 256)
		);
}
static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & rhs)
{
	// it seems that H3 does not requires exact match to replace colors -> (255, 103, 255) gets interpreted as shadow
	// exact logic is not clear and requires extensive testing with image editing
	// potential reason is that H3 uses 16-bit color format (565 RGB bits), meaning that 3 least significant bits are lost in red and blue component
	static const int threshold = 8;

	int diffR = static_cast<int>(lhs.r) - rhs.r;
	int diffG = static_cast<int>(lhs.g) - rhs.g;
	int diffB = static_cast<int>(lhs.b) - rhs.b;
	int diffA = static_cast<int>(lhs.a) - rhs.a;

	return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold;
}

ScalableImageParameters::ScalableImageParameters(const SDL_Palette * originalPalette, EImageBlitMode blitMode)
{
	if (originalPalette)
	{
		palette = SDL_AllocPalette(originalPalette->ncolors);
		SDL_SetPaletteColors(palette, originalPalette->colors, 0, originalPalette->ncolors);
		preparePalette(originalPalette, blitMode);
	}
}

ScalableImageParameters::~ScalableImageParameters()
{
	SDL_FreePalette(palette);
}

void ScalableImageParameters::preparePalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode)
{
	switch(blitMode)
	{
		case EImageBlitMode::ONLY_SHADOW:
		case EImageBlitMode::ONLY_OVERLAY:
			adjustPalette(originalPalette, blitMode, ColorFilter::genAlphaShifter(0), 0);
			break;
	}

	switch(blitMode)
	{
		case EImageBlitMode::SIMPLE:
		case EImageBlitMode::WITH_SHADOW:
		case EImageBlitMode::ONLY_SHADOW:
		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
			setShadowTransparency(originalPalette, 1.0);
			break;
		case EImageBlitMode::ONLY_BODY:
		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
		case EImageBlitMode::ONLY_OVERLAY:
			setShadowTransparency(originalPalette, 0.0);
			break;
	}

	switch(blitMode)
	{
		case EImageBlitMode::ONLY_OVERLAY:
		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
			setOverlayColor(originalPalette, Colors::WHITE_TRUE);
			break;
		case EImageBlitMode::ONLY_SHADOW:
		case EImageBlitMode::ONLY_BODY:
			setOverlayColor(originalPalette, Colors::TRANSPARENCY);
			break;
	}
}

void ScalableImageParameters::setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color)
{
	palette->colors[5] = CSDL_Ext::toSDL(addColors(targetPalette[5], color));

	for (int i : {6,7})
	{
		if (colorsSimilar(originalPalette->colors[i], sourcePalette[i]))
			palette->colors[i] = CSDL_Ext::toSDL(addColors(targetPalette[i], color));
	}
}

void ScalableImageParameters::shiftPalette(const SDL_Palette * originalPalette, uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
{
	std::vector<SDL_Color> shifterColors(colorsToMove);

	for(uint32_t i=0; i<colorsToMove; ++i)
		shifterColors[(i+distanceToMove)%colorsToMove] = originalPalette->colors[firstColorID + i];

	SDL_SetPaletteColors(palette, shifterColors.data(), firstColorID, colorsToMove);
}

void ScalableImageParameters::setShadowTransparency(const SDL_Palette * originalPalette, float factor)
{
	ColorRGBA shadow50(0, 0, 0, 128 * factor);
	ColorRGBA shadow25(0, 0, 0,  64 * factor);

	std::array<SDL_Color, 5> colorsSDL = {
		originalPalette->colors[0],
		originalPalette->colors[1],
		originalPalette->colors[2],
		originalPalette->colors[3],
		originalPalette->colors[4]
	};

	// seems to be used unconditionally
	colorsSDL[0] = CSDL_Ext::toSDL(Colors::TRANSPARENCY);
	colorsSDL[1] = CSDL_Ext::toSDL(shadow25);
	colorsSDL[4] = CSDL_Ext::toSDL(shadow50);

	// seems to be used only if color matches
	if (colorsSimilar(originalPalette->colors[2], sourcePalette[2]))
		colorsSDL[2] = CSDL_Ext::toSDL(shadow25);

	if (colorsSimilar(originalPalette->colors[3], sourcePalette[3]))
		colorsSDL[3] = CSDL_Ext::toSDL(shadow50);

	SDL_SetPaletteColors(palette, colorsSDL.data(), 0, colorsSDL.size());
}

void ScalableImageParameters::adjustPalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode, const ColorFilter & shifter, uint32_t colorsToSkipMask)
{
	// If shadow is enabled, following colors must be skipped unconditionally
	if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_OVERLAY)
		colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4);

	// Note: here we skip first colors in the palette that are predefined in H3 images
	for(int i = 0; i < palette->ncolors; i++)
	{
		if (i < std::size(sourcePalette) && colorsSimilar(sourcePalette[i], originalPalette->colors[i]))
			continue;

		if(i < std::numeric_limits<uint32_t>::digits && ((colorsToSkipMask >> i) & 1) == 1)
			continue;

		palette->colors[i] = CSDL_Ext::toSDL(shifter.shiftColor(CSDL_Ext::fromSDL(originalPalette->colors[i])));
	}
}

ScalableImageShared::ScalableImageShared(const SharedImageLocator & locator, const std::shared_ptr<const ISharedImage> & baseImage)
	:locator(locator)
{
	scaled[1].body[0] = baseImage;
	assert(scaled[1].body[0] != nullptr);

	loadScaledImages(GH.screenHandler().getScalingFactor(), PlayerColor::CANNOT_DETERMINE);
}

Point ScalableImageShared::dimensions() const
{
	return scaled[1].body[0]->dimensions();
}

void ScalableImageShared::exportBitmap(const boost::filesystem::path & path, const ScalableImageParameters & parameters) const
{
	scaled[1].body[0]->exportBitmap(path, parameters.palette);
}

bool ScalableImageShared::isTransparent(const Point & coords) const
{
	return scaled[1].body[0]->isTransparent(coords);
}

Rect ScalableImageShared::contentRect() const
{
	return scaled[1].body[0]->contentRect();
}

void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Rect * src, const ScalableImageParameters & parameters, int scalingFactor)
{
	const auto & getFlippedImage = [&](FlippedImages & images){
		int index = 0;
		if (parameters.flipVertical)
		{
			if (!images[index|1])
				images[index|1] = images[index]->verticalFlip();

			index |= 1;
		}

		if (parameters.flipHorizontal)
		{
			if (!images[index|2])
				images[index|2] = images[index]->horizontalFlip();

			index |= 2;
		}

		return images[index];
	};

	const auto & flipAndDraw = [&](FlippedImages & images, const ColorRGBA & colorMultiplier, uint8_t alphaValue){

		getFlippedImage(images)->draw(where, parameters.palette, dest, src, colorMultiplier, alphaValue, locator.layer);
	};

	bool shadowLoading = scaled.at(scalingFactor).shadow.at(0) && scaled.at(scalingFactor).shadow.at(0)->isLoading();
	bool bodyLoading = scaled.at(scalingFactor).body.at(0) && scaled.at(scalingFactor).body.at(0)->isLoading();
	bool overlayLoading = scaled.at(scalingFactor).overlay.at(0) && scaled.at(scalingFactor).overlay.at(0)->isLoading();
	bool playerLoading = parameters.player != PlayerColor::CANNOT_DETERMINE && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum()) && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum())->isLoading();

	if (shadowLoading || bodyLoading || overlayLoading || playerLoading)
	{
		getFlippedImage(scaled[1].body)->scaledDraw(where, parameters.palette, dimensions() * scalingFactor, dest, src, parameters.colorMultiplier, parameters.alphaValue, locator.layer);
		return;
	}

	if (scaled.at(scalingFactor).shadow.at(0))
		flipAndDraw(scaled.at(scalingFactor).shadow, Colors::WHITE_TRUE, parameters.alphaValue);

	if (parameters.player != PlayerColor::CANNOT_DETERMINE && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum()))
	{
		scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum())->draw(where, parameters.palette, dest, src, Colors::WHITE_TRUE, parameters.alphaValue, locator.layer);
	}
	else
	{
		if (scaled.at(scalingFactor).body.at(0))
			flipAndDraw(scaled.at(scalingFactor).body, parameters.colorMultiplier, parameters.alphaValue);
	}

	if (scaled.at(scalingFactor).overlay.at(0))
		flipAndDraw(scaled.at(scalingFactor).overlay, parameters.ovelayColorMultiplier, static_cast<int>(parameters.alphaValue) * parameters.ovelayColorMultiplier.a / 255);
}

const SDL_Palette * ScalableImageShared::getPalette() const
{
	return scaled[1].body[0]->getPalette();
}

std::shared_ptr<ScalableImageInstance> ScalableImageShared::createImageReference()
{
	return std::make_shared<ScalableImageInstance>(shared_from_this(), locator.layer);
}

ScalableImageInstance::ScalableImageInstance(const std::shared_ptr<ScalableImageShared> & image, EImageBlitMode blitMode)
	:image(image)
	,parameters(image->getPalette(), blitMode)
	,blitMode(blitMode)
{
	assert(image);
}

void ScalableImageInstance::scaleTo(const Point & size, EScalingAlgorithm algorithm)
{
	scaledImage = nullptr;

	auto newScaledImage = GH.renderHandler().createImage(dimensions(), CanvasScalingPolicy::AUTO);

	newScaledImage->getCanvas().draw(*this, Point(0, 0));
	newScaledImage->scaleTo(size, algorithm);
	scaledImage = newScaledImage;
}

void ScalableImageInstance::exportBitmap(const boost::filesystem::path & path) const
{
	image->exportBitmap(path, parameters);
}

bool ScalableImageInstance::isTransparent(const Point & coords) const
{
	return image->isTransparent(coords);
}

Rect ScalableImageInstance::contentRect() const
{
	return image->contentRect();
}

Point ScalableImageInstance::dimensions() const
{
	if (scaledImage)
		return scaledImage->dimensions() / GH.screenHandler().getScalingFactor();
	return image->dimensions();
}

void ScalableImageInstance::setAlpha(uint8_t value)
{
	parameters.alphaValue = value;
}

void ScalableImageInstance::draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const
{
	if (scaledImage)
		scaledImage->draw(where, pos, src, scalingFactor);
	else
		image->draw(where, pos, src, parameters, scalingFactor);
}

void ScalableImageInstance::setOverlayColor(const ColorRGBA & color)
{
	parameters.ovelayColorMultiplier = color;

	if (parameters.palette)
		parameters.setOverlayColor(image->getPalette(), color);
}

void ScalableImageInstance::playerColored(const PlayerColor & player)
{
	parameters.player = player;

	if (parameters.palette)
		parameters.playerColored(player);

	image->preparePlayerColoredImage(player);
}

void ScalableImageParameters::playerColored(PlayerColor player)
{
	graphics->setPlayerPalette(palette, player);
}

void ScalableImageInstance::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
{
	if (parameters.palette)
		parameters.shiftPalette(image->getPalette(),firstColorID, colorsToMove, distanceToMove);
}

void ScalableImageInstance::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask)
{
	if (parameters.palette)
		parameters.adjustPalette(image->getPalette(), blitMode, shifter, colorsToSkipMask);
}

void ScalableImageInstance::horizontalFlip()
{
	parameters.flipHorizontal = !parameters.flipHorizontal;
}

void ScalableImageInstance::verticalFlip()
{
	parameters.flipVertical = !parameters.flipVertical;
}

std::shared_ptr<const ISharedImage> ScalableImageShared::loadOrGenerateImage(EImageBlitMode mode, int8_t scalingFactor, PlayerColor color, ImageType upscalingSource) const
{
	ImageLocator loadingLocator;

	loadingLocator.image = locator.image;
	loadingLocator.defFile = locator.defFile;
	loadingLocator.defFrame = locator.defFrame;
	loadingLocator.defGroup = locator.defGroup;
	loadingLocator.layer = mode;
	loadingLocator.scalingFactor = scalingFactor;
	loadingLocator.playerColored = color;

	// best case - requested image is already available in filesystem
	auto loadedImage = GH.renderHandler().loadScaledImage(loadingLocator);
	if (loadedImage)
		return loadedImage;

	if (scalingFactor == 1)
	{
		// optional images for 1x resolution - only try load them, don't attempt to generate
		// this block should never be called for 'body' layer - that image is loaded unconditionally before construction
		assert(mode == EImageBlitMode::ONLY_SHADOW || mode == EImageBlitMode::ONLY_OVERLAY || color != PlayerColor::CANNOT_DETERMINE);
		return nullptr;
	}

	// alternatively, find largest pre-scaled image, load it and rescale to desired scaling
	for (int8_t scaling = 4; scaling > 0; --scaling)
	{
		loadingLocator.scalingFactor = scaling;
		auto loadedImage = GH.renderHandler().loadScaledImage(loadingLocator);
		if (loadedImage)
		{
			if (scaling == 1)
			{
				if (mode == EImageBlitMode::ONLY_SHADOW || mode == EImageBlitMode::ONLY_OVERLAY || color != PlayerColor::CANNOT_DETERMINE)
				{
					ScalableImageParameters parameters(getPalette(), mode);
					return loadedImage->scaleInteger(scalingFactor, parameters.palette, mode);
				}
			}
			else
			{
				Point targetSize = scaled[1].body[0]->dimensions() * scalingFactor;
				return loadedImage->scaleTo(targetSize, nullptr);
			}
		}
	}

	ScalableImageParameters parameters(getPalette(), mode);
	// if all else fails - use base (presumably, indexed) image and convert it to desired form
	if (color != PlayerColor::CANNOT_DETERMINE)
		parameters.playerColored(color);

	if (upscalingSource)
		return upscalingSource->scaleInteger(scalingFactor, parameters.palette, mode);
	else
		return scaled[1].body[0]->scaleInteger(scalingFactor, parameters.palette, mode);
}

void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor color)
{
	if (scaled[scalingFactor].body[0] == nullptr && scalingFactor != 1)
	{
		switch(locator.layer)
		{
			case EImageBlitMode::OPAQUE:
			case EImageBlitMode::COLORKEY:
			case EImageBlitMode::SIMPLE:
				scaled[scalingFactor].body[0] = loadOrGenerateImage(locator.layer, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
				break;

			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
			case EImageBlitMode::ONLY_BODY:
				scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
				break;

			case EImageBlitMode::WITH_SHADOW:
			case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
				scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
				break;
		}
	}

	if (color != PlayerColor::CANNOT_DETERMINE && scaled[scalingFactor].playerColored[1+color.getNum()] == nullptr)
	{
		switch(locator.layer)
		{
			case EImageBlitMode::OPAQUE:
			case EImageBlitMode::COLORKEY:
			case EImageBlitMode::SIMPLE:
				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(locator.layer, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
				break;

			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
			case EImageBlitMode::ONLY_BODY:
				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
				break;

			case EImageBlitMode::WITH_SHADOW:
			case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
				break;
		}
	}

	if (scaled[scalingFactor].shadow[0] == nullptr)
	{
		switch(locator.layer)
		{
			case EImageBlitMode::WITH_SHADOW:
			case EImageBlitMode::ONLY_SHADOW:
			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
				scaled[scalingFactor].shadow[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SHADOW, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].shadow[0]);
				break;
			default:
				break;
		}
	}

	if (scaled[scalingFactor].overlay[0] == nullptr)
	{
		switch(locator.layer)
		{
			case EImageBlitMode::ONLY_OVERLAY:
			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
				scaled[scalingFactor].overlay[0] = loadOrGenerateImage(EImageBlitMode::ONLY_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].overlay[0]);
				break;
			default:
				break;
		}
	}
}

void ScalableImageShared::preparePlayerColoredImage(PlayerColor color)
{
	loadScaledImages(GH.screenHandler().getScalingFactor(), color);
}