/* 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include "sci/console.h"
#include "sci/engine/segment.h"
#include "sci/engine/seg_manager.h"
#include "sci/engine/state.h"
#include "sci/graphics/frameout.h"
#include "sci/graphics/palette32.h"
#include "sci/graphics/text32.h"
#include "sci/graphics/transitions32.h"
#include "sci/sci.h"

namespace Sci {
static int dissolveSequences[2][20] = {
	/* SCI2.1early- */ { 3, 6, 12, 20, 48, 96, 184, 272, 576, 1280, 3232, 6912, 13568, 24576, 46080 },
	/* SCI2.1mid+ */ { 0, 0, 3, 6, 12, 20, 48, 96, 184, 272, 576, 1280, 3232, 6912, 13568, 24576, 46080, 73728, 132096, 466944 }
};
static int16 divisionsDefaults[2][16] = {
	/* SCI2.1early- */ { 1, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 40, 40, 101, 101 },
	/* SCI2.1mid+ */   { 1, 20, 20, 20, 20, 10, 10, 10, 10, 20, 20,  6, 10, 101, 101, 2 }
};

GfxTransitions32::GfxTransitions32(SegManager *segMan) :
	_segMan(segMan) {
	for (int i = 0; i < 236; i += 2) {
		_styleRanges[i] = 0;
		_styleRanges[i + 1] = -1;
	}
	for (int i = 236; i < ARRAYSIZE(_styleRanges); ++i) {
		_styleRanges[i] = 0;
	}

	if (getSciVersion() < SCI_VERSION_2_1_MIDDLE) {
		_dissolveSequenceSeeds = dissolveSequences[0];
		_defaultDivisions = divisionsDefaults[0];
	} else {
		_dissolveSequenceSeeds = dissolveSequences[1];
		_defaultDivisions = divisionsDefaults[1];
	}
}

GfxTransitions32::~GfxTransitions32() {
	for (ShowStyleList::iterator it = _showStyles.begin();
		 it != _showStyles.end();
		 it = deleteShowStyle(it));
	_scrolls.clear();
}

void GfxTransitions32::throttle(const uint32 ms) {
	g_sci->getEngineState()->speedThrottler(ms);
	g_sci->getEngineState()->_throttleTrigger = true;
}

void GfxTransitions32::clearShowRects() {
	g_sci->_gfxFrameout->_showList.clear();
}

void GfxTransitions32::addShowRect(const Common::Rect &rect) {
	if (!rect.isEmpty()) {
		g_sci->_gfxFrameout->_showList.add(rect);
	}
}

void GfxTransitions32::sendShowRects() {
	g_sci->_gfxFrameout->showBits();
	throttle();
}

#pragma mark -
#pragma mark Show styles

void GfxTransitions32::processShowStyles() {
	uint32 now = g_sci->getTickCount();

	bool continueProcessing;
	bool doFrameOut;
	do {
		continueProcessing = false;
		doFrameOut = false;
		ShowStyleList::iterator showStyle = _showStyles.begin();
		while (showStyle != _showStyles.end()) {
			bool finished = false;

			if (!showStyle->animate) {
				doFrameOut = true;
			}

			finished = processShowStyle(*showStyle, now);

			if (!finished) {
				continueProcessing = true;
			}

			if (finished && showStyle->processed) {
				showStyle = deleteShowStyle(showStyle);
			} else {
				showStyle = ++showStyle;
			}
		}

		if (g_engine->shouldQuit()) {
			return;
		}

		if (doFrameOut) {
			g_sci->_gfxFrameout->frameOut(true);
			throttle();
		}
	} while (continueProcessing && doFrameOut);
}

void GfxTransitions32::processEffects(PlaneShowStyle &showStyle) {
	switch(showStyle.type) {
	case kShowStyleHShutterOut:
		processHShutterOut(showStyle);
		break;
	case kShowStyleHShutterIn:
		processHShutterIn(showStyle);
		break;
	case kShowStyleVShutterOut:
		processVShutterOut(showStyle);
		break;
	case kShowStyleVShutterIn:
		processVShutterIn(showStyle);
		break;
	case kShowStyleWipeLeft:
		processWipeLeft(showStyle);
		break;
	case kShowStyleWipeRight:
		processWipeRight(showStyle);
		break;
	case kShowStyleWipeUp:
		processWipeUp(showStyle);
		break;
	case kShowStyleWipeDown:
		processWipeDown(showStyle);
		break;
	case kShowStyleIrisOut:
		processIrisOut(showStyle);
		break;
	case kShowStyleIrisIn:
		processIrisIn(showStyle);
		break;
	case kShowStyleDissolveNoMorph:
	case kShowStyleDissolve:
		processPixelDissolve(showStyle);
		break;
	case kShowStyleNone:
	case kShowStyleFadeOut:
	case kShowStyleFadeIn:
	case kShowStyleMorph:
	default:
		break;
	}
}

