/* 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 "hopkins/graphics.h"

#include "hopkins/files.h"
#include "hopkins/globals.h"
#include "hopkins/hopkins.h"

#include "common/system.h"
#include "graphics/palette.h"
#include "image/pcx.h"
#include "common/file.h"
#include "common/rect.h"
#include "engines/util.h"

namespace Hopkins {

GraphicsManager::GraphicsManager(HopkinsEngine *vm) {
	_vm = vm;

	_lockCounter = 0;
	_initGraphicsFl = false;
	_screenWidth = _screenHeight = 0;
	_screenLineSize = 0;
	_palettePixels = NULL;
	_lineNbr = 0;
	_videoPtr = NULL;
	_scrollOffset = 0;
	_scrollPosX = 0;
	_largeScreenFl = false;
	_oldScrollPosX = 0;
	_backBuffer = NULL;
	_frontBuffer = NULL;
	_screenBuffer = NULL;
	_backupScreen = NULL;
	_showDirtyRects = false;

	_lineNbr2 = 0;
	_enlargedX = _enlargedY = 0;
	_enlargedXFl = _enlargedYFl = false;
	_fadeDefaultSpeed = 15;
	_fadingFl = false;
	_skipVideoLockFl = false;
	_scrollStatus = 0;
	_minX = 0;
	_minY = 20;
	_maxX = SCREEN_WIDTH * 2;
	_maxY = SCREEN_HEIGHT - 20;
	_posXClipped = _posYClipped = 0;
	_clipX1 = _clipY1 = 0;
	_clipFl = false;
	_reduceX = _reducedY = 0;
	_zoomOutFactor = 0;
	_width = 0;
	_specialWidth = 0;
	_showZones = false;
	_showLines = false;

	Common::fill(&_paletteBuffer[0], &_paletteBuffer[PALETTE_SIZE * 2], 0);
	Common::fill(&_colorTable[0], &_colorTable[PALETTE_EXT_BLOCK_SIZE], 0);
	Common::fill(&_palette[0], &_palette[PALETTE_EXT_BLOCK_SIZE], 0);
	Common::fill(&_oldPalette[0], &_oldPalette[PALETTE_EXT_BLOCK_SIZE], 0);

	if (_vm->getIsDemo()) {
		if (_vm->getPlatform() == Common::kPlatformLinux)
			// CHECKME: Should be false?
			_manualScroll = true;
		else
			_manualScroll = false;
		_scrollSpeed = 16;
	} else {
		_manualScroll = false;
		_scrollSpeed = 32;
	}

	_noFadingFl = false;
}

GraphicsManager::~GraphicsManager() {
	_vm->_globals->freeMemory(_backBuffer);
	_vm->_globals->freeMemory(_frontBuffer);
	_vm->_globals->freeMemory(_screenBuffer);
	_vm->_globals->freeMemory(_backupScreen);
}

void GraphicsManager::setGraphicalMode(int width, int height) {
	if (!_initGraphicsFl) {
		Graphics::PixelFormat pixelFormat16(2, 5, 6, 5, 0, 11, 5, 0, 0);
		initGraphics(width, height, &pixelFormat16);

		// Init surfaces
		_backBuffer = _vm->_globals->allocMemory(SCREEN_WIDTH * 2 * SCREEN_HEIGHT);
		_frontBuffer = _vm->_globals->allocMemory(SCREEN_WIDTH * 2 * SCREEN_HEIGHT);
		_screenBuffer = _vm->_globals->allocMemory(SCREEN_WIDTH * 2 * SCREEN_HEIGHT);

		_videoPtr = NULL;
		_screenWidth = width;
		_screenHeight = height;

		_screenLineSize = SCREEN_WIDTH * 2;
		_palettePixels = _paletteBuffer;
		_lineNbr = width;

		_initGraphicsFl = true;
	} else {
		error("setGraphicalMode called multiple times");
	}
}

/**
 * (try to) Lock Screen
 */
void GraphicsManager::lockScreen() {
	if (!_skipVideoLockFl) {
		if (_lockCounter++ == 0) {
			_videoPtr = _screenBuffer;
			_screenLineSize = SCREEN_WIDTH * 2;
		}
	}
}

/**
 * (try to) Unlock Screen
 */
void GraphicsManager::unlockScreen() {
	assert(_videoPtr);
	if (--_lockCounter == 0) {
		_videoPtr = NULL;
	}
}

/**
 * Clear Screen
 */
void GraphicsManager::clearScreen() {
	lockScreen();
	assert(_videoPtr);

	Common::fill(_screenBuffer, _screenBuffer + _screenLineSize * _screenHeight, 0);
	addRefreshRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
	unlockScreen();
}

void GraphicsManager::clearVesaScreen() {
	Common::fill(_backBuffer, _backBuffer + _screenLineSize * _screenHeight, 0);
	Common::fill(_frontBuffer, _frontBuffer + _screenLineSize * _screenHeight, 0);
	addDirtyRect(Common::Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT));
}

/**
 * Load Image
 */
void GraphicsManager::loadImage(const Common::String &file) {
	Common::String filename	= Common::String::format("%s.PCX", file.c_str());
	loadScreen(filename);
	initColorTable(165, 170, _palette);
}

/**
 * Load VGA Image
 */
void GraphicsManager::loadVgaImage(const Common::String &file) {
	setScreenWidth(SCREEN_WIDTH);
	clearScreen();
	loadPCX320(_backBuffer, file, _palette);
	memcpy(_frontBuffer, _backBuffer, 64000);
	setScreenWidth(320);
	_maxX = 320;

	copy16bFromSurfaceScaleX2(_frontBuffer);
	addRefreshRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

	fadeInBreakout();
}

/**
 * Load Screen
 */
void GraphicsManager::loadScreen(const Common::String &file) {
	Common::File f;
	assert(!_videoPtr);

	bool flag = true;
	bool fileFoundFl = false;
	_vm->_fileIO->searchCat(file, RES_PIC, fileFoundFl);
	if (!fileFoundFl) {
		if (!f.open(file))
			error("loadScreen - %s", file.c_str());

		f.seek(0, SEEK_END);
		f.close();
		flag = false;
	}

	scrollScreen(0);
	loadPCX640(_backBuffer, file, _palette, flag);

	_scrollPosX = 0;
	_oldScrollPosX = 0;
	clearPalette();

	if (!_largeScreenFl) {
		setScreenWidth(SCREEN_WIDTH);
		_maxX = SCREEN_WIDTH;
		clearScreen();

		display8BitRect(_backBuffer, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0);
	} else {
		setScreenWidth(SCREEN_WIDTH * 2);
		_maxX = SCREEN_WIDTH * 2;
		clearScreen();

		if (_manualScroll)
			display8BitRect(_backBuffer, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0);
	}

	memcpy(_frontBuffer, _backBuffer, SCREEN_WIDTH * 2 * SCREEN_HEIGHT);
}

void GraphicsManager::initColorTable(int minIndex, int maxIndex, byte *palette) {
	for (int idx = 0; idx < 256; ++idx)
		_colorTable[idx] = idx;

	translateSurface(_colorTable, palette, 256, minIndex, maxIndex);

	for (int idx = 0; idx < 256; ++idx) {
		byte v = _colorTable[idx];
		if (v > 27 || !v)
			_colorTable[idx] = 0;
	}

	_colorTable[0] = 1;
}

/**
 * Scroll Screen
 */
void GraphicsManager::scrollScreen(int amount) {
	int result = CLIP(amount, 0, SCREEN_WIDTH);
	_vm->_events->_startPos.x = result;
	_scrollOffset = result;
	_scrollPosX = result;
}

void GraphicsManager::translateSurface(byte *destP, const byte *srcP, int count, int minThreshold, int maxThreshold) {
	byte *destPosP = destP;
	for (int idx = 0; idx < count; ++idx) {
		int palIndex = *destPosP;
		int srcOffset = 3 * palIndex;
		int col1 = srcP[srcOffset] + srcP[srcOffset + 1] + srcP[srcOffset + 2];

		for (int idx2 = 0; idx2 < 38; ++idx2) {
			srcOffset = 3 * idx2;
			int col2 = srcP[srcOffset] + srcP[srcOffset + 1] + srcP[srcOffset + 2];

			col2 += minThreshold;
			if (col2 < col1)
				continue;

			col2 -= maxThreshold;
			if (col2 > col1)
				continue;

			*destPosP = (idx2 == 0) ? 1 : idx2;
			break;
		}
		destPosP++;
	}
}

