/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include "sci/sci.h"
#include "sci/engine/features.h"
#include "sci/graphics/palette32.h"
#include "sci/graphics/remap32.h"

namespace Sci {

#pragma mark SingleRemap

void SingleRemap::reset() {
	_lastPercent = 100;
	_lastGray = 0;

	const uint8 remapStartColor = g_sci->_gfxRemap32->getStartColor();
	const Palette &currentPalette = g_sci->_gfxPalette32->getCurrentPalette();
	for (uint i = 0; i < remapStartColor; ++i) {
		const Color &color = currentPalette.colors[i];
		_remapColors[i] = i;
		_originalColors[i] = color;
		_originalColorsChanged[i] = true;
		_idealColors[i] = color;
		_idealColorsChanged[i] = false;
		_matchDistances[i] = 0;
	}
}

bool SingleRemap::update() {
	switch (_type) {
	case kRemapNone:
		break;
	case kRemapByRange:
		return updateRange();
	case kRemapByPercent:
		return updateBrightness();
	case kRemapToGray:
		return updateSaturation();
	case kRemapToPercentGray:
		return updateSaturationAndBrightness();
	default:
		error("Illegal remap type %d", _type);
	}

	return false;
}

bool SingleRemap::updateRange() {
	const uint8 remapStartColor = g_sci->_gfxRemap32->getStartColor();
	bool updated = false;

	for (uint i = 0; i < remapStartColor; ++i) {
		uint8 targetColor;
		if (_from <= i && i <= _to) {
			targetColor = i + _delta;
		} else {
			targetColor = i;
		}

		if (_remapColors[i] != targetColor) {
			updated = true;
			_remapColors[i] = targetColor;
		}

		_originalColorsChanged[i] = true;
	}

	return updated;
}

bool SingleRemap::updateBrightness() {
	const uint8 remapStartColor = g_sci->_gfxRemap32->getStartColor();
	const Palette &nextPalette = g_sci->_gfxPalette32->getNextPalette();
	for (uint i = 1; i < remapStartColor; ++i) {
		Color color(nextPalette.colors[i]);

		if (_originalColors[i] != color) {
			_originalColorsChanged[i] = true;
			_originalColors[i] = color;
		}

		if (_percent != _lastPercent || _originalColorsChanged[i]) {
			// SSCI checked if percent was over 100 and only then clipped
			// values, but we always unconditionally ensure the result is in the
			// correct range for simplicity's sake
			color.r = MIN(255, (uint16)color.r * _percent / 100);
			color.g = MIN(255, (uint16)color.g * _percent / 100);
			color.b = MIN(255, (uint16)color.b * _percent / 100);

			if (_idealColors[i] != color) {
				_idealColorsChanged[i] = true;
				_idealColors[i] = color;
			}
		}
	}

	const bool updated = apply();
	Common::fill(_originalColorsChanged, _originalColorsChanged + remapStartColor, false);
	Common::fill(_idealColorsChanged, _idealColorsChanged + remapStartColor, false);
	_lastPercent = _percent;
	return updated;
}

bool SingleRemap::updateSaturation() {
	const uint8 remapStartColor = g_sci->_gfxRemap32->getStartColor();
	const Palette &currentPalette = g_sci->_gfxPalette32->getCurrentPalette();
	for (uint i = 1; i < remapStartColor; ++i) {
		Color color(currentPalette.colors[i]);
		if (_originalColors[i] != color) {
			_originalColorsChanged[i] = true;
			_originalColors[i] = color;
		}

		if (_gray != _lastGray || _originalColorsChanged[i]) {
			const int luminosity = (((color.r * 77) + (color.g * 151) + (color.b * 28)) >> 8) * _percent / 100;

			color.r = MIN(255, color.r - ((color.r - luminosity) * _gray / 100));
			color.g = MIN(255, color.g - ((color.g - luminosity) * _gray / 100));
			color.b = MIN(255, color.b - ((color.b - luminosity) * _gray / 100));

			if (_idealColors[i] != color) {
				_idealColorsChanged[i] = true;
				_idealColors[i] = color;
			}
		}
	}

	const bool updated = apply();
	Common::fill(_originalColorsChanged, _originalColorsChanged + remapStartColor, false);
	Common::fill(_idealColorsChanged, _idealColorsChanged + remapStartColor, false);
	_lastGray = _gray;
	return updated;
}

bool SingleRemap::updateSaturationAndBrightness() {
	const uint8 remapStartColor = g_sci->_gfxRemap32->getStartColor();
	const Palette &currentPalette = g_sci->_gfxPalette32->getCurrentPalette();
	for (uint i = 1; i < remapStartColor; i++) {
		Color color(currentPalette.colors[i]);
		if (_originalColors[i] != color) {
			_originalColorsChanged[i] = true;
			_originalColors[i] = color;
		}

		if (_percent != _lastPercent || _gray != _lastGray || _originalColorsChanged[i]) {
			const int luminosity = (((color.r * 77) + (color.g * 151) + (color.b * 28)) >> 8) * _percent / 100;

			color.r = MIN(255, color.r - ((color.r - luminosity) * _gray) / 100);
			color.g = MIN(255, color.g - ((color.g - luminosity) * _gray) / 100);
			color.b = MIN(255, color.b - ((color.b - luminosity) * _gray) / 100);

			if (_idealColors[i] != color) {
				_idealColorsChanged[i] = true;
				_idealColors[i] = color;
			}
		}
	}

	const bool updated = apply();
	Common::fill(_originalColorsChanged, _originalColorsChanged + remapStartColor, false);
	Common::fill(_idealColorsChanged, _idealColorsChanged + remapStartColor, false);
	_lastPercent = _percent;
	_lastGray = _gray;
	return updated;
}

bool SingleRemap::apply() {
	const GfxRemap32 *const gfxRemap32 = g_sci->_gfxRemap32;
	const uint8 remapStartColor = gfxRemap32->getStartColor();

	// Blocked colors are not allowed to be used as target colors for the remap
	bool blockedColors[236];
	Common::fill(blockedColors, blockedColors + remapStartColor, false);

	const bool *const paletteCycleMap = g_sci->_gfxPalette32->getCycleMap();

	const int16 blockedRangeCount = gfxRemap32->getBlockedRangeCount();
	if (blockedRangeCount) {
		const uint8 blockedRangeStart = gfxRemap32->getBlockedRangeStart();
		Common::fill(blockedColors + blockedRangeStart, blockedColors + blockedRangeStart + blockedRangeCount, true);
	}

	for (uint i = 0; i < remapStartColor; ++i) {
		if (paletteCycleMap[i]) {
			blockedColors[i] = true;
		}
	}

	// SSCI did a loop over colors here to create a new array of updated,
	// unblocked colors, but then never used it

	bool updated = false;
	for (uint i = 1; i < remapStartColor; ++i) {
		int distance;

		if (!_idealColorsChanged[i] && !_originalColorsChanged[_remapColors[i]]) {
			continue;
		}

		if (
			_idealColorsChanged[i] &&
			_originalColorsChanged[_remapColors[i]] &&
			_matchDistances[i] < 100 &&
			colorDistance(_idealColors[i], _originalColors[_remapColors[i]]) <= _matchDistances[i]
		) {
			continue;
		}

		const int16 bestColor = matchColor(_idealColors[i], _matchDistances[i], distance, blockedColors);

		if (bestColor != -1 && _remapColors[i] != bestColor) {
			updated = true;
			_remapColors[i] = bestColor;
			_matchDistances[i] = distance;
		}
	}

	return updated;
}

int SingleRemap::colorDistance(const Color &a, const Color &b) const {
	int channelDistance = a.r - b.r;
	int distance = channelDistance * channelDistance;
	channelDistance = a.g - b.g;
	distance += channelDistance * channelDistance;
	channelDistance = a.b - b.b;
	distance += channelDistance * channelDistance;
	return distance;
}

int16 SingleRemap::matchColor(const Color &color, const int minimumDistance, int &outDistance, const bool *const blockedIndexes) const {
	int16 bestIndex = -1;
	int bestDistance = 0xFFFFF;
	int distance = minimumDistance;
	const Palette &nextPalette = g_sci->_gfxPalette32->getNextPalette();

	for (uint i = 0, channelDistance; i < g_sci->_gfxRemap32->getStartColor(); ++i) {
		if (blockedIndexes[i]) {
			continue;
		}

		distance = nextPalette.colors[i].r - color.r;
		distance *= distance;
		if (bestDistance <= distance) {
			continue;
		}
		channelDistance = nextPalette.colors[i].g - color.g;
		distance += channelDistance * channelDistance;
		if (bestDistance <= distance) {
			continue;
		}
		channelDistance = nextPalette.colors[i].b - color.b;
		distance += channelDistance * channelDistance;
		if (bestDistance <= distance) {
			continue;
		}
		bestDistance = distance;
		bestIndex = i;
	}

	// This value is only valid if the last index to perform a distance
	// calculation was the best index
	outDistance = distance;
	return bestIndex;
}

#pragma mark -
#pragma mark GfxRemap32

GfxRemap32::GfxRemap32() :
	_needsUpdate(false),
	_blockedRangeStart(0),
	_blockedRangeCount(0),
	_remapStartColor(236),
	_numActiveRemaps(0) {
	// The `_remapStartColor` seems to always be 236 in SSCI, but if it is ever
	// changed then the various C-style member arrays hard-coded to 236 need to
	// be changed to match the highest possible value of `_remapStartColor`
	assert(_remapStartColor == 236);

	if (g_sci->_features->hasMidPaletteCode()) {
		_remaps.resize(9);
	} else {
		_remaps.resize(19);
	}

	_remapEndColor = _remapStartColor + _remaps.size() - 1;
}

void GfxRemap32::remapOff(const uint8 color) {
	if (color == 0) {
		remapAllOff();
		return;
	}

	// SSCI simply ignored invalid input values, but we at least give a warning
	// so games can be investigated for script bugs
	if (color < _remapStartColor || color > _remapEndColor) {
		warning("GfxRemap32::remapOff: %d out of remap range", color);
		return;
	}

	const uint8 index = _remapEndColor - color;
	SingleRemap &singleRemap = _remaps[index];
	singleRemap._type = kRemapNone;
	--_numActiveRemaps;
	_needsUpdate = true;
}

void GfxRemap32::remapAllOff() {
	for (uint i = 0, len = _remaps.size(); i < len; ++i) {
		_remaps[i]._type = kRemapNone;
	}

	_numActiveRemaps = 0;
	_needsUpdate = true;
}

void GfxRemap32::remapByRange(const uint8 color, const int16 from, const int16 to, const int16 delta) {
	// SSCI simply ignored invalid input values, but we at least give a warning
	// so games can be investigated for script bugs
	if (color < _remapStartColor || color > _remapEndColor) {
		warning("GfxRemap32::remapByRange: %d out of remap range", color);
		return;
	}

	if (from < 0) {
		warning("GfxRemap32::remapByRange: attempt to remap negative color %d", from);
		return;
	}

	if (to >= _remapStartColor) {
		warning("GfxRemap32::remapByRange: attempt to remap into the remap zone at %d", to);
		return;
	}

	const uint8 index = _remapEndColor - color;
	SingleRemap &singleRemap = _remaps[index];

	if (singleRemap._type == kRemapNone) {
		++_numActiveRemaps;
		singleRemap.reset();
	}

	singleRemap._from = from;
	singleRemap._to = to;
	singleRemap._delta = delta;
	singleRemap._type = kRemapByRange;
	_needsUpdate = true;
}

void GfxRemap32::remapByPercent(const uint8 color, const int16 percent) {
	// SSCI simply ignored invalid input values, but we at least give a warning
	// so games can be investigated for script bugs
	if (color < _remapStartColor || color > _remapEndColor) {
		warning("GfxRemap32::remapByPercent: %d out of remap range", color);
		return;
	}

	const uint8 index = _remapEndColor - color;
	SingleRemap &singleRemap = _remaps[index];

	if (singleRemap._type == kRemapNone) {
		++_numActiveRemaps;
		singleRemap.reset();
	}

	singleRemap._percent = percent;
	singleRemap._type = kRemapByPercent;
	_needsUpdate = true;
}

void GfxRemap32::remapToGray(const uint8 color, const int8 gray) {
	// SSCI simply ignored invalid input values, but we at least give a warning
	// so games can be investigated for script bugs
	if (color < _remapStartColor || color > _remapEndColor) {
		warning("GfxRemap32::remapToGray: %d out of remap range", color);
		return;
	}

	if (gray < 0 || gray > 100) {
		error("RemapToGray percent out of range; gray = %d", gray);
	}

	const uint8 index = _remapEndColor - color;
	SingleRemap &singleRemap = _remaps[index];

	if (singleRemap._type == kRemapNone) {
		++_numActiveRemaps;
		singleRemap.reset();
	}

	singleRemap._gray = gray;
	singleRemap._type = kRemapToGray;
	_needsUpdate = true;
}

void GfxRemap32::remapToPercentGray(const uint8 color, const int16 gray, const int16 percent) {
	// SSCI simply ignored invalid input values, but we at least give a warning
	// so games can be investigated for script bugs
	if (color < _remapStartColor || color > _remapEndColor) {
		warning("GfxRemap32::remapToPercentGray: %d out of remap range", color);
		return;
	}

	const uint8 index = _remapEndColor - color;
	SingleRemap &singleRemap = _remaps[index];

	if (singleRemap._type == kRemapNone) {
		++_numActiveRemaps;
		singleRemap.reset();
	}

	singleRemap._percent = percent;
	singleRemap._gray = gray;
	singleRemap._type = kRemapToPercentGray;
	_needsUpdate = true;
}

void GfxRemap32::blockRange(const uint8 from, const int16 count) {
	_blockedRangeStart = from;
	_blockedRangeCount = count;
}

bool GfxRemap32::remapAllTables(const bool paletteUpdated) {
	if (!_needsUpdate && !paletteUpdated) {
		return false;
	}

	bool updated = false;

	for (SingleRemapsList::iterator it = _remaps.begin(); it != _remaps.end(); ++it) {
		if (it->_type != kRemapNone) {
			updated |= it->update();
		}
	}

	_needsUpdate = false;
	return updated;
}
} // End of namespace Sci