void GfxTransitions32::kernelSetShowStyle(const reg_t planeObj, const ShowStyleType type, const int16 seconds,
	const int16 back, const int16 priority, const int16 animate, const int16 frameOutNow,
	const reg_t pFadeArray, const int16 divisions, const int16 blackScreen) {

	const bool hasFadeArray = !pFadeArray.isNull();
	const bool hasDivisions = (divisions != -1);

	bool isFadeUp;
	int16 color;
	if (back != -1) {
		isFadeUp = false;
		color = back;
	} else {
		isFadeUp = true;
		color = g_sci->_gfxPalette32->getPlatformBlack();
	}

	// HACK: Swap Mac black/white colors to PC palette for kShowStyleDissolve.
	// We perform most SCI32 Mac palette conversions while drawing pics, views, and color CelObjs.
	// That doesn't work for kShowStyleDisolve, it uses a CelObjMem that's initialized from the
	// current frameout buffer after Mac palette conversions have already taken place.
	// Example: GK1 room 471 which dissolves from an already black screen to black.
	if (type == kShowStyleDissolve && g_sci->getPlatform() == Common::kPlatformMacintosh) {
		if (color == 0) {
			color = 255;
		} else if (color == 255) {
			color = 0;
		}
	}

	Plane *plane = g_sci->_gfxFrameout->getPlanes().findByObject(planeObj);
	if (plane == nullptr) {
		error("Plane %04x:%04x is not present in active planes list", PRINT_REG(planeObj));
	}

	bool createNewEntry = true;
	PlaneShowStyle *entry = findShowStyleForPlane(planeObj);
	if (entry != nullptr) {
		bool useExisting = true;

		if (getSciVersion() < SCI_VERSION_2_1_MIDDLE) {
			useExisting = plane->_gameRect.width() == entry->width && plane->_gameRect.height() == entry->height;
		}

		if (useExisting) {
			useExisting = entry->divisions == (hasDivisions ? divisions : _defaultDivisions[type]);
		}

		if (useExisting) {
			createNewEntry = false;
			isFadeUp = true;
			entry->currentStep = 0;
		} else {
			isFadeUp = true;
			color = entry->color;
			deleteShowStyle(findIteratorForPlane(planeObj));
			entry = nullptr;
		}
	}

	if (type == kShowStyleNone) {
		if (createNewEntry == false) {
			deleteShowStyle(findIteratorForPlane(planeObj));
		}

		// Do not add kShowStyleNone types to the showStyles list.
		//
		// This return was removed in some interpreters, notably the Windows
		// version of Hoyle 5. In this version, a fade out is done and then
		// kShowStyleNone is called to enter the new screen without a fade in.
		// In such cases, we should not return here, so that the code for
		// queuing kShowStyleNone calls is enabled and the screen palette is
		// restored in the processNone() call inside processShowStyle().
		if (g_sci->getGameId() != GID_HOYLE5)
			return;
	}

	if (createNewEntry) {
		entry = new PlaneShowStyle;
		// SSCI2.1 tests if allocation returned a null pointer but then only
		// avoids setting currentStep if this is so. Since this nonsensical, we
		// do not do that here
		entry->currentStep = 0;
		entry->processed = false;
		entry->divisions = hasDivisions ? divisions : _defaultDivisions[type];
		entry->plane = planeObj;

		if (getSciVersion() < SCI_VERSION_2_1_MIDDLE) {
			// for pixel dissolve
			entry->bitmap = NULL_REG;
			entry->bitmapScreenItem = nullptr;

			// for wipe
			entry->screenItems.clear();
			entry->width = plane->_gameRect.width();
			entry->height = plane->_gameRect.height();
		} else if (hasFadeArray) {
			// SSCI2.1mid does no check to verify that an array is successfully
			// retrieved
			SciArray &table = *_segMan->lookupArray(pFadeArray);

			const uint32 rangeCount = table.size();

			// SSCI always allocates memory even if the range table has no
			// entries, but this does not really make sense, so we avoid the
			// allocation call in this case
			if (rangeCount > 0) {
				entry->fadeColorRanges.reserve(rangeCount);
				for (uint32 i = 0; i < rangeCount; ++i) {
					entry->fadeColorRanges.push_back(table.getAsInt16(i));
				}
			}
		}
	}

	// SSCI had no nullptr check and would just crash if it got to here
	if (entry == nullptr) {
		error("Cannot edit non-existing ShowStyle entry");
	}

	entry->fadeUp = isFadeUp;
	entry->color = color;
	entry->nextTick = g_sci->getTickCount();
	entry->type = type;
	entry->animate = animate;
	entry->delay = (seconds * 60 + entry->divisions - 1) / entry->divisions;

	if (entry->delay == 0) {
		error("ShowStyle has no duration");
	}

	if (frameOutNow) {
		// Creates a reference frame for the pixel dissolves to use
		g_sci->_gfxFrameout->frameOut(false);
	}

	if (createNewEntry) {
		if (getSciVersion() <= SCI_VERSION_2_1_EARLY) {
			switch (entry->type) {
			case kShowStyleWipeLeft:
			case kShowStyleWipeRight:
				configure21EarlyHorizontalWipe(*entry, priority);
				break;
			case kShowStyleHShutterOut:
				configure21EarlyHorizontalShutter(*entry, priority);
				break;
			case kShowStyleIrisOut:
			case kShowStyleIrisIn:
				configure21EarlyIris(*entry, priority);
				break;
			case kShowStyleDissolve:
				configure21EarlyDissolve(*entry, priority, plane->_gameRect);
				break;
			default:
				// do nothing
				break;
			}
		}

		_showStyles.push_back(*entry);
		delete entry;
	}
}