void GraphicsManager::fillSurface(byte *surface, byte *col, int size) {
	byte dataVal;

	byte *dataP = surface;
	for (int count = size - 1; count; count--){
		dataVal = *dataP;
		*dataP = col[dataVal];
		dataP++;
	}
}

void GraphicsManager::loadPCX640(byte *surface, const Common::String &file, byte *palette, bool typeFlag) {
	Common::File f;
	Image::PCXDecoder pcxDecoder;

	// Clear the passed surface
	memset(surface, 0, SCREEN_WIDTH * 2 * SCREEN_HEIGHT);

	if (typeFlag) {
		// Load PCX from within the PIC resource
		if (!f.open("PIC.RES"))
			error("Error opening PIC.RES.");
		f.seek(_vm->_fileIO->_catalogPos);
	} else {
		// Load stand alone PCX file
		if (!f.open(file))
		  error("Error opening PCX %s.", file.c_str());
	}

	// Decode the PCX
	if (!pcxDecoder.loadStream(f))
		error("Error decoding PCX %s", file.c_str());

	const Graphics::Surface *s = pcxDecoder.getSurface();

	// Copy out the dimensions and pixels of the decoded surface
	_largeScreenFl = s->w > SCREEN_WIDTH;
	Common::copy((const byte *)s->getPixels(), (const byte *)s->getBasePtr(0, s->h), surface);

	// Copy out the palette
	const byte *palSrc = pcxDecoder.getPalette();
	Common::copy((const byte *)palSrc, (const byte *)palSrc + PALETTE_BLOCK_SIZE, palette);

	f.close();
}

void GraphicsManager::loadPCX320(byte *surface, const Common::String &file, byte *palette) {
	Common::File f;
	if (!f.open(file))
		error("File not found - %s", file.c_str());

	size_t filesize = f.size();

	f.read(surface, 128);
	int imageSize = filesize - 896;
	byte *ptr = _vm->_globals->allocMemory(65024);
	size_t curBufSize;
	int imageNumb;
	int imageDataSize;
	if (imageSize >= 64000) {
		imageNumb = imageSize / 64000 + 1;
		imageDataSize = abs(64000 * (imageSize / 64000) - imageSize);
		f.read(ptr, 64000);
		curBufSize = 64000;
	} else {
		imageNumb = 1;
		imageDataSize = imageSize;
		f.read(ptr, imageSize);
		curBufSize = imageSize;
	}
	imageNumb--;
	size_t curByteIdx = 0;
	for (int i = 0; i < 64000; i++) {
		if (curByteIdx == curBufSize) {
			curByteIdx = 0;
			--imageNumb;
			curBufSize = 64000;
			if (!imageNumb)
				curBufSize = imageDataSize;
			f.read(ptr, curBufSize);
		}
		byte curByte = ptr[curByteIdx++];
		if (curByte > 192) {
			int repeatCount = curByte - 192;
			if (curByteIdx == curBufSize) {
				curByteIdx = 0;
				--imageNumb;
				curBufSize = 64000;
				if (imageNumb == 1)
					curBufSize = imageDataSize;
				f.read(ptr, curBufSize);
			}
			curByte = ptr[curByteIdx++];
			for (; repeatCount; repeatCount--)
				surface[i++] = curByte;

			--i;
		} else {
			surface[i] = curByte;
		}
	}

	f.seek(filesize - 768);
	f.read(palette, 768);
	f.close();

	_vm->_globals->freeMemory(ptr);
}

// Clear Palette
void GraphicsManager::clearPalette() {
	// As weird as it sounds, this is what the original Linux executable does,
	// and not a full array clear.
	_paletteBuffer[0] = 0;
}

void GraphicsManager::setScreenWidth(int pitch) {
	_lineNbr = _lineNbr2 = pitch;
}

/**
 * Copies data from a 8-bit palette surface into the 16-bit screen
 */
void GraphicsManager::display8BitRect(const byte *surface, int xs, int ys, int width, int height, int destX, int destY) {
	lockScreen();

	assert(_videoPtr);
	const byte *srcP = xs + _lineNbr2 * ys + surface;
	byte *destP = (byte *)_videoPtr + destX * 2 + _screenLineSize * destY;

	for (int yp = 0; yp < height; ++yp) {
		// Copy over the line, using the source pixels as lookups into the pixels palette
		const byte *lineSrcP = srcP;
		byte *lineDestP = destP;

		for (int xp = 0; xp < width; ++xp) {
			lineDestP[0] = _palettePixels[lineSrcP[0] * 2];
			lineDestP[1] = _palettePixels[(lineSrcP[0] * 2) + 1];
			lineDestP += 2;
			lineSrcP++;
		}
		// Move to the start of the next line
		srcP += _lineNbr2;
		destP += _screenLineSize;
	}

	unlockScreen();
	addRefreshRect(destX, destY, destX + width, destY + height);
}

void GraphicsManager::displayScaled8BitRect(const byte *surface, int xp, int yp, int width, int height, int destX, int destY) {
	const byte *palette;
	byte *loopDestP;
	const byte *loopSrcP;
	int yCtr;

	assert(_videoPtr);
	const byte *srcP = surface + xp + 320 * yp;
	byte *destP = (byte *)_videoPtr + 30 * _screenLineSize + destX + destX + destX + destX + _screenLineSize * 2 * destY;
	int yCount = height;
	int xCount = width;

	do {
		yCtr = yCount;
		int xCtr = xCount;
		loopSrcP = srcP;
		loopDestP = destP;
		int savedXCount = xCount;
		palette = _palettePixels;

		do {
			destP[0] = destP[2] = destP[_screenLineSize] = destP[_screenLineSize + 2] = palette[2 * srcP[0]];
			destP[1] = destP[3] = destP[_screenLineSize + 1] = destP[_screenLineSize + 3] = palette[(2 * srcP[0]) + 1];
			++srcP;
			destP += 4;
			--xCtr;
		} while (xCtr);

		xCount = savedXCount;
		destP = loopDestP + _screenLineSize * 2;
		srcP = loopSrcP + 320;
		yCount = yCtr - 1;
	} while (yCtr != 1);

	addRefreshRect(destX, destY, destX + width, destY + width);
}

/**
 * Fade in. the step number is determine by parameter.
 */
void GraphicsManager::fadeIn(const byte *palette, int step, const byte *surface) {
	byte palData2[PALETTE_BLOCK_SIZE];
	int fadeStep;
	if (step > 1)
		fadeStep = step;
	else
		fadeStep = 2;
	// Initialize temporary palette
	Common::fill(&palData2[0], &palData2[PALETTE_BLOCK_SIZE], 0);

	// Set current palette to black
	setPaletteVGA256(palData2);

	// Loop through fading in the palette
	for (int fadeIndex = 0; fadeIndex < fadeStep; ++fadeIndex) {
		for (int palOffset = 0; palOffset < PALETTE_BLOCK_SIZE; palOffset += 3) {
			palData2[palOffset + 0] = fadeIndex * palette[palOffset + 0] / (fadeStep - 1);
			palData2[palOffset + 1] = fadeIndex * palette[palOffset + 1] / (fadeStep - 1);
			palData2[palOffset + 2] = fadeIndex * palette[palOffset + 2] / (fadeStep - 1);
		}

		// Set the transition palette and refresh the screen
		setPaletteVGA256(palData2);
		display8BitRect(surface, _vm->_events->_startPos.x, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0);
		updateScreen();

		// Added a delay in order to see the fading
		_vm->_events->delay(20);
	}

	// Set the final palette
	setPaletteVGA256(palette);

	// Refresh the screen
	display8BitRect(surface, _vm->_events->_startPos.x, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0);
	updateScreen();
}

/**
 * Fade out. the step number is determine by parameter.
 */