void GfxTransitions32::kernelSetPalStyleRange(const uint8 fromColor, const uint8 toColor) {
	if (toColor > fromColor) {
		return;
	}

	for (int i = fromColor; i <= toColor; ++i) {
		_styleRanges[i] = 0;
	}
}

PlaneShowStyle *GfxTransitions32::findShowStyleForPlane(const reg_t planeObj) {
	for (ShowStyleList::iterator it = _showStyles.begin(); it != _showStyles.end(); ++it) {
		if (it->plane == planeObj) {
			return &*it;
		}
	}

	return nullptr;
}

ShowStyleList::iterator GfxTransitions32::findIteratorForPlane(const reg_t planeObj) {
	ShowStyleList::iterator it;
	for (it = _showStyles.begin(); it != _showStyles.end(); ++it) {
		if (it->plane == planeObj) {
			break;
		}
	}

	return it;
}

ShowStyleList::iterator GfxTransitions32::deleteShowStyle(const ShowStyleList::iterator &showStyle) {
	switch (showStyle->type) {
	case kShowStyleDissolveNoMorph:
	case kShowStyleDissolve:
		if (getSciVersion() <= SCI_VERSION_2_1_EARLY) {
			_segMan->freeBitmap(showStyle->bitmap);
			g_sci->_gfxFrameout->deleteScreenItem(*showStyle->bitmapScreenItem);
		}
		break;
	case kShowStyleWipeLeft:
	case kShowStyleWipeRight:
	case kShowStyleIrisOut:
	case kShowStyleIrisIn:
	case kShowStyleHShutterOut:
		if (getSciVersion() <= SCI_VERSION_2_1_EARLY) {
			for (uint i = 0; i < showStyle->screenItems.size(); ++i) {
				ScreenItem *screenItem = showStyle->screenItems[i];
				if (screenItem != nullptr) {
					g_sci->_gfxFrameout->deleteScreenItem(*screenItem);
				}
			}
		}
		break;
	case kShowStyleFadeIn:
	case kShowStyleFadeOut:
		// SSCI manually allocated the color ranges for fades and deleted that
		// memory here, but we use a container so there is no extra cleanup
		// needed
	case kShowStyleNone:
	case kShowStyleMorph:
	case kShowStyleHShutterIn:
		// do nothing
		break;
	default:
		error("Unknown delete transition type %d", showStyle->type);
	}

	return _showStyles.erase(showStyle);
}

void GfxTransitions32::configure21EarlyHorizontalWipe(PlaneShowStyle &showStyle, const int16 priority) {
	showStyle.numEdges = 1;
	const int divisions = showStyle.divisions;
	showStyle.screenItems.reserve(divisions);

	CelInfo32 celInfo;
	celInfo.type = kCelTypeColor;
	celInfo.color = showStyle.color;

	for (int i = 0; i < divisions; ++i) {
		Common::Rect rect;
		rect.left = showStyle.width * i / divisions;
		rect.top = 0;
		rect.right = showStyle.width * (i + 1) / divisions;
		rect.bottom = showStyle.height;
		showStyle.screenItems.push_back(new ScreenItem(showStyle.plane, celInfo, rect));
		showStyle.screenItems.back()->_priority = priority;
		showStyle.screenItems.back()->_fixedPriority = true;
	}

	if (showStyle.fadeUp) {
		for (int i = 0; i < divisions; ++i) {
			g_sci->_gfxFrameout->addScreenItem(*showStyle.screenItems[i]);
		}
	}
}

void GfxTransitions32::configure21EarlyHorizontalShutter(PlaneShowStyle &showStyle, const int16 priority) {
	showStyle.numEdges = 2;
	const int numScreenItems = showStyle.numEdges * showStyle.divisions;
	showStyle.screenItems.reserve(numScreenItems);

	CelInfo32 celInfo;
	celInfo.type = kCelTypeColor;
	celInfo.color = showStyle.color;

	const int width = showStyle.width;
	const int divisions = showStyle.divisions;

	for (int i = 0; i < divisions; ++i) {
		Common::Rect rect;

		// Left
		rect.top = 0;
		rect.right = (width + 1) * (i + 1) / (2 * divisions);
		rect.bottom = showStyle.height;
		const int16 leftLeft = rect.left;

		showStyle.screenItems.push_back(new ScreenItem(showStyle.plane, celInfo, rect));
		showStyle.screenItems.back()->_priority = priority;
		showStyle.screenItems.back()->_fixedPriority = true;

		// Right
		rect.left = width - rect.right;
		rect.right = width - leftLeft;

		showStyle.screenItems.push_back(new ScreenItem(showStyle.plane, celInfo, rect));
		showStyle.screenItems.back()->_priority = priority;
		showStyle.screenItems.back()->_fixedPriority = true;
	}

	if (showStyle.fadeUp) {
		for (int i = 0; i < numScreenItems; ++i) {
			g_sci->_gfxFrameout->addScreenItem(*showStyle.screenItems[i]);
		}
	}
}

void GfxTransitions32::configure21EarlyIris(PlaneShowStyle &showStyle, const int16 priority) {
	showStyle.numEdges = 4;
	const int numScreenItems = showStyle.numEdges * showStyle.divisions;
	showStyle.screenItems.reserve(numScreenItems);

	CelInfo32 celInfo;
	celInfo.type = kCelTypeColor;
	celInfo.color = showStyle.color;

	const int width = showStyle.width;
	const int height = showStyle.height;
	const int divisions = showStyle.divisions;

	for (int i = 0; i < divisions; ++i) {
		Common::Rect rect;

		// Top
		rect.left = (width * i) / (2 * divisions);
		rect.top = (height * i) / (2 * divisions);
		rect.right = width - rect.left;
		rect.bottom = (height + 1) * (i + 1) / (2 * divisions);
		const int16 topTop = rect.top;
		const int16 topBottom = rect.bottom;

		showStyle.screenItems.push_back(new ScreenItem(showStyle.plane, celInfo, rect));
		showStyle.screenItems.back()->_priority = priority;
		showStyle.screenItems.back()->_fixedPriority = true;

		// Bottom
		rect.top = height - rect.bottom;
		rect.bottom = height - topTop;
		const int16 bottomTop = rect.top;

		showStyle.screenItems.push_back(new ScreenItem(showStyle.plane, celInfo, rect));
		showStyle.screenItems.back()->_priority = priority;
		showStyle.screenItems.back()->_fixedPriority = true;

		// Left
		rect.top = topBottom;
		rect.right = (width + 1) * (i + 1) / (2 * divisions);
		rect.bottom = bottomTop;
		const int16 leftLeft = rect.left;

		showStyle.screenItems.push_back(new ScreenItem(showStyle.plane, celInfo, rect));
		showStyle.screenItems.back()->_priority = priority;
		showStyle.screenItems.back()->_fixedPriority = true;

		// Right
		rect.left = width - rect.right;
		rect.right = width - leftLeft;

		showStyle.screenItems.push_back(new ScreenItem(showStyle.plane, celInfo, rect));
		showStyle.screenItems.back()->_priority = priority;
		showStyle.screenItems.back()->_fixedPriority = true;
	}

	if (showStyle.fadeUp) {
		for (int i = 0; i < numScreenItems; ++i) {
			g_sci->_gfxFrameout->addScreenItem(*showStyle.screenItems[i]);
		}
	}
}

void GfxTransitions32::configure21EarlyDissolve(PlaneShowStyle &showStyle, const int16 priority, const Common::Rect &gameRect) {

	reg_t bitmapId;
	SciBitmap &bitmap = *_segMan->allocateBitmap(&bitmapId, showStyle.width, showStyle.height, kDefaultSkipColor, 0, 0, kLowResX, kLowResY, 0, false, false);

	showStyle.bitmap = bitmapId;

	const Buffer &source = g_sci->_gfxFrameout->getCurrentBuffer();
	Buffer target;
	target.init(showStyle.width, showStyle.height, showStyle.width, bitmap.getPixels(), Graphics::PixelFormat::createFormatCLUT8());

	target.fillRect(Common::Rect(bitmap.getWidth(), bitmap.getHeight()), kDefaultSkipColor);
	target.copyRectToSurface(source, 0, 0, gameRect);

	CelInfo32 celInfo;
	celInfo.type = kCelTypeMem;
	celInfo.bitmap = bitmapId;

	showStyle.bitmapScreenItem = new ScreenItem(showStyle.plane, celInfo, Common::Point(0, 0), ScaleInfo());
	showStyle.bitmapScreenItem->_priority = priority;
	showStyle.bitmapScreenItem->_fixedPriority = true;

	g_sci->_gfxFrameout->addScreenItem(*showStyle.bitmapScreenItem);
}