void GraphicsManager::fadeOut(const byte *palette, int step, const byte *surface) {
	byte palData[PALETTE_BLOCK_SIZE];
	if ((step > 1) && (palette) && (!_vm->_events->_escKeyFl)) {
		int fadeStep = step;
		for (int fadeIndex = 0; fadeIndex < fadeStep; fadeIndex++) {
			for (int palOffset = 0; palOffset < PALETTE_BLOCK_SIZE; palOffset += 3) {
				palData[palOffset + 0] = (fadeStep - fadeIndex - 1) * palette[palOffset + 0] / (fadeStep - 1);
				palData[palOffset + 1] = (fadeStep - fadeIndex - 1) * palette[palOffset + 1] / (fadeStep - 1);
				palData[palOffset + 2] = (fadeStep - fadeIndex - 1) * palette[palOffset + 2] / (fadeStep - 1);
			}

			setPaletteVGA256(palData);
			display8BitRect(surface, _vm->_events->_startPos.x, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0);
			updateScreen();

			_vm->_events->delay(20);
		}
	}

	// No initial palette, or end of fading
	for (int i = 0; i < PALETTE_BLOCK_SIZE; i++)
		palData[i] = 0;

	setPaletteVGA256(palData);
	display8BitRect(surface, _vm->_events->_startPos.x, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0);

	updateScreen();
}

/**
 * Short fade in. The step number is 1, the default step number is also set to 1.
 */
void GraphicsManager::fadeInShort() {
	_fadeDefaultSpeed = 1;
	fadeIn(_palette, 1, (const byte *)_frontBuffer);
}

/**
 * Short fade out. The step number is 1, the default step number is also set to 1.
 */
void GraphicsManager::fadeOutShort() {
	_fadeDefaultSpeed = 1;
	fadeOut(_palette, 1, (const byte *)_frontBuffer);
}

/**
 * Long fade in. The step number is 20, the default step number is also set to 15.
 */
void GraphicsManager::fadeInLong() {
	_fadeDefaultSpeed = 15;
	fadeIn(_palette, 20, (const byte *)_frontBuffer);
}

/**
 * Long fade out. The step number is 20, the default step number is also set to 15.
 */
void GraphicsManager::fadeOutLong() {
	_fadeDefaultSpeed = 15;
	fadeOut(_palette, 20, (const byte *)_frontBuffer);
}

/**
 * Fade in. The step number used is the default step number.
 */
void GraphicsManager::fadeInDefaultLength(const byte *surface) {
	assert(surface);
	fadeIn(_palette, _fadeDefaultSpeed, surface);
}

/**
 * Fade out. The step number used is the default step number.
 */
void GraphicsManager::fadeOutDefaultLength(const byte *surface) {
	assert(surface);
	fadeOut(_palette, _fadeDefaultSpeed, surface);
}

/**
 * Fade in used by for the breakout mini-game
 */
void GraphicsManager::fadeInBreakout() {
	setPaletteVGA256(_palette);
	copy16bFromSurfaceScaleX2(_frontBuffer);
	updateScreen();
}

/**
 * Fade out used by for the breakout mini-game
 */
void GraphicsManager::fadeOutBreakout() {
	byte palette[PALETTE_EXT_BLOCK_SIZE];

	memset(palette, 0, PALETTE_EXT_BLOCK_SIZE);
	setPaletteVGA256(palette);
	copy16bFromSurfaceScaleX2(_frontBuffer);
	updateScreen();
}

void GraphicsManager::setPaletteVGA256(const byte *palette) {
	changePalette(palette);
}

void GraphicsManager::setPaletteVGA256WithRefresh(const byte *palette, const byte *surface) {
	changePalette(palette);
	display8BitRect(surface, _vm->_events->_startPos.x, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0);
	updateScreen();
}

void GraphicsManager::setColorPercentage(int palIndex, int r, int g, int b) {
	int palOffset = 3 * palIndex;
	_palette[palOffset] = 255 * r / 100;
	_palette[palOffset + 1] = 255 * g / 100;
	_palette[palOffset + 2] = 255 * b / 100;
}

void GraphicsManager::setColorPercentage2(int palIndex, int r, int g, int b) {
	int rv = 255 * r / 100;
	int gv = 255 * g / 100;
	int bv = 255 * b / 100;

	int palOffset = 3 * palIndex;
	_palette[palOffset] = rv;
	_palette[palOffset + 1] = gv;
	_palette[palOffset + 2] = bv;

	WRITE_UINT16(&_paletteBuffer[2 * palIndex], mapRGB(rv, gv, bv));
}

void GraphicsManager::changePalette(const byte *palette) {
	const byte *srcP = &palette[0];
	for (int idx = 0; idx < PALETTE_SIZE; ++idx, srcP += 3) {
		WRITE_UINT16(&_paletteBuffer[2 * idx], mapRGB(srcP[0], srcP[1], srcP[2]));
	}
}

uint16 GraphicsManager::mapRGB(byte r, byte g, byte b) {
	Graphics::PixelFormat format = g_system->getScreenFormat();

	return (r >> format.rLoss) << format.rShift
			| (g >> format.gLoss) << format.gShift
			| (b >> format.bLoss) << format.bShift;
}

void GraphicsManager::updateScreen() {
	// Display any aras of the screen that need refreshing
	displayDirtyRects();
	displayRefreshRects();

	// Extra checks for debug information
	if (_showZones)
		displayZones();

	if (_showLines)
		displayLines();

	// Update the screen
	g_system->updateScreen();
	debugC(1, kDebugGraphics, "updateScreen()");
}

void GraphicsManager::copyWinscanVbe3(const byte *srcData, byte *destSurface) {
	byte srcByte;
	byte destLen1;
	byte *destSlice1P;
	byte destLen2;
	byte *destSlice2P;

	int rleValue = 0;
	int destOffset = 0;
	const byte *srcP = srcData;
	for (;;) {
		srcByte = srcP[0];
		if (srcByte == kByteStop)
			return;
		if (srcByte == 211) {
			destLen1 = srcP[1];
			rleValue = srcP[2];
			destSlice1P = destOffset + destSurface;
			destOffset += destLen1;
			memset(destSlice1P, rleValue, destLen1);
			srcP += 3;
		} else if (srcByte < 222) {
			if (srcByte > 211) {
				destLen2 = (byte)(srcP[0] + 45);
				rleValue = srcP[1];
				destSlice2P = destOffset + destSurface;
				destOffset += destLen2;
				memset(destSlice2P, rleValue, destLen2);
				srcP += 2;
			} else {
				destSurface[destOffset] = srcByte;
				++srcP;
				++destOffset;
			}
		} else if (srcByte < kSetOffset) {
			destOffset += (byte)(srcP[0] + 35);
			srcP++;
		} else if (srcByte == k8bVal) {
			destOffset += srcP[1];
			srcP += 2;
		} else if (srcByte == k16bVal) {
			destOffset += READ_LE_UINT16(srcP + 1);
			srcP += 3;
		} else {
			destOffset += READ_LE_UINT32(srcP + 1);
			srcP += 5;
		}
	}
}

void GraphicsManager::copyVideoVbe16(const byte *srcData) {
	const byte *srcP = srcData;
	int destOffset = 0;

	lockScreen();
	assert(_videoPtr);

	for (;;) {
		byte srcByte = srcP[0];
		if (srcByte >= 222) {
			if (srcByte == kByteStop)
				break;

			if (srcByte < kSetOffset) {
				destOffset += srcByte - 221;
				srcByte = *++srcP;
			} else if (srcByte == k8bVal) {
				destOffset += srcP[1];
				srcByte = srcP[2];
				srcP += 2;
			} else if (srcByte == k16bVal) {
				destOffset += READ_LE_UINT16(srcP + 1);
				srcByte = srcP[3];
				srcP += 3;
			} else {
				destOffset += READ_LE_UINT32(srcP + 1);
				srcByte = srcP[5];
				srcP += 5;
			}
		}

		if (destOffset > SCREEN_WIDTH * SCREEN_HEIGHT) {
			warning("HACK: Stopping anim, out of bounds - 0x%x %d", srcByte, destOffset);
			break;
		}

		if (srcByte > 210) {
			if (srcByte == 211) {
				int pixelCount = srcP[1];
				int pixelIndex = srcP[2];
				byte *destP = (byte *)_videoPtr + destOffset * 2;
				destOffset += pixelCount;

				while (pixelCount--) {
					destP[0] = _palettePixels[2 * pixelIndex];
					destP[1] = _palettePixels[(2 * pixelIndex) + 1];
					destP += 2;
				}

				srcP += 3;
			} else {
				int pixelCount = srcByte - 211;
				int pixelIndex = srcP[1];
				byte *destP = (byte *)_videoPtr + destOffset * 2;
				destOffset += pixelCount;

				while (pixelCount--) {
					destP[0] = _palettePixels[2 * pixelIndex];
					destP[1] = _palettePixels[(2 * pixelIndex) + 1];
					destP += 2;
				}

				srcP += 2;
			}
		} else {
			byte *destP = (byte *)_videoPtr + destOffset * 2;
			destP[0] = _palettePixels[2 * srcByte];
			destP[1] = _palettePixels[(2 * srcByte) + 1];
			++srcP;
			++destOffset;
		}
	}
	unlockScreen();
}