bool GfxTransitions32::processShowStyle(PlaneShowStyle &showStyle, uint32 now) {
	if (showStyle.nextTick >= now && showStyle.animate) {
		return false;
	}

	switch (showStyle.type) {
	default:
	case kShowStyleNone:
		return processNone(showStyle);
	case kShowStyleHShutterIn:
	case kShowStyleVShutterOut:
	case kShowStyleVShutterIn:
	case kShowStyleWipeUp:
	case kShowStyleWipeDown:
	case kShowStyleDissolveNoMorph:
	case kShowStyleMorph:
		return processMorph(showStyle);
	case kShowStyleHShutterOut:
		if (getSciVersion() > SCI_VERSION_2_1_EARLY) {
			return processMorph(showStyle);
		} else {
			return processHShutterOut(showStyle);
		}
	case kShowStyleWipeLeft:
		if (getSciVersion() > SCI_VERSION_2_1_EARLY) {
			return processMorph(showStyle);
		} else {
			return processWipe(-1, showStyle);
		}
	case kShowStyleWipeRight:
		if (getSciVersion() > SCI_VERSION_2_1_EARLY) {
			return processMorph(showStyle);
		} else {
			return processWipe(1, showStyle);
		}
	case kShowStyleDissolve:
		if (getSciVersion() > SCI_VERSION_2_1_EARLY) {
			return processMorph(showStyle);
		} else {
			return processPixelDissolve(showStyle);
		}
	case kShowStyleIrisOut:
		if (getSciVersion() > SCI_VERSION_2_1_EARLY) {
			return processMorph(showStyle);
		} else {
			return processIrisOut(showStyle);
		}
	case kShowStyleIrisIn:
		if (getSciVersion() > SCI_VERSION_2_1_EARLY) {
			return processMorph(showStyle);
		} else {
			return processIrisIn(showStyle);
		}
	case kShowStyleFadeOut:
		return processFade(-1, showStyle);
	case kShowStyleFadeIn:
		return processFade(1, showStyle);
	}
}

bool GfxTransitions32::processNone(PlaneShowStyle &showStyle) {
	if (showStyle.fadeUp) {
		g_sci->_gfxPalette32->setFade(100, 0, 255);
	} else {
		g_sci->_gfxPalette32->setFade(0, 0, 255);
	}

	showStyle.processed = true;
	return true;
}

bool GfxTransitions32::processHShutterOut(PlaneShowStyle &showStyle) {
	if (getSciVersion() > SCI_VERSION_2_1_EARLY) {
		error("HShutterOut is not known to be used by any game. Please submit a bug report with details about the game you were playing and what you were doing that triggered this error. Thanks!");
	}

	return processWipe(-1, showStyle);
}

void GfxTransitions32::processHShutterIn(const PlaneShowStyle &showStyle) {
	if (getSciVersion() <= SCI_VERSION_2_1_EARLY) {
		error("HShutterIn is not known to be used by any SCI2.1early- game. Please submit a bug report with details about the game you were playing and what you were doing that triggered this error. Thanks!");
	}

	Plane* plane = g_sci->_gfxFrameout->getVisiblePlanes().findByObject(showStyle.plane);
	const Common::Rect &screenRect = plane->_screenRect;
	Common::Rect rect;

	const int divisions = showStyle.divisions;
	const int width = screenRect.width();
	const int divisionWidth = width / divisions - 1;

	clearShowRects();

	if (width % divisions) {
		rect.left = (divisionWidth + 1) * divisions + screenRect.left;
		rect.top = screenRect.top;
		rect.right = (divisionWidth + 1) * divisions + (width % divisions) + screenRect.left;
		rect.bottom = screenRect.bottom;
		addShowRect(rect);
		sendShowRects();
	}

	for (int i = 0; i < width / (2 * divisions); ++i) {
		// Left side
		rect.left = i * divisions + screenRect.left;
		rect.top = screenRect.top;
		rect.right = i * divisions + divisions + screenRect.left;
		rect.bottom = screenRect.bottom;
		addShowRect(rect);

		// Right side
		rect.left = (divisionWidth - i) * divisions + screenRect.left;
		rect.top = screenRect.top;
		rect.right = (divisionWidth - i) * divisions + divisions + screenRect.left;
		rect.bottom = screenRect.bottom;
		addShowRect(rect);

		sendShowRects();
	}

	addShowRect(screenRect);
	sendShowRects();
}

void GfxTransitions32::processVShutterOut(PlaneShowStyle &showStyle) {
	error("VShutterOut is not known to be used by any game. Please submit a bug report with details about the game you were playing and what you were doing that triggered this error. Thanks!");
}

void GfxTransitions32::processVShutterIn(PlaneShowStyle &showStyle) {
	error("VShutterIn is not known to be used by any game. Please submit a bug report with details about the game you were playing and what you were doing that triggered this error. Thanks!");
}

void GfxTransitions32::processWipeLeft(PlaneShowStyle &showStyle) {
	if (getSciVersion() > SCI_VERSION_2_1_EARLY) {
		error("WipeLeft is not known to be used by any SCI2.1mid+ game. Please submit a bug report with details about the game you were playing and what you were doing that triggered this error. Thanks!");
	}
}

void GfxTransitions32::processWipeRight(PlaneShowStyle &showStyle) {
	if (getSciVersion() > SCI_VERSION_2_1_EARLY) {
		error("WipeRight is not known to be used by any SCI2.1mid+ game. Please submit a bug report with details about the game you were playing and what you were doing that triggered this error. Thanks!");
	}
}

void GfxTransitions32::processWipeUp(PlaneShowStyle &showStyle) {
	error("WipeUp is not known to be used by any game. Please submit a bug report with details about the game you were playing and what you were doing that triggered this error. Thanks!");
}

void GfxTransitions32::processWipeDown(PlaneShowStyle &showStyle) {
	error("WipeDown is not known to be used by any game. Please submit a bug report with details about the game you were playing and what you were doing that triggered this error. Thanks!");
}

bool GfxTransitions32::processIrisOut(PlaneShowStyle &showStyle) {
	if (getSciVersion() > SCI_VERSION_2_1_EARLY) {
		error("IrisOut is not known to be used by any SCI2.1mid+ game. Please submit a bug report with details about the game you were playing and what you were doing that triggered this error. Thanks!");
	}

	return processWipe(-1, showStyle);
}

bool GfxTransitions32::processIrisIn(PlaneShowStyle &showStyle) {
	if (getSciVersion() > SCI_VERSION_2_1_EARLY) {
		error("IrisIn is not known to be used by any SCI2.1mid+ game. Please submit a bug report with details about the game you were playing and what you were doing that triggered this error. Thanks!");
	}

	return processWipe(1, showStyle);
}

void GfxTransitions32::processDissolveNoMorph(PlaneShowStyle &showStyle) {
	error("DissolveNoMorph is not known to be used by any game. Please submit a bug report with details about the game you were playing and what you were doing that triggered this error. Thanks!");
}

inline int bitWidth(int number) {
	int width = 0;
	while (number != 0) {
		number >>= 1;
		width += 1;
	}
	return width;
}

bool GfxTransitions32::processPixelDissolve(PlaneShowStyle &showStyle) {
	if (getSciVersion() > SCI_VERSION_2_1_EARLY) {
		return processPixelDissolve21Mid(showStyle);
	} else {
		return processPixelDissolve21Early(showStyle);
	}
}

bool GfxTransitions32::processPixelDissolve21Early(PlaneShowStyle &showStyle) {
	bool unchanged = true;

	SciBitmap &bitmap = *_segMan->lookupBitmap(showStyle.bitmap);
	Buffer buffer;
	buffer.init(showStyle.width, showStyle.height, showStyle.width, bitmap.getPixels(), Graphics::PixelFormat::createFormatCLUT8());

	uint32 numPixels = showStyle.width * showStyle.height;
	uint32 numPixelsPerDivision = (numPixels + showStyle.divisions) / showStyle.divisions;

	uint32 index;
	if (showStyle.currentStep == 0) {
		int i = 0;
		index = numPixels;
		if (index != 1) {
			for (;;) {
				index >>= 1;
				if (index == 1) {
					break;
				}
				i++;
			}
		}

		showStyle.dissolveMask = _dissolveSequenceSeeds[i];
		index = 53427;

		showStyle.firstPixel = index;
		showStyle.pixel = index;
	} else {
		index = showStyle.pixel;
		for (;;) {
			if (index & 1) {
				index >>= 1;
				index ^= showStyle.dissolveMask;
			} else {
				index >>= 1;
			}

			if (index < numPixels) {
				break;
			}
		}

		if (index == showStyle.firstPixel) {
			index = 0;
		}
	}

	if (showStyle.currentStep < showStyle.divisions) {
		for (uint32 i = 0; i < numPixelsPerDivision; ++i) {
			*(byte *)buffer.getBasePtr(index % showStyle.width, index / showStyle.width) = showStyle.color;

			for (;;) {
				if (index & 1) {
					index >>= 1;
					index ^= showStyle.dissolveMask;
				} else {
					index >>= 1;
				}

				if (index < numPixels) {
					break;
				}
			}

			if (index == showStyle.firstPixel) {
				buffer.fillRect(Common::Rect(0, 0, showStyle.width, showStyle.height), showStyle.color);
				break;
			}
		}

		showStyle.pixel = index;
		showStyle.nextTick += showStyle.delay;
		++showStyle.currentStep;
		unchanged = false;
		if (showStyle.bitmapScreenItem->_created == 0) {
			showStyle.bitmapScreenItem->_updated = g_sci->_gfxFrameout->getScreenCount();
		}
	}

	if ((showStyle.currentStep >= showStyle.divisions) && unchanged) {
		if (showStyle.fadeUp) {
			showStyle.processed = true;
		}

		return true;
	}

	return false;
}