void GraphicsManager::copyVideoVbe16a(const byte *srcData) {
	byte srcByte;
	int destOffset = 0;
	const byte *srcP = srcData;

	lockScreen();
	for (;;) {
		srcByte = srcP[0];
		if (srcByte == kByteStop)
			break;
		if (srcP[0] > kByteStop) {
			if (srcByte == k8bVal) {
				destOffset += srcP[1];
				srcByte = srcP[2];
				srcP += 2;
			} else if (srcByte == k16bVal) {
				destOffset += READ_LE_UINT16(srcP + 1);
				srcByte = srcP[3];
				srcP += 3;
			} else {
				destOffset += READ_LE_UINT32(srcP + 1);
				srcByte = srcP[5];
				srcP += 5;
			}
		}

		WRITE_LE_UINT16((byte *)_videoPtr + destOffset * 2, READ_LE_UINT16(_palettePixels + 2 * srcByte));
		++srcP;
		++destOffset;
	}
	unlockScreen();
}

void GraphicsManager::copySurfaceRect(const byte *srcSurface, byte *destSurface, int xs, int ys, int width, int height) {
	const byte *srcP;
	byte *destP;
	int rowCount;
	int rowCount2;

	// TODO: This code in the original is potentially dangerous, as it doesn't clip the area to within
	// the screen, and so thus can read areas outside of the allocated surface buffer
	srcP = xs + _lineNbr2 * ys + srcSurface;
	destP = destSurface;
	rowCount = height;
	do {
		rowCount2 = rowCount;
		if (width & 1) {
			memcpy(destP, srcP, width);
			srcP += width;
			destP += width;
		} else if (width & 2) {
			for (int i = width >> 1; i; --i) {
				destP[0] = srcP[0];
				destP[1] = srcP[1];
				srcP += 2;
				destP += 2;
			}
		} else {
			memcpy(destP, srcP, 4 * (width >> 2));
			srcP += 4 * (width >> 2);
			destP += 4 * (width >> 2);
		}
		srcP = _lineNbr2 + srcP - width;
		rowCount = rowCount2 - 1;
	} while (rowCount2 != 1);
}

/**
 * Draws a sprite onto the screen
 * @param surface		Destination surface
 * @param spriteData	The raw data for a sprite set
 * @param xp			X co-ordinate. For some reason, starts from 300 = first column
 * @param yp			Y co-ordinate. FOr some reason, starts from 300 = top row
 * @param spriteIndex	Index of the sprite to draw
 */
void GraphicsManager::drawVesaSprite(byte *surface, const byte *spriteData, int xp, int yp, int spriteIndex) {
	// Get a pointer to the start of the desired sprite
	const byte *spriteP = spriteData + 3;
	for (int i = spriteIndex; i; --i)
		spriteP += READ_LE_UINT32(spriteP) + 16;

	_posXClipped = 0;
	_posYClipped = 0;
	_clipFl = false;

	spriteP += 4;
	int width = READ_LE_UINT16(spriteP);
	spriteP += 2;
	int height = READ_LE_UINT16(spriteP);

	// Clip X
	_clipX1 = width;
	if ((xp + width) <= _minX + 300)
		return;
	if (xp < _minX + 300) {
		_posXClipped = _minX + 300 - xp;
		_clipFl = true;
	}

	// Clip Y
	_clipY1 = height;
	if (yp <= 0)
		return;
	if (yp < _minY + 300) {
		_posYClipped = _minY + 300 - yp;
		_clipFl = true;
	}

	// Clip X1
	if (xp >= _maxX + 300)
		return;
	if (xp + width > _maxX + 300) {
		int xAmount = width + 10 - (xp + width - (_maxX + 300));
		if (xAmount <= 10)
			return;

		_clipX1 = xAmount - 10;
		_clipFl = true;
	}

	// Clip Y1
	if (yp >= _maxY + 300)
		return;
	if (yp + height > _maxY + 300) {
		int yAmount = height + 10 - (yp + height - (_maxY + 300));
		if (yAmount <= 10)
			return;

		// _clipY1 is always positive thanks to the previous check
		_clipY1 = yAmount - 10;
		_clipFl = true;
	}

	// Sprite display

	// Set up source
	spriteP += 6;
	int srcOffset = READ_LE_UINT16(spriteP);
	spriteP += 4;
	const byte *srcP = spriteP;
	spriteP += srcOffset;

	// Set up surface destination
	byte *destP = surface + (yp - 300) * _lineNbr2 + (xp - 300);

	// Handling for clipped versus non-clipped
	if (_clipFl) {
		// Clipped version
		for (int yc = 0; yc < _clipY1; ++yc, destP += _lineNbr2) {
			byte *tempDestP = destP;
			byte byteVal;
			int xc = 0;

			while ((byteVal = *srcP) != 253) {
				++srcP;
				width = READ_LE_UINT16(srcP);
				srcP += 2;

				if (byteVal == 254) {
					// Copy pixel range
					for (int xv = 0; xv < width; ++xv, ++xc, ++spriteP, ++tempDestP) {
						if (_posYClipped == 0 && xc >= _posXClipped && xc < _clipX1)
							*tempDestP = *spriteP;
					}
				} else {
					// Skip over bytes
					tempDestP += width;
					xc += width;
				}
			}

			if (_posYClipped > 0)
				--_posYClipped;
			srcP += 3;
		}
	} else {
		// Non-clipped
		for (int yc = 0; yc < height; ++yc, destP += _lineNbr2) {
			byte *tempDestP = destP;
			byte byteVal;

			while ((byteVal = *srcP) != 253) {
				++srcP;
				width = READ_LE_UINT16(srcP);
				srcP += 2;

				if (byteVal == 254) {
					// Copy pixel range
					Common::copy(spriteP, spriteP + width, tempDestP);
					spriteP += width;
				}

				tempDestP += width;
			}

			// Skip over control byte and width
			srcP += 3;
		}
	}
}

void GraphicsManager::endDisplayBob() {
	for (int idx = 1; idx <= 20; ++idx) {
		if (_vm->_animMan->_animBqe[idx]._enabledFl)
			_vm->_objectsMan->hideBob(idx);
	}

	_vm->_events->refreshScreenAndEvents();
	_vm->_events->refreshScreenAndEvents();

	for (int idx = 1; idx <= 20; ++idx) {
		if (_vm->_animMan->_animBqe[idx]._enabledFl)
			_vm->_objectsMan->resetBob(idx);
	}

	for (int idx = 1; idx < 36; ++idx) {
		_vm->_objectsMan->_lockedAnims[idx]._enableFl = false;
	}

	for (int idx = 1; idx <= 20; ++idx) {
		_vm->_animMan->_animBqe[idx]._enabledFl = false;
	}
}

void GraphicsManager::displayAllBob() {
	for (int idx = 1; idx <= 20; ++idx) {
		if (_vm->_animMan->_animBqe[idx]._enabledFl)
			_vm->_objectsMan->displayBob(idx);
	}
}

void GraphicsManager::resetDirtyRects() {
	_dirtyRects.clear();
}

void GraphicsManager::resetRefreshRects() {
	_refreshRects.clear();
}

// Add a game area dirty rectangle
void GraphicsManager::addDirtyRect(int x1, int y1, int x2, int y2) {
	x1 = CLIP(x1, _minX, _maxX);
	y1 = CLIP(y1, _minY, _maxY);
	x2 = CLIP(x2, _minX, _maxX);
	y2 = CLIP(y2, _minY, _maxY);

	if ((x2 > x1) && (y2 > y1))
		addRectToArray(_dirtyRects, Common::Rect(x1, y1, x2, y2));
}

// Add a refresh rect
void GraphicsManager::addRefreshRect(int x1, int y1, int x2, int y2) {
	x1 = MAX(x1, 0);
	y1 = MAX(y1, 0);
	x2 = MIN(x2, SCREEN_WIDTH);
	y2 = MIN(y2, SCREEN_HEIGHT);

	if ((x2 > x1) && (y2 > y1))
		addRectToArray(_refreshRects, Common::Rect(x1, y1, x2, y2));
}