bool GfxTransitions32::processPixelDissolve21Mid(const PlaneShowStyle &showStyle) {
	// SQ6 room 530, LSL7 room 130

	Plane* plane = g_sci->_gfxFrameout->getVisiblePlanes().findByObject(showStyle.plane);
	const Common::Rect &screenRect = plane->_screenRect;
	Common::Rect rect;

	const int planeWidth = screenRect.width();
	const int planeHeight = screenRect.height();
	const int divisions = showStyle.divisions;
	const int width = planeWidth / divisions + ((planeWidth % divisions) ? 1 : 0);
	const int height = planeHeight / divisions + ((planeHeight % divisions) ? 1 : 0);

	const uint32 mask = _dissolveSequenceSeeds[bitWidth(width * height - 1)];
	int seq = 1;

	uint iteration = 0;
	const uint numIterationsPerTick = g_sci->_gfxFrameout->_showList.max_size();

	clearShowRects();

	do {
		int row = seq / width;
		int col = seq % width;

		if (row < height) {
			if (row == height - 1 && (planeHeight % divisions)) {
				if (col == width - 1 && (planeWidth % divisions)) {
					rect.left = col * divisions;
					rect.top = row * divisions;
					rect.right = col * divisions + (planeWidth % divisions);
					rect.bottom = row * divisions + (planeHeight % divisions);
					addShowRect(rect);
				} else {
					rect.left = col * divisions;
					rect.top = row * divisions;
					rect.right = col * divisions + divisions;
					rect.bottom = row * divisions + (planeHeight % divisions);
					addShowRect(rect);
				}
			} else {
				if (col == width - 1 && (planeWidth % divisions)) {
					rect.left = col * divisions;
					rect.top = row * divisions;
					rect.right = col * divisions + (planeWidth % divisions);
					rect.bottom = row * divisions + divisions;
					addShowRect(rect);
				} else {
					rect.left = col * divisions;
					rect.top = row * divisions;
					rect.right = col * divisions + divisions;
					rect.bottom = row * divisions + divisions;
					addShowRect(rect);
				}
			}
		}

		if (seq & 1) {
			seq = (seq >> 1) ^ mask;
		} else {
			seq >>= 1;
		}

		if (++iteration == numIterationsPerTick) {
			sendShowRects();
			iteration = 0;
		}
	} while (seq != 1 && !g_engine->shouldQuit());

	rect.left = screenRect.left;
	rect.top = screenRect.top;
	rect.right = screenRect.left + divisions;
	rect.bottom = screenRect.top + divisions;
	addShowRect(rect);
	sendShowRects();

	addShowRect(screenRect);
	sendShowRects();

	return true;
}

bool GfxTransitions32::processFade(const int8 direction, PlaneShowStyle &showStyle) {
	bool unchanged = true;
	if (showStyle.currentStep < showStyle.divisions) {
		int percent;
		if (direction <= 0) {
			percent = showStyle.divisions - showStyle.currentStep - 1;
		} else {
			percent = showStyle.currentStep;
		}

		percent *= 100;
		percent /= showStyle.divisions - 1;

		if (showStyle.fadeColorRanges.size()) {
			for (uint i = 0, len = showStyle.fadeColorRanges.size(); i < len; i += 2) {
				g_sci->_gfxPalette32->setFade(percent, showStyle.fadeColorRanges[i], showStyle.fadeColorRanges[i + 1]);
			}
		} else {
			g_sci->_gfxPalette32->setFade(percent, 0, 255);
		}

		++showStyle.currentStep;
		showStyle.nextTick += showStyle.delay;
		unchanged = false;
	}

	if (showStyle.currentStep >= showStyle.divisions && unchanged) {
		if (direction > 0) {
			showStyle.processed = true;
		}

		return true;
	}

	return false;
}

bool GfxTransitions32::processMorph(PlaneShowStyle &showStyle) {
	g_sci->_gfxFrameout->palMorphFrameOut(_styleRanges, &showStyle);
	showStyle.processed = true;
	return true;
}

bool GfxTransitions32::processWipe(const int8 direction, PlaneShowStyle &showStyle) {
	bool unchanged = true;
	if (showStyle.currentStep < showStyle.divisions) {
		int index;
		if (direction > 0) {
			index = showStyle.currentStep;
		} else {
			index = showStyle.divisions - showStyle.currentStep - 1;
		}

		index *= showStyle.numEdges;
		for (int i = 0; i < showStyle.numEdges; ++i) {
			ScreenItem *screenItem = showStyle.screenItems[index + i];
			if (showStyle.fadeUp) {
				g_sci->_gfxFrameout->deleteScreenItem(*screenItem);
				showStyle.screenItems[index + i] = nullptr;
			} else {
				g_sci->_gfxFrameout->addScreenItem(*screenItem);
			}
		}

		++showStyle.currentStep;
		showStyle.nextTick += showStyle.delay;
		unchanged = false;
	}

	if (showStyle.currentStep >= showStyle.divisions && unchanged) {
		if (showStyle.fadeUp) {
			showStyle.processed = true;
		}

		return true;
	}

	return false;
}

#pragma mark -
#pragma mark Scrolls

void GfxTransitions32::processScrolls() {
	for (ScrollList::iterator it = _scrolls.begin(); it != _scrolls.end(); ) {
		bool finished = processScroll(*it);
		if (finished) {
			it = _scrolls.erase(it);
		} else {
			++it;
		}
	}

	throttle(33);
}

void GfxTransitions32::kernelSetScroll(const reg_t planeId, const int16 deltaX, const int16 deltaY, const GuiResourceId pictureId, const bool animate, const bool mirrorX) {

	for (ScrollList::const_iterator it = _scrolls.begin(); it != _scrolls.end(); ++it) {
		if (it->plane == planeId) {
			error("Scroll already exists on plane %04x:%04x", PRINT_REG(planeId));
		}
	}

	if (!deltaX && !deltaY) {
		error("kSetScroll: Scroll has no movement");
	}

	if (deltaX && deltaY) {
		error("kSetScroll: Cannot scroll in two dimensions");
	}

	PlaneScroll *scroll = new PlaneScroll;
	scroll->plane = planeId;
	scroll->x = 0;
	scroll->y = 0;
	scroll->deltaX = deltaX;
	scroll->deltaY = deltaY;
	scroll->newPictureId = pictureId;
	scroll->animate = animate;
	scroll->startTick = g_sci->getTickCount();

	Plane *plane = g_sci->_gfxFrameout->getPlanes().findByObject(planeId);
	if (plane == nullptr) {
		error("kSetScroll: Plane %04x:%04x not found", PRINT_REG(planeId));
	}

	Plane *visiblePlane = g_sci->_gfxFrameout->getPlanes().findByObject(planeId);
	if (visiblePlane == nullptr) {
		error("kSetScroll: Visible plane %04x:%04x not found", PRINT_REG(planeId));
	}

	const Common::Rect &gameRect = visiblePlane->_gameRect;
	Common::Point picOrigin;

	if (deltaX) {
		picOrigin.y = 0;

		if (deltaX > 0) {
			scroll->x = picOrigin.x = -gameRect.width();
		} else {
			scroll->x = picOrigin.x = gameRect.width();
		}
	} else {
		picOrigin.x = 0;

		if (deltaY > 0) {
			scroll->y = picOrigin.y = -gameRect.height();
		} else {
			scroll->y = picOrigin.y = gameRect.height();
		}
	}

	scroll->oldPictureId = plane->addPic(pictureId, picOrigin, mirrorX);

	if (animate) {
		_scrolls.push_front(*scroll);
	} else {
		bool finished = false;
		while (!finished && !g_engine->shouldQuit()) {
			finished = processScroll(*scroll);
			g_sci->_gfxFrameout->frameOut(true);
			throttle(33);
		}
	}

	delete scroll;
}

bool GfxTransitions32::processScroll(PlaneScroll &scroll) {
	bool finished = false;
	uint32 now = g_sci->getTickCount();
	if (scroll.startTick >= now) {
		return false;
	}

	int deltaX = scroll.deltaX;
	int deltaY = scroll.deltaY;
	if (((scroll.x + deltaX) * scroll.x) <= 0) {
		deltaX = -scroll.x;
	}
	if (((scroll.y + deltaY) * scroll.y) <= 0) {
		deltaY = -scroll.y;
	}

	scroll.x += deltaX;
	scroll.y += deltaY;

	Plane *plane = g_sci->_gfxFrameout->getPlanes().findByObject(scroll.plane);

	if (plane == nullptr) {
		error("[GfxTransitions32::processScroll]: Plane %04x:%04x not found", PRINT_REG(scroll.plane));
	}

	if ((scroll.x == 0) && (scroll.y == 0)) {
		plane->deletePic(scroll.oldPictureId, scroll.newPictureId);
		finished = true;
	}

	plane->scrollScreenItems(deltaX, deltaY, true);

	return finished;
}

} // End of namespace Sci