void GraphicsManager::addRectToArray(Common::Array<Common::Rect> &rects, const Common::Rect &newRect) {
	// Scan for an intersection with existing rects
	uint rectIndex;
	for (rectIndex = 0; rectIndex < rects.size(); ++rectIndex) {
		Common::Rect &r = rects[rectIndex];

		if (r.intersects(newRect)) {
			// Rect either intersects or is completely inside existing one, so extend existing one as necessary
			r.extend(newRect);
			break;
		}
	}
	if (rectIndex == rects.size()) {
		// Rect not intersecting any existing one, so add it in
		assert(rects.size() < DIRTY_RECTS_SIZE);
		rects.push_back(newRect);
	}

	// Take care of merging the existing rect list. This is done as a separate check even if
	// a previous extending above has been done, since the merging of the new rect above may
	// result in further rects now able to be merged

	for (int srcIndex = rects.size() - 1; srcIndex > 0; --srcIndex) {
		const Common::Rect &srcRect = rects[srcIndex];

		// Loop through all the other rects to see if it intersects them
		for (int destIndex = srcIndex - 1; destIndex >= 0; --destIndex) {
			if (rects[destIndex].intersects(srcRect)) {
				// Found an intersection, so extend the found one, and delete the original
				rects[destIndex].extend(srcRect);
				rects.remove_at(srcIndex);
				break;
			}
		}
	}
}

// Draw any game dirty rects onto the screen intermediate surface
void GraphicsManager::displayDirtyRects() {
	if (_dirtyRects.size() == 0)
		return;

	lockScreen();

	// Refresh the entire screen
	for (uint idx = 0; idx < _dirtyRects.size(); ++idx) {
		Common::Rect &r = _dirtyRects[idx];
		Common::Rect dstRect;

		if (_vm->_events->_breakoutFl) {
			displayScaled8BitRect(_frontBuffer, r.left, r.top, r.right - r.left, r.bottom - r.top, r.left, r.top);
			dstRect.left = r.left * 2;
			dstRect.top = r.top * 2 + 30;
			dstRect.setWidth((r.right - r.left) * 2);
			dstRect.setHeight((r.bottom - r.top) * 2);
		} else if (r.right > _vm->_events->_startPos.x && r.left < _vm->_events->_startPos.x + SCREEN_WIDTH) {
			r.left = MAX<int16>(r.left, _vm->_events->_startPos.x);
			r.right = MIN<int16>(r.right, (int16)_vm->_events->_startPos.x + SCREEN_WIDTH);

			display8BitRect(_frontBuffer, r.left, r.top, r.right - r.left, r.bottom - r.top, r.left - _vm->_events->_startPos.x, r.top);

			dstRect.left = r.left - _vm->_events->_startPos.x;
			dstRect.top = r.top;
			dstRect.setWidth(r.right - r.left);
			dstRect.setHeight(r.bottom - r.top);
		}

		// If it's a valid rect, then add it to the list of areas to refresh on the screen
		if (dstRect.isValidRect() && dstRect.width() > 0 && dstRect.height() > 0)
			addRectToArray(_refreshRects, dstRect);
	}

	unlockScreen();
	resetDirtyRects();
}

void GraphicsManager::displayRefreshRects() {
	debugC(1, kDebugGraphics, "displayRefreshRects() start");
	Graphics::Surface *screenSurface = NULL;
	if (_showDirtyRects) {
		screenSurface = g_system->lockScreen();
		g_system->copyRectToScreen(_screenBuffer, _screenLineSize, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
		debugC(1, kDebugGraphics, "\tcopyRectToScreen(_screenBuffer, %d, %d, %d, %d, %d) - Full Blit", _screenLineSize, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
	}

	// Loop through copying over any  specified rects to the screen
	for (uint idx = 0; idx < _refreshRects.size(); ++idx) {
		const Common::Rect &r = _refreshRects[idx];

		byte *srcP = _screenBuffer + _screenLineSize * r.top + (r.left * 2);
		g_system->copyRectToScreen(srcP, _screenLineSize, r.left, r.top, r.width(), r.height());
		debugC(1, kDebugGraphics, "\tcopyRectToScreen(_screenBuffer[%d][%d], %d, %d, %d, %d, %d) - Rect Blit", (r.left * 2), (_screenLineSize * r.top), _screenLineSize, r.left, r.top, r.width(), r.height());

		if (_showDirtyRects)
			screenSurface->frameRect(r, 0xffffff);
	}

	if (_showDirtyRects)
		g_system->unlockScreen();

	resetRefreshRects();
	debugC(1, kDebugGraphics, "displayRefreshRects() end");
}

/**
 * Display any zones for the current room
 */
void GraphicsManager::displayZones() {
	Graphics::Surface *screenSurface = g_system->lockScreen();

	for (int bobZoneId = 0; bobZoneId <= 48; bobZoneId++) {
		int bobId = _vm->_linesMan->_bobZone[bobZoneId];
		if (bobId) {
			// Get the rectangle for the zone
			Common::Rect r(_vm->_objectsMan->_bob[bobId]._oldX, _vm->_objectsMan->_bob[bobId]._oldY,
				_vm->_objectsMan->_bob[bobId]._oldX + _vm->_objectsMan->_bob[bobId]._oldWidth,
				_vm->_objectsMan->_bob[bobId]._oldY + _vm->_objectsMan->_bob[bobId]._oldHeight);

			displayDebugRect(screenSurface, r, 0xff0000);
		}
	}

	for (int squareZoneId = 0; squareZoneId <= 99; squareZoneId++) {
		if (_vm->_linesMan->_zone[squareZoneId]._enabledFl && _vm->_linesMan->_squareZone[squareZoneId]._enabledFl) {
			Common::Rect r(_vm->_linesMan->_squareZone[squareZoneId]._left, _vm->_linesMan->_squareZone[squareZoneId]._top,
				_vm->_linesMan->_squareZone[squareZoneId]._right, _vm->_linesMan->_squareZone[squareZoneId]._bottom);

			displayDebugRect(screenSurface, r, 0x00ff00);
		}
	}

	g_system->unlockScreen();
}

/**
 * Display any zones for the current room
 */
void GraphicsManager::displayLines() {
	Graphics::Surface *screenSurface = g_system->lockScreen();

	for (int lineIndex = 0; lineIndex < _vm->_linesMan->_linesNumb; lineIndex++) {
		int i = 0;
		do {
			int x = _vm->_linesMan->_lineItem[lineIndex]._lineData[i] - _scrollPosX;
			int y = _vm->_linesMan->_lineItem[lineIndex]._lineData[i+1];
			if (x >= 0 && x < SCREEN_WIDTH && y >= 0 && y < SCREEN_HEIGHT) {
				WRITE_UINT16(screenSurface->getBasePtr(x, y), 0xffff);
			}
			i += 2;
		}
		while(_vm->_linesMan->_lineItem[lineIndex]._lineData[i] != -1);
	}

	g_system->unlockScreen();
}


void GraphicsManager::displayDebugRect(Graphics::Surface *surface, const Common::Rect &srcRect, uint32 color) {
	Common::Rect r = srcRect;

	// Move for scrolling offset and adjust to crop on-screen
	r.translate(-_scrollPosX, 0);
	r.left = MAX(r.left, (int16)0);
	r.top = MAX(r.top, (int16)0);
	r.right = MIN(r.right, (int16)SCREEN_WIDTH);
	r.bottom = MIN(r.bottom, (int16)SCREEN_HEIGHT);

	// If there's an on-screen portion, display it
	if (r.isValidRect())
		surface->frameRect(r, color);
}

/**
 * Fast Display of either a compressed or vesa sprite
 */
void GraphicsManager::fastDisplay(const byte *spriteData, int xp, int yp, int spriteIndex, bool addSegment) {
	int width = _vm->_objectsMan->getWidth(spriteData, spriteIndex);
	int height = _vm->_objectsMan->getHeight(spriteData, spriteIndex);

	if (*spriteData == 78) {
		drawCompressedSprite(_backBuffer, spriteData, xp + 300, yp + 300, spriteIndex, 0, 0, false);
		drawCompressedSprite(_frontBuffer, spriteData, xp + 300, yp + 300, spriteIndex, 0, 0, false);
	} else {
		drawVesaSprite(_frontBuffer, spriteData, xp + 300, yp + 300, spriteIndex);
		drawVesaSprite(_backBuffer, spriteData, xp + 300, yp + 300, spriteIndex);
	}
	if (addSegment)
		addDirtyRect(xp, yp, xp + width, yp + height);
}

void GraphicsManager::fastDisplay2(const byte *objectData, int xp, int yp, int idx, bool addSegment) {
	int width = _vm->_objectsMan->getWidth(objectData, idx);
	int height = _vm->_objectsMan->getHeight(objectData, idx);
	if (*objectData == 78) {
		drawCompressedSprite(_backBuffer, objectData, xp + 300, yp + 300, idx, 0, 0, false);
		drawCompressedSprite(_frontBuffer, objectData, xp + 300, yp + 300, idx, 0, 0, false);
	} else {
		drawVesaSprite(_frontBuffer, objectData, xp + 300, yp + 300, idx);
		drawVesaSprite(_backBuffer, objectData, xp + 300, yp + 300, idx);
	}
	if (addSegment)
		addDirtyRect(xp, yp, xp + width, yp + height);
}

/**
 * Copy from surface to video buffer, scale 2x.
 */
void GraphicsManager::copy16bFromSurfaceScaleX2(const byte *surface) {
	byte *palPtr;
	int curPixel;

	lockScreen();

	assert(_videoPtr);
	const byte *curSurface = surface;
	byte *destPtr = 30 * _screenLineSize + (byte *)_videoPtr;
	for (int y = 200; y; y--) {
		byte *oldDestPtr = destPtr;
		for (int x = 320; x; x--) {
			curPixel = 2 * *curSurface;
			palPtr = _palettePixels + curPixel;
			destPtr[0] = destPtr[2] = destPtr[_screenLineSize] = destPtr[_screenLineSize + 2] = palPtr[0];
			destPtr[1] = destPtr[3] = destPtr[_screenLineSize + 1] = destPtr[_screenLineSize + 3] = palPtr[1];
			++curSurface;
			destPtr += 4;
		}
		destPtr = _screenLineSize * 2 + oldDestPtr;
	}

	unlockScreen();
}

void GraphicsManager::restoreSurfaceRect(byte *destSurface, const byte *src, int xp, int yp, int width, int height) {
	int yCtr;

	byte *destP = xp + _lineNbr2 * yp + destSurface;
	int yNext = height;
	const byte *srcP = src;
	do {
		yCtr = yNext;
		if (width & 1) {
			memcpy(destP, srcP, width);
			srcP += width;
			destP += width;
		} else if (width & 2) {
			for (int i = width >> 1; i; --i) {
				destP[0] = srcP[0];
				destP[1] = srcP[1];
				srcP += 2;
				destP += 2;
			}
		} else {
			memcpy(destP, srcP, 4 * (width >> 2));
			srcP += 4 * (width >> 2);
			destP += 4 * (width >> 2);
		}
		destP = _lineNbr2 + destP - width;
		yNext = yCtr - 1;
	} while (yCtr != 1);
}

/**
 * Compute the value of a parameter plus a given percentage
 */
int GraphicsManager::zoomIn(int val, int percentage) {
	if (val)
		val += percentage * (long int)val / 100;

	return val;
}

/**
 * Compute the value of a parameter minus a given percentage
 */
int GraphicsManager::zoomOut(int val, int percentage) {
	if (val)
		val -= percentage * (long int)val / 100;

	return val;
}

// Display 'Perfect?'
void GraphicsManager::drawCompressedSprite(byte *surface, const byte *srcData, int xp300, int yp300, int frameIndex, int zoom1, int zoom2, bool flipFl) {
	const byte *spriteStartP = srcData + 3;
	for (int i = frameIndex; i; --i)
		spriteStartP += READ_LE_UINT32(spriteStartP) + 16;

	const byte *spriteSizeP = spriteStartP + 4;
	int spriteWidth = READ_LE_INT16(spriteSizeP);
	spriteSizeP += 2;
	int spriteHeight2 = READ_LE_INT16(spriteSizeP);
	int spriteHeight1 = spriteHeight2;
	const byte *spritePixelsP = spriteSizeP + 10;
	_posXClipped = 0;
	_posYClipped = 0;
	_clipX1 = 0;
	_clipY1 = 0;
	if ((xp300 <= _minX) || (yp300 <= _minY) || (xp300 >= _maxX + 300) || (yp300 >= _maxY + 300))
		return;

	// Clipped values are greater or equal to zero, thanks to the previous test
	_clipX1 = _maxX + 300 - xp300;
	_clipY1 = _maxY + 300 - yp300;

	// _minX is never negative, and should be always 0
	// The previous check insures that xp300 it's always greater to it
	// After this check, posXClipped is always positive
	if (xp300 < _minX + 300)
		_posXClipped = _minX + 300 - xp300;

	// Ditto.
	if (yp300 < _minY + 300)
		_posYClipped = _minY + 300 - yp300;

	byte *dest1P = xp300 + _lineNbr2 * (yp300 - 300) - 300 + surface;
	if (zoom2) {
		_enlargedX = 0;
		_enlargedY = 0;
		_enlargedYFl = false;
		_enlargedXFl = false;
		_width = spriteWidth;
		int zoomedWidth = zoomIn(spriteWidth, zoom2);
		int zoomedHeight = zoomIn(spriteHeight1, zoom2);
		if (flipFl) {
			byte *clippedDestP = zoomedWidth + dest1P;
			if (_posYClipped) {
				if (_posYClipped < 0 || _posYClipped >= zoomedHeight)
					return;
				int hiddenHeight = 0;
				while (zoomIn(++hiddenHeight, zoom2) < _posYClipped)
					;
				spritePixelsP += _width * hiddenHeight;
				clippedDestP += _lineNbr2 * _posYClipped;
				zoomedHeight -= _posYClipped;
			}
			if (zoomedHeight > _clipY1)
				zoomedHeight = _clipY1;
			if (_posXClipped) {
				if (_posXClipped >= zoomedWidth)
					return;
				zoomedWidth -= _posXClipped;
			}
			if (zoomedWidth > _clipX1) {
				int clippedZoomedWidth = zoomedWidth - _clipX1;
				clippedDestP -= clippedZoomedWidth;
				int closestWidth = 0;
				while (zoomIn(++closestWidth, zoom2) < clippedZoomedWidth)
					;
				spritePixelsP += closestWidth;
				zoomedWidth = _clipX1;
			}
			int curHeight;
			do {
				for (;;) {
					curHeight = zoomedHeight;
					byte *oldDestP = clippedDestP;
					const byte *oldSpritePixelsP = spritePixelsP;
					_enlargedXFl = false;
					_enlargedX = 0;
					for (int i = zoomedWidth; i; _enlargedXFl = false, i--) {
						for (;;) {
							if (*spritePixelsP)
								*clippedDestP = *spritePixelsP;
							--clippedDestP;
							++spritePixelsP;
							if (!_enlargedXFl)
								_enlargedX += zoom2;
							if (_enlargedX >= 0 && _enlargedX < 100)
								break;
							_enlargedX -= 100;
							--spritePixelsP;
							_enlargedXFl = true;
							--i;
							if (!i)
								break;
						}
					}
					spritePixelsP = _width + oldSpritePixelsP;
					clippedDestP = _lineNbr2 + oldDestP;
					if (!_enlargedYFl)
						_enlargedY += zoom2;
					if (_enlargedY  >= 0 && _enlargedY < 100)
						break;
					_enlargedY -= 100;
					spritePixelsP = oldSpritePixelsP;
					_enlargedYFl = true;
					zoomedHeight = curHeight - 1;
					if (curHeight == 1)
						return;
				}
				_enlargedYFl = false;
				zoomedHeight = curHeight - 1;
			} while (curHeight != 1);
		} else {
			if (_posYClipped) {
				if (_posYClipped >= zoomedHeight)
					return;
				int closerHeight = 0;
				while (zoomIn(++closerHeight, zoom2) < _posYClipped)
					;
				spritePixelsP += _width * closerHeight;
				dest1P += _lineNbr2 * _posYClipped;
				zoomedHeight -= _posYClipped;
			}
			if (zoomedHeight > _clipY1)
				zoomedHeight = _clipY1;
			if (_posXClipped) {
				if (_posXClipped >= zoomedWidth)
					return;
				int closerWidth = 0;
				while (zoomIn(++closerWidth, zoom2) < _posXClipped)
					;
				spritePixelsP += closerWidth;
				dest1P += _posXClipped;
				zoomedWidth = zoomedWidth - _posXClipped;
			}
			if (zoomedWidth > _clipX1)
				zoomedWidth = _clipX1;

			int curHeight;
			do {
				for (;;) {
					curHeight = zoomedHeight;
					byte *oldDest1P = dest1P;
					const byte *oldSpritePixelsP = spritePixelsP;
					_enlargedXFl = false;
					_enlargedX = 0;
					for (int i = zoomedWidth; i; _enlargedXFl = false, i--) {
						for (;;) {
							if (*spritePixelsP)
								*dest1P = *spritePixelsP;
							++dest1P;
							++spritePixelsP;
							if (!_enlargedXFl)
								_enlargedX += zoom2;
							if (_enlargedX >= 0 && _enlargedX < 100)
								break;
							_enlargedX -= 100;
							--spritePixelsP;
							_enlargedXFl = true;
							--i;
							if (!i)
								break;
						}
					}
					spritePixelsP = _width + oldSpritePixelsP;
					dest1P = _lineNbr2 + oldDest1P;
					if (!_enlargedYFl)
						_enlargedY += zoom2;
					if (_enlargedY >= 0 && _enlargedY < 100)
						break;
					_enlargedY -= 100;
					spritePixelsP = oldSpritePixelsP;
					_enlargedYFl = true;
					zoomedHeight = curHeight - 1;
					if (curHeight == 1)
						return;
				}
				_enlargedYFl = false;
				zoomedHeight = curHeight - 1;
			} while (curHeight != 1);
		}
	} else if (zoom1) {
		_reduceX = 0;
		_reducedY = 0;
		_width = spriteWidth;
		_zoomOutFactor = zoom1;
		if (zoom1 < 100) {
			int zoomedSpriteWidth = zoomOut(spriteWidth, _zoomOutFactor);
			if (flipFl) {
				byte *curDestP = zoomedSpriteWidth + dest1P;
				do {
					byte *oldDestP = curDestP;
					_reducedY += _zoomOutFactor;
					if (_reducedY >= 0 && _reducedY < 100) {
						_reduceX = 0;
						int curWidth = zoomedSpriteWidth;
						for (int i = _width; i; i--) {
							_reduceX += _zoomOutFactor;
							if (_reduceX >= 0 && _reduceX < 100) {
								if (curWidth >= _posXClipped && curWidth < _clipX1 && *spritePixelsP)
									*curDestP = *spritePixelsP;
								--curDestP;
								++spritePixelsP;
								--curWidth;
							} else {
								_reduceX -= 100;
								++spritePixelsP;
							}
						}
						curDestP = _lineNbr2 + oldDestP;
					} else {
						_reducedY -= 100;
						spritePixelsP += _width;
					}
					--spriteHeight2;
				} while (spriteHeight2);
			} else {
				do {
					int oldSpriteHeight = spriteHeight2;
					byte *oldDest1P = dest1P;
					_reducedY += _zoomOutFactor;
					if (_reducedY >= 0 && _reducedY < 100) {
						_reduceX = 0;
						int curX = 0;
						for (int i = _width; i; i--) {
							_reduceX += _zoomOutFactor;
							if (_reduceX >= 0 && _reduceX < 100) {
								if (curX >= _posXClipped && curX < _clipX1 && *spritePixelsP)
									*dest1P = *spritePixelsP;
								++dest1P;
								++spritePixelsP;
								++curX;
							} else {
								_reduceX -= 100;
								++spritePixelsP;
							}
						}
						spriteHeight2 = oldSpriteHeight;
						dest1P = _lineNbr2 + oldDest1P;
					} else {
						_reducedY -= 100;
						spritePixelsP += _width;
					}
					--spriteHeight2;
				} while (spriteHeight2);
			}
		}
	} else {
		_width = spriteWidth;
		if (flipFl) {
			byte *dest2P = spriteWidth + dest1P;
			_specialWidth = spriteWidth;
			if (_posYClipped) {
				if (_posYClipped >= spriteHeight1 || spriteHeight1 < 0)
					return;
				spritePixelsP += spriteWidth * _posYClipped;
				dest2P += _lineNbr2 * _posYClipped;
				spriteHeight1 -= _posYClipped;
			}
			if (spriteHeight1 > _clipY1)
				spriteHeight1 = _clipY1;

			if (_posXClipped >= spriteWidth)
				return;
			spriteWidth -= _posXClipped;

			if (spriteWidth > _clipX1) {
				int clippedWidth = spriteWidth - _clipX1;
				spritePixelsP += clippedWidth;
				dest2P -= clippedWidth;
				spriteWidth = _clipX1;
			}
			int yCtr2;
			do {
				yCtr2 = spriteHeight1;
				byte *destCopy2P = dest2P;
				const byte *spritePixelsCopy2P = spritePixelsP;
				for (int xCtr2 = spriteWidth; xCtr2; xCtr2--) {
					if (*spritePixelsP)
						*dest2P = *spritePixelsP;
					++spritePixelsP;
					--dest2P;
				}
				spritePixelsP = _specialWidth + spritePixelsCopy2P;
				dest2P = _lineNbr2 + destCopy2P;
				spriteHeight1 = yCtr2 - 1;
			} while (yCtr2 != 1);
		} else {
			_specialWidth = spriteWidth;
			if (_posYClipped) {
				if (_posYClipped >= spriteHeight1 || spriteHeight1 < 0)
					return;
				spritePixelsP += spriteWidth * _posYClipped;
				dest1P += _lineNbr2 * _posYClipped;
				spriteHeight1 -= _posYClipped;
			}
			if (spriteHeight1 > _clipY1)
				spriteHeight1 = _clipY1;
			if (_posXClipped) {
				if (_posXClipped >= spriteWidth)
					return;
				spritePixelsP += _posXClipped;
				dest1P += _posXClipped;
				spriteWidth -= _posXClipped;
			}
			if (spriteWidth > _clipX1)
				spriteWidth = _clipX1;
			int yCtr1;
			do {
				yCtr1 = spriteHeight1;
				byte *dest1CopyP = dest1P;
				const byte *spritePixelsCopyP = spritePixelsP;
				for (int xCtr1 = spriteWidth; xCtr1; xCtr1--) {
					if (*spritePixelsP)
						*dest1P = *spritePixelsP;
					++dest1P;
					++spritePixelsP;
				}
				spritePixelsP = _specialWidth + spritePixelsCopyP;
				dest1P = _lineNbr2 + dest1CopyP;
				spriteHeight1 = yCtr1 - 1;
			} while (yCtr1 != 1);
		}
	}
}

void GraphicsManager::copySurface(const byte *surface, int x1, int y1, int width, int height, byte *destSurface, int destX, int destY) {
	int left = x1;
	int top = y1;
	int croppedWidth = width;
	int croppedHeight = height;

	if (x1 < _minX) {
		croppedWidth = width - (_minX - x1);
		left = _minX;
	}
	if (y1 < _minY) {
		croppedHeight = height - (_minY - y1);
		top = _minY;
	}

	if (top + croppedHeight > _maxY)
		croppedHeight = _maxY - top;
	if (left + croppedWidth > _maxX)
		croppedWidth = _maxX - left;

	if (croppedWidth > 0 && croppedHeight > 0) {
		int height2 = croppedHeight;
		copyRect(surface, left, top, croppedWidth, croppedHeight, destSurface, destX, destY);
		addDirtyRect(left, top, left + croppedWidth, top + height2);
	}
}

void GraphicsManager::copyRect(const byte *srcSurface, int x1, int y1, uint16 width, int height, byte *destSurface, int destX, int destY) {
	const byte *srcP = x1 + _lineNbr2 * y1 + srcSurface;
	byte *destP = destX + _lineNbr2 * destY + destSurface;
	int yp = height;
	int yCurrent;
	do {
		yCurrent = yp;
		memcpy(destP, srcP, 4 * (width >> 2));
		const byte *src2P = (srcP + 4 * (width >> 2));
		byte *dest2P = (destP + 4 * (width >> 2));
		int pitch = width - 4 * (width >> 2);
		memcpy(dest2P, src2P, pitch);
		destP = (dest2P + pitch + _lineNbr2 - width);
		srcP = (src2P + pitch + _lineNbr2 - width);
		yp = yCurrent - 1;
	} while (yCurrent != 1);
}

// Display Font
void GraphicsManager::displayFont(byte *surface, const byte *spriteData, int xp, int yp, int characterIndex, int color) {
	const byte *spriteDataP = spriteData + 3;
	for (int i = characterIndex; i; --i)
		spriteDataP += READ_LE_UINT32(spriteDataP) + 16;

	int spriteWidth = 0;
	int spriteHeight = 0;
	const byte *spriteSizeP = spriteDataP + 4;
	spriteWidth = READ_LE_INT16(spriteSizeP);
	spriteSizeP += 2;
	spriteHeight = READ_LE_INT16(spriteSizeP);
	const byte *spritePixelsP = spriteSizeP + 10;
	byte *destP = surface + xp + _lineNbr2 * yp;
	_width = spriteWidth;

	int yCtr;
	do {
		yCtr = spriteHeight;
		byte *destLineP = destP;
		for (int xCtr = spriteWidth; xCtr; xCtr--) {
			byte destByte = *spritePixelsP;
			if (*spritePixelsP) {
				if (destByte == 252)
					destByte = color;
				*destP = destByte;
			}

			++destP;
			++spritePixelsP;
		}
		destP = _lineNbr2 + destLineP;
		spriteHeight = yCtr - 1;
	} while (yCtr != 1);
}

void GraphicsManager::initScreen(const Common::String &file, int mode, bool initializeScreen) {
	Common::String filename = file + ".ini";
	bool fileFoundFl = false;

	byte *ptr = _vm->_fileIO->searchCat(filename, RES_INI, fileFoundFl);

	if (!fileFoundFl) {
		ptr = _vm->_fileIO->loadFile(filename);
	}

	if (!mode) {
		filename = file + ".spr";
		_vm->_globals->_levelSpriteBuf = _vm->_globals->freeMemory(_vm->_globals->_levelSpriteBuf);
		if (initializeScreen) {
			fileFoundFl = false;
			_vm->_globals->_levelSpriteBuf = _vm->_fileIO->searchCat(filename, RES_SLI, fileFoundFl);
			if (!fileFoundFl) {
				_vm->_globals->_levelSpriteBuf = _vm->_fileIO->loadFile(filename);
			} else {
				_vm->_globals->_levelSpriteBuf = _vm->_fileIO->loadFile("RES_SLI.RES");
			}
		}
	}
	if (READ_BE_UINT24(ptr) != MKTAG24('I', 'N', 'I')) {
		error("Invalid INI File %s", file.c_str());
	} else {
		bool doneFlag = false;
		int dataOffset = 1;

		do {
			int dataVal1 = _vm->_script->handleOpcode(ptr + 20 * dataOffset);
			if (dataVal1 == -1 || _vm->shouldQuit())
				return;

			if (dataVal1 == 2)
				dataOffset =  _vm->_script->handleGoto((ptr + 20 * dataOffset));
			if (dataVal1 == 3)
				dataOffset =  _vm->_script->handleIf(ptr, dataOffset);
			if (dataOffset == -1)
				error("Error, defective IFF");
			if (dataVal1 == 1 || dataVal1 == 4)
				++dataOffset;
			if (!dataVal1 || dataVal1 == 5)
				doneFlag = true;
		} while (!doneFlag);
	}
	_vm->_globals->freeMemory(ptr);
	_vm->_globals->_answerBuffer = _vm->_globals->freeMemory(_vm->_globals->_answerBuffer);

	filename = file + ".rep";
	fileFoundFl = false;
	byte *dataP = _vm->_fileIO->searchCat(filename, RES_REP, fileFoundFl);
	if (!fileFoundFl)
		dataP = _vm->_fileIO->loadFile(filename);

	_vm->_globals->_answerBuffer = dataP;
	_vm->_objectsMan->_forceZoneFl = true;
	_vm->_objectsMan->_changeVerbFl = false;
}

void GraphicsManager::displayScreen(bool initPalette) {
	if (initPalette)
		initColorTable(50, 65, _palette);

	if (_lineNbr == SCREEN_WIDTH)
		fillSurface(_frontBuffer, _colorTable, SCREEN_WIDTH * SCREEN_HEIGHT);
	else if (_lineNbr == (SCREEN_WIDTH * 2))
		fillSurface(_frontBuffer, _colorTable, SCREEN_WIDTH * SCREEN_HEIGHT * 2);

	display8BitRect(_frontBuffer, _vm->_events->_startPos.x, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0);
	memcpy(_backBuffer, _frontBuffer, 614399);
	updateScreen();
}

void GraphicsManager::copyWinscanVbe(const byte *src, byte *dest) {
	int destOffset = 0;
	const byte *srcPtr = src;
	for (;;) {
		byte byteVal = *srcPtr;
		if (byteVal == kByteStop)
			return;
		if (*srcPtr > kByteStop) {
			if (byteVal == k8bVal) {
				destOffset += srcPtr[1];
				byteVal = srcPtr[2];
				srcPtr += 2;
			} else if (byteVal == k16bVal) {
				destOffset += READ_LE_UINT16(srcPtr + 1);
				byteVal = srcPtr[3];
				srcPtr += 3;
			} else {
				destOffset += READ_LE_UINT32(srcPtr + 1);
				byteVal = srcPtr[5];
				srcPtr += 5;
			}
		}
		dest[destOffset] = byteVal;
		++srcPtr;
		++destOffset;
	}
}

// Reduce Screen
void GraphicsManager::reduceScreenPart(const byte *srcSurface, byte *destSurface, int xp, int yp, int width, int height, int zoom) {
	const byte *srcP = xp + _lineNbr2 * yp + srcSurface;
	byte *destP = destSurface;
	_zoomOutFactor = zoom;
	_width = width;
	_reduceX = 0;
	_reducedY = 0;
	if (zoom < 100) {
		for (int yCtr = 0; yCtr < height; ++yCtr, srcP += _lineNbr2) {
			_reducedY += _zoomOutFactor;
			if (_reducedY < 100) {
				_reduceX = 0;
				const byte *lineSrcP = srcP;

				for (int xCtr = 0; xCtr < _width; ++xCtr) {
					_reduceX += _zoomOutFactor;
					if (_reduceX < 100) {
						*destP++ = *lineSrcP++;
					} else {
						_reduceX -= 100;
						++lineSrcP;
					}
				}
			} else {
				_reducedY -= 100;
			}
		}
	}
}

/**
 * Draw horizontal line
 */
void GraphicsManager::drawHorizontalLine(byte *surface, int xp, int yp, uint16 width, byte col) {
	memset(surface + xp + _lineNbr2 * yp, col, width);
}

/**
 * Draw vertical line
 */
void GraphicsManager::drawVerticalLine(byte *surface, int xp, int yp, int height, byte col) {
	byte *destP = surface + xp + _lineNbr2 * yp;

	for (int yCtr = height; yCtr; yCtr--) {
		*destP = col;
		destP += _lineNbr2;
	}
}

/**
 * Backup the current screen
 */
void GraphicsManager::backupScreen() {
	// Allocate a new data block for the screen, if necessary
	if (_vm->_graphicsMan->_backupScreen == NULL)
		_vm->_graphicsMan->_backupScreen = _vm->_globals->allocMemory(SCREEN_WIDTH * 2 * SCREEN_HEIGHT);

	// Backup the screen
	Common::copy(_vm->_graphicsMan->_backBuffer, _vm->_graphicsMan->_backBuffer +
		SCREEN_WIDTH * 2 * SCREEN_HEIGHT, _vm->_graphicsMan->_backupScreen);
}

/**
 * Restore a previously backed up screen
 */
void GraphicsManager::restoreScreen() {
	assert(_vm->_graphicsMan->_backupScreen);

	// Restore the screen and free the buffer
	Common::copy(_vm->_graphicsMan->_backupScreen, _vm->_graphicsMan->_backupScreen +
		SCREEN_WIDTH * 2 * SCREEN_HEIGHT, _vm->_graphicsMan->_backBuffer);
	_vm->_globals->freeMemory(_vm->_graphicsMan->_backupScreen);
	_backupScreen = NULL;
}

} // End of namespace Hopkins
