/* 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.
 *
 * $URL$
 * $Id$
 *
 */

#include "common/file.h"
#include "common/endian.h"
#include "common/system.h"
#include "common/util.h"
#include "common/ptr.h"

#include "m4/globals.h"
#include "m4/graphics.h"
#include "m4/sprite.h"
#include "m4/m4.h"
#include "m4/compression.h"

namespace M4 {

RGBList::RGBList(int numEntries, RGB8 *srcData, bool freeData) {
	_size = numEntries;
	assert(numEntries <= 256);

	if (srcData == NULL) {
		_data = new RGB8[numEntries];
		_freeData = true;
	} else {
		_data = srcData;
		_freeData = freeData;
	}

	_palIndexes = new byte[numEntries];
	Common::set_to(&_palIndexes[0], &_palIndexes[numEntries], 0);
}

RGBList::~RGBList() {
	if (_freeData)
		delete[] _data;
	delete[] _palIndexes;
}

void RGBList::setRange(int start, int count, const RGB8 *src) {
	assert((start + count) <= _size);

	Common::copy(&src[0], &src[count], &_data[start]);
}

/**
 * Creates a duplicate of the given rgb list
 */
RGBList *RGBList::clone() const {
	RGBList *dest = new RGBList(_size, _data, false);
	_madsVm->_palette->addRange(dest);
	return dest;
}

//--------------------------------------------------------------------------

#define VGA_COLOR_TRANS(x) (x == 0x3f ? 255 : x << 2)

M4Surface::~M4Surface() {
	if (_rgbList) {
		_madsVm->_palette->deleteRange(_rgbList);
		delete _rgbList;
	}
	if (_ownsData)
		free();
}

void M4Surface::loadCodesM4(Common::SeekableReadStream *source) {
	if (!source) {
		free();
		return;
	}

	uint16 widthVal = source->readUint16LE();
	uint16 heightVal = source->readUint16LE();

	create(widthVal, heightVal, 1);
	source->read(pixels, widthVal * heightVal);
}

void M4Surface::loadCodesMads(Common::SeekableReadStream *source) {
	if (!source) {
		free();
		return;
	}

	uint16 widthVal = 320;
	uint16 heightVal = 156;
	byte *walkMap = new byte[source->size()];

	create(widthVal, heightVal, 1);
	source->read(walkMap, source->size());

	byte *ptr = (byte *)getBasePtr(0, 0);

	for (int y = 0; y < heightVal; y++) {
		for (int x = 0; x < widthVal; x++) {
			int ofs = x + (y * widthVal);
			if ((walkMap[ofs / 8] << (ofs % 8)) & 0x80)
				*ptr++ = 1;		// walkable
			else
				*ptr++ = 0;
		}
	}

	delete[] walkMap;
}

// Sprite related methods

void M4Surface::vLine(int x, int y1, int y2) {
	Graphics::Surface::vLine(x, y1, y2, _color);
}

void M4Surface::hLine(int x1, int x2, int y) {
	Graphics::Surface::hLine(x1, y, x2, _color);
}

void M4Surface::vLineXor(int x, int y1, int y2) {
	// Clipping
	if (x < 0 || x >= w)
		return;

	if (y2 < y1)
		SWAP(y2, y1);

	if (y1 < 0)
		y1 = 0;
	if (y2 >= h)
		y2 = h - 1;

	byte *ptr = (byte *)getBasePtr(x, y1);
	while (y1++ <= y2) {
		*ptr ^= 0xFF;
		ptr += pitch;
	}

}

void M4Surface::hLineXor(int x1, int x2, int y) {
	// Clipping
	if (y < 0 || y >= h)
		return;

	if (x2 < x1)
		SWAP(x2, x1);

	if (x1 < 0)
		x1 = 0;
	if (x2 >= w)
		x2 = w - 1;

	if (x2 < x1)
		return;

	byte *ptr = (byte *)getBasePtr(x1, y);
	while (x1++ <= x2)
		*ptr++ ^= 0xFF;

}

void M4Surface::drawLine(int x1, int y1, int x2, int y2, byte color) {
	Graphics::Surface::drawLine(x1, y1, x2, y2, color);
}


void M4Surface::frameRect(int x1, int y1, int x2, int y2) {
	Graphics::Surface::frameRect(Common::Rect(x1, y1, x2, y2), _color);
}

void M4Surface::fillRect(int x1, int y1, int x2, int y2) {
	Graphics::Surface::fillRect(Common::Rect(x1, y1, x2, y2), _color);
}

void M4Surface::drawSprite(int x, int y, SpriteInfo &info, const Common::Rect &clipRect) {

	enum {
		kStatusSkip,
		kStatusScale,
		kStatusDraw
	};

	// NOTE: The current clipping code assumes that the top left corner of the clip
	// rectangle is always 0, 0
	assert(clipRect.top == 0 && clipRect.left == 0);

	// TODO: Put err* and scaled* into SpriteInfo
	int errX = info.hotX * info.scaleX % 100;
	int errY = info.hotY * info.scaleY % 100;
	int scaledWidth = scaleValue(info.width, info.scaleX, errX);
	int scaledHeight = scaleValue(info.height, info.scaleY, errY);

	/*
	debugCN(kDebugGraphics, "M4Surface::drawSprite() info.width = %d; info.scaleX = %d; info.height = %d; info.scaleY = %d; scaledWidth = %d; scaledHeight = %d\n",
		info.width, info.scaleX, info.height, info.scaleY, scaledWidth, scaledHeight);
	*/

	int clipX = 0, clipY = 0;
	// Clip the sprite's width and height according to the clip rectangle's dimensions
	// This clips the sprite to the bottom and right
	if (x >= 0) {
		scaledWidth = MIN<int>(x + scaledWidth, clipRect.right) - x;
	} else {
		clipX = x;
		scaledWidth = x + scaledWidth;
	}
	if (y >= 0) {
		scaledHeight = MIN<int>(y + scaledHeight, clipRect.bottom) - y;
	} else {
		clipY = y;
		scaledHeight = y + scaledHeight;
	}

	//debugCN(kDebugGraphics, "M4Surface::drawSprite() width = %d; height = %d; scaledWidth = %d; scaledHeight = %d\n", info.width, info.height, scaledWidth, scaledHeight);

	// Check if sprite is inside the screen. If it's not, there's no need to draw it
	if (scaledWidth + x <= 0 || scaledHeight + y <= 0)	// check left and top (in case x,y are negative)
		return;
	if (scaledWidth <= 0 || scaledHeight <= 0)			// check right and bottom
		return;
	int heightAmt = scaledHeight;

	byte *src = info.sprite->getBasePtr();
	byte *dst = getBasePtr(x - info.hotX - clipX, y - info.hotY - clipY);

	int status = kStatusSkip;
	byte *scaledLineBuf = new byte[scaledWidth];

	while (heightAmt > 0) {

		if (status == kStatusSkip) {
			// Skip line
			errY -= info.scaleY;
			if (errY < 0)
				status = kStatusScale;
			else
				src += info.width;
		} else {

			if (status == kStatusScale) {
				// Scale current line
				byte *lineDst = scaledLineBuf;
				int curErrX = errX;
				int widthVal = scaledWidth;
				byte *tempSrc = src;
				int startX = clipX;
				while (widthVal > 0) {
					byte pixel = *tempSrc++;
					curErrX -= info.scaleX;
					while (curErrX < 0) {
						if (startX == 0) {
							*lineDst++ = pixel;
							widthVal--;
						} else {
							startX++;
						}
						curErrX += 100;
					}
				}
				src += info.width;
				status = kStatusDraw;
			}

			if (status == kStatusDraw && clipY == 0) {
				// Draw previously scaled line
				// TODO Implement different drawing types (depth, shadow etc.)
				byte *tempDst = dst;
				for (int lineX = 0; lineX < scaledWidth; lineX++) {
					byte pixel = scaledLineBuf[lineX];

					if (info.encoding & 0x80) {

						if (pixel == 0x80) {
							pixel = 0;
						} else {
							byte destPixel = *tempDst;
							byte r, g, b;
							r = CLIP((info.palette[destPixel].r * pixel) >> 10, 0, 31);
							g = CLIP((info.palette[destPixel].g * pixel) >> 10, 0, 31);
							b = CLIP((info.palette[destPixel].b * pixel) >> 10, 0, 31);
							pixel = info.inverseColourTable[(b << 10) | (g << 5) | r];
						}
					}

					if (pixel)
						*tempDst = pixel;

					tempDst++;
				}
				dst += pitch;
				heightAmt--;
				// TODO depth etc.
				//depthAddress += Destination -> Width;

				errY += 100;
				if (errY >= 0)
					status = kStatusSkip;
			} else if (status == kStatusDraw && clipY < 0) {
				clipY++;

				errY += 100;
				if (errY >= 0)
					status = kStatusSkip;
			}

		}

	}

	delete[] scaledLineBuf;

}

// Surface methods

void M4Surface::freeData() {
}

void M4Surface::clear() {
	Common::set_to((byte *)pixels, (byte *)pixels + w * h, _vm->_palette->BLACK);
}

void M4Surface::reset() {
	::free(pixels);
	pixels = NULL;
	if (_rgbList) {
		_vm->_palette->deleteRange(_rgbList);
		delete _rgbList;
		_rgbList = NULL;
	}
}

void M4Surface::frameRect(const Common::Rect &r, uint8 color) {
	Graphics::Surface::frameRect(r, color);
}

void M4Surface::fillRect(const Common::Rect &r, uint8 color) {
	Graphics::Surface::fillRect(r, color);
}

void M4Surface::copyFrom(M4Surface *src, const Common::Rect &srcBounds, int destX, int destY,
						 int transparentColour) {
	// Validation of the rectangle and position
	if ((destX >= w) || (destY >= h))
		return;

	Common::Rect copyRect = srcBounds;
	if (destX < 0) {
		copyRect.left += -destX;
		destX = 0;
	} else if (destX + copyRect.width() > w) {
		copyRect.right -= destX + copyRect.width() - w;
	}
	if (destY < 0) {
		copyRect.top += -destY;
		destY = 0;
	} else if (destY + copyRect.height() > h) {
		copyRect.bottom -= destY + copyRect.height() - h;
	}

	if (!copyRect.isValidRect())
		return;

	// Copy the specified area

	byte *data = src->getBasePtr();
	byte *srcPtr = data + (src->width() * copyRect.top + copyRect.left);
	byte *destPtr = (byte *)pixels + (destY * width()) + destX;

	for (int rowCtr = 0; rowCtr < copyRect.height(); ++rowCtr) {
		if (transparentColour == -1)
			// No transparency, so copy line over
			Common::copy(srcPtr, srcPtr + copyRect.width(), destPtr);
		else {
			// Copy each byte one at a time checking for the transparency color
			for (int xCtr = 0; xCtr < copyRect.width(); ++xCtr)
				if (srcPtr[xCtr] != transparentColour) destPtr[xCtr] = srcPtr[xCtr];
		}

		srcPtr += src->width();
		destPtr += width();
	}

	src->freeData();
}

/**
 * Copies a given image onto a destination surface with scaling, transferring only pixels that meet
 * the specified depth requirement on a secondary surface contain depth information
 */
void M4Surface::copyFrom(M4Surface *src, int destX, int destY, int depth, 
						 M4Surface *depthsSurface, int scale, int transparentColour) {

	if (scale == 100) {
		// Copy the specified area
		Common::Rect copyRect(0, 0, src->width(), src->height());

		if (destX < 0) {
			copyRect.left += -destX;
			destX = 0;
		} else if (destX + copyRect.width() > w) {
			copyRect.right -= destX + copyRect.width() - w;
		}
		if (destY < 0) {
			copyRect.top += -destY;
			destY = 0;
		} else if (destY + copyRect.height() > h) {
			copyRect.bottom -= destY + copyRect.height() - h;
		}

		if (!copyRect.isValidRect())
			return;

		byte *data = src->getBasePtr();
		byte *srcPtr = data + (src->width() * copyRect.top + copyRect.left);
		byte *depthsData = depthsSurface->getBasePtr();
		byte *depthsPtr = depthsData + (depthsSurface->pitch * destY) + destX;
		byte *destPtr = (byte *)pixels + (destY * pitch) + destX;

		// 100% scaling variation
		for (int rowCtr = 0; rowCtr < copyRect.height(); ++rowCtr) {
			// Copy each byte one at a time checking against the depth
			for (int xCtr = 0; xCtr < copyRect.width(); ++xCtr) {
				if ((depth <= (depthsPtr[xCtr] & 0x7f)) && (srcPtr[xCtr] != transparentColour))
					destPtr[xCtr] = srcPtr[xCtr];
			}

			srcPtr += src->width();
			depthsPtr += depthsSurface->width();
			destPtr += width();
		}

		src->freeData();
		depthsSurface->freeData();	
		return;
	}

	// Start of draw logic for scaled sprites
	const byte *srcPixelsP = src->getBasePtr();

	int destRight = this->width() - 1;
	int destBottom = this->height() - 1;
	bool normalFrame = true;	// TODO: false for negative frame numbers
	int frameWidth = src->width();
	int frameHeight = src->height();

	int highestDim = MAX(frameWidth, frameHeight);
	bool lineDist[MADS_SURFACE_WIDTH];
	int distIndex = 0;
	int distXCount = 0, distYCount = 0;

	int distCtr = 0;
	do {
		distCtr += scale;
		if (distCtr < 100) {
			lineDist[distIndex] = false;
		} else {
			lineDist[distIndex] = true;
			distCtr -= 100;

			if (distIndex < frameWidth)
				++distXCount;

			if (distIndex < frameHeight)
				++distYCount;
		}
	} while (++distIndex < highestDim);

	destX -= distXCount / 2;
	destY -= distYCount - 1;

	// Check x bounding area
	int spriteLeft = 0;
	int spriteWidth = distXCount;
	int widthAmount = destX + distXCount - 1;

	if (destX < 0) {
		spriteWidth += destX;
		spriteLeft -= destX;
	}
	widthAmount -= destRight;
	if (widthAmount > 0)
		spriteWidth -= widthAmount;
	
	int spriteRight = spriteLeft + spriteWidth;
	if (spriteWidth <= 0)
		return;
	if (!normalFrame) {
		destX += distXCount - 1;
		spriteLeft = -(distXCount - spriteRight);
		spriteRight = (-spriteLeft + spriteWidth);
	}

	// Check y bounding area
	int spriteTop = 0;
	int spriteHeight = distYCount;
	int heightAmount = destY + distYCount - 1;

	if (destY < 0) {
		spriteHeight += destY;
		spriteTop -= destY;
	}
	heightAmount -= destBottom;
	if (heightAmount > 0)
		spriteHeight -= heightAmount;
	int spriteBottom = spriteTop + spriteHeight;

	if (spriteHeight <= 0)
		return;

	byte *destPixelsP = this->getBasePtr(destX + spriteLeft, destY + spriteTop);
	const byte *depthPixelsP = depthsSurface->getBasePtr(destX + spriteLeft, destY + spriteTop);

	spriteLeft = (spriteLeft * (normalFrame ? 1 : -1));

	// Loop through the lines of the sprite
	for (int yp = 0, sprY = -1; yp < frameHeight; ++yp, srcPixelsP += src->pitch) {
		if (!lineDist[yp])
			// Not a display line, so skip it
			continue;
		// Check whether the sprite line is in the display range
		++sprY;
		if ((sprY >= spriteBottom) || (sprY < spriteTop))
			continue;

		// Found a line to display. Loop through the pixels
		const byte *srcP = srcPixelsP;
		const byte *depthP = depthPixelsP;
		byte *destP = destPixelsP;
		for (int xp = 0, sprX = 0; xp < frameWidth; ++xp, ++srcP) {
			if (xp < spriteLeft)
				// Not yet reached start of display area
				continue;
			if (!lineDist[sprX++])
				// Not a display pixel
				continue;

			if ((*srcP != transparentColour) && (depth <= (*depthP & 0x7f)))
				*destP = *srcP;

			++destP;
			++depthP;
		}

		// Move to the next destination line
		destPixelsP += this->pitch;
		depthPixelsP += depthsSurface->pitch;
	}

	src->freeData();
	depthsSurface->freeData();	
	this->freeData();
}

void M4Surface::loadBackgroundRiddle(const char *sceneName) {
	char resourceName[20];
	Common::SeekableReadStream *stream;
	// Loads a Riddle scene
	sprintf(resourceName, "%s.tt", sceneName);
	stream = _vm->_resourceManager->get(resourceName);
	m4LoadBackground(stream);
	_vm->_resourceManager->toss(resourceName);
}

void M4Surface::loadBackground(int sceneNumber, RGBList **palData) {
	if (_vm->isM4() || (_vm->getGameType() == GType_RexNebular)) {
		char resourceName[20];
		Common::SeekableReadStream *stream;

		if (_vm->getGameType() == GType_RexNebular) {
			// Load Rex Nebular screen
			bool hasPalette = palData != NULL;
			if (!hasPalette)
				palData = &_rgbList;

			sprintf(resourceName, "rm%d.art", sceneNumber);
			stream = _vm->_resourceManager->get(resourceName);
			rexLoadBackground(stream, palData);

			if (!hasPalette) {
				_vm->_palette->addRange(_rgbList);
				this->translate(_rgbList);
			}
		} else {
			// Loads M4 game scene
			if (palData)
				*palData = NULL;
			sprintf(resourceName, "%i.tt", sceneNumber);
			stream = _vm->_resourceManager->get(resourceName);
			m4LoadBackground(stream);
		}

		_vm->_resourceManager->toss(resourceName);

	} else {
		madsLoadBackground(sceneNumber, palData);
	}
}

void M4Surface::madsLoadBackground(int roomNumber, RGBList **palData) {
	// Get a MadsPack reference to the tile set and mapping
	char resourceName[20];
	int i;

	// Uncompressed tile map resource
	sprintf(resourceName, "rm%d.mm", roomNumber);
	MadsPack tileMapFile(resourceName, _vm);
	Common::SeekableReadStream *mapStream = tileMapFile.getItemStream(0);

	// Get the details of the tiles and map
	mapStream->readUint32LE();
	int tileCountX = mapStream->readUint16LE();
	int tileCountY = mapStream->readUint16LE();
	int tileWidthMap = mapStream->readUint16LE();
	int tileHeightMap = mapStream->readUint16LE();
	int screenWidth = mapStream->readUint16LE();
	int screenHeight = mapStream->readUint16LE();
	int tileCountMap = tileCountX * tileCountY;
	delete mapStream;

	// Obtain tile map information
	typedef Common::List<Common::SharedPtr<M4Surface> > TileSetList;
	typedef TileSetList::iterator TileSetIterator;
	TileSetList tileSet;
	uint16 *tileMap = new uint16[tileCountMap];
	mapStream = tileMapFile.getItemStream(1);
	for (i = 0; i < tileCountMap; ++i)
		tileMap[i] = mapStream->readUint16LE();
	delete mapStream;
	_vm->res()->toss(resourceName);

	// --------------------------------------------------------------------------------

	// Tile map data, which needs to be kept compressed, as the tile offsets refer to
	// the compressed data. Each tile is then uncompressed separately
	sprintf(resourceName, "rm%d.tt", roomNumber);
	Common::SeekableReadStream *tileDataComp = _vm->_resourceManager->get(resourceName);
	MadsPack tileData(tileDataComp);
	Common::SeekableReadStream *tileDataUncomp = tileData.getItemStream(0);

	// Validate that the data matches between the tiles and tile map file and is valid
	int tileCount = tileDataUncomp->readUint16LE();
	int tileWidth = tileDataUncomp->readUint16LE();
	int tileHeight = tileDataUncomp->readUint16LE();
	delete tileDataUncomp;
	assert(tileCountMap == tileCount);
	assert(tileWidth == tileWidthMap);
	assert(tileHeight == tileHeightMap);
	assert(screenWidth == _vm->_screen->width());
	assert(screenHeight <= _vm->_screen->height());

	// --------------------------------------------------------------------------------

	// Get the palette to use
	tileDataUncomp = tileData.getItemStream(2);
	// Set palette
	if (!palData) {
		_vm->_palette->setMadsPalette(tileDataUncomp, 4);
	} else {
		int numColors;
		RGB8 *rgbList = _vm->_palette->decodeMadsPalette(tileDataUncomp, &numColors);
		*palData = new RGBList(numColors, rgbList, true);
	}
	delete tileDataUncomp;

	// --------------------------------------------------------------------------------

	// Get tile data

	tileDataUncomp = tileData.getItemStream(1);
	FabDecompressor fab;
	uint32 compressedTileDataSize = 0;

	for (i = 0; i < tileCount; i++) {
		tileDataUncomp->seek(i * 4, SEEK_SET);
		uint32 tileOfs = tileDataUncomp->readUint32LE();
		M4Surface* newTile = new M4Surface(tileWidth, tileHeight);

		if (i == tileCount - 1)
			compressedTileDataSize = tileDataComp->size() - tileOfs;
		else
			compressedTileDataSize = tileDataUncomp->readUint32LE() - tileOfs;

		//debugCN(kDebugGraphics, "Tile: %i, compressed size: %i\n", i, compressedTileDataSize);

		newTile->clear();

		byte *compressedTileData = new byte[compressedTileDataSize];

		tileDataComp->seek(tileData.getDataOffset() + tileOfs, SEEK_SET);
		tileDataComp->read(compressedTileData, compressedTileDataSize);

		fab.decompress(compressedTileData, compressedTileDataSize, (byte*)newTile->pixels, tileWidth * tileHeight);
		tileSet.push_back(TileSetList::value_type(newTile));
		delete[] compressedTileData;
	}

	delete tileDataUncomp;

	// --------------------------------------------------------------------------------

	// Loop through the mapping data to place the tiles on the screen

	uint16 *tIndex = &tileMap[0];
	for (int y = 0; y < tileCountY; y++) {
		for (int x = 0; x < tileCountX; x++) {
			int tileIndex = *tIndex++;
			assert(tileIndex < tileCount);
			TileSetIterator tile = tileSet.begin();
			for (i = 0; i < tileIndex; i++)
				++tile;
			((*tile).get())->copyTo(this, x * tileWidth, y * tileHeight);
		}
	}
	tileSet.clear();
	_vm->res()->toss(resourceName);
}

void M4Surface::rexLoadBackground(Common::SeekableReadStream *source, RGBList **palData) {
	MadsPack packData(source);
	Common::SeekableReadStream *sourceUnc = packData.getItemStream(0);

	int sceneWidth = sourceUnc->readUint16LE();
	int sceneHeight = sourceUnc->readUint16LE();
	int sceneSize = sceneWidth * sceneHeight;

	// Set palette
	if (!palData) {
		_vm->_palette->setMadsPalette(sourceUnc, 4);
	} else {
		int numColors;
		RGB8 *rgbList = _vm->_palette->decodeMadsPalette(sourceUnc, &numColors);
		*palData = new RGBList(numColors, rgbList, true);
	}
	delete sourceUnc;

	// Get the raw data for the background
	sourceUnc = packData.getItemStream(1);
	assert((int)sourceUnc->size() >= sceneSize);

	create(sceneWidth, sceneHeight, 1);
	byte *pData = (byte *)pixels;
	sourceUnc->read(pData, sceneSize);

	freeData();
	delete sourceUnc;
}

#undef COL_TRANS

void M4Surface::m4LoadBackground(Common::SeekableReadStream *source) {
	M4Surface *tileBuffer = new M4Surface();
	uint curTileX = 0, curTileY = 0;
	int clipX = 0, clipY = 0;
	RGB8 palette[256];

	source->readUint32LE();			// magic, unused
	/*uint32 size =*/ source->readUint32LE();
	uint32 widthVal = source->readUint32LE();
	uint32 heightVal = source->readUint32LE();
	uint32 tilesX = source->readUint32LE();
	uint32 tilesY = source->readUint32LE();
	uint32 tileWidth = source->readUint32LE();
	uint32 tileHeight = source->readUint32LE();
	uint8 blackIndex = 0;

	// Debug
	//debugCN(kDebugGraphics, "loadBackground(): %dx%d picture (%d bytes) - %dx%d tiles of size %dx%d\n",
	//	   widthVal, heightVal, size, tilesX, tilesY, tileWidth, tileHeight);

	// BGR data, which is converted to RGB8
	for (uint i = 0; i < 256; i++) {
		palette[i].b = source->readByte() << 2;
		palette[i].g = source->readByte() << 2;
		palette[i].r = source->readByte() << 2;
		palette[i].u = source->readByte() << 2;

		if ((blackIndex == 0) && !palette[i].r && !palette[i].g && !palette[i].b)
			blackIndex = i;
	}

	_vm->_palette->setPalette(palette, 0, 256);

	// resize or create the surface
	// note that the height of the scene in game scenes is smaller than 480, as the bottom part of the
	// screen is the inventory
	assert(width() == (int)widthVal);
	//debugCN(kDebugGraphics, "width(): %d, widthVal: %d, height(): %d, heightVal: %d\n", width(), widthVal, height(), heightVal);

	tileBuffer->create(tileWidth, tileHeight, 1);

	for (curTileY = 0; curTileY < tilesY; curTileY++) {
		clipY = MIN(heightVal, (1 + curTileY) * tileHeight) - (curTileY * tileHeight);

		for (curTileX = 0; curTileX < tilesX; curTileX++) {
			clipX = MIN(widthVal, (1 + curTileX) * tileWidth) - (curTileX * tileWidth);

			// Read a tile and copy it to the destination surface
			source->read(tileBuffer->pixels, tileWidth * tileHeight);
			Common::Rect srcBounds(0, 0, clipX, clipY);
			copyFrom(tileBuffer, srcBounds, curTileX * tileWidth, curTileY * tileHeight);
		}
	}

	if (heightVal < (uint)height())
		fillRect(Common::Rect(0, heightVal, width(), height()), blackIndex);

	delete tileBuffer;
}

void M4Surface::madsLoadInterface(const Common::String &filename) {
	MadsPack intFile(filename.c_str(), _vm);
	RGB8 *palette = new RGB8[16];

	// Chunk 0, palette
	Common::SeekableReadStream *intStream = intFile.getItemStream(0);

	for (int i = 0; i < 16; i++) {
		palette[i].r = intStream->readByte() << 2;
		palette[i].g = intStream->readByte() << 2;
		palette[i].b = intStream->readByte() << 2;
		intStream->readByte();
		intStream->readByte();
		intStream->readByte();
	}
	_rgbList = new RGBList(16, palette, true);
	delete intStream;

	// Chunk 1, data
	intStream = intFile.getItemStream(1);
	create(320, 44, 1);
	intStream->read(pixels, 320 * 44);
	delete intStream;

	// Translate the interface palette
	_vm->_palette->addRange(_rgbList);
	this->translate(_rgbList);
}

void M4Surface::scrollX(int xAmount) {
	if (xAmount == 0)
		return;

	byte buffer[80];
	int direction = (xAmount > 0) ? -1 : 1;
	int xSize = ABS(xAmount);
	assert(xSize <= 80);

	byte *srcP = (byte *)getBasePtr(0, 0);

	for (int y = 0; y < height(); ++y, srcP += pitch) {
		if (direction < 0) {
			// Copy area to be overwritten
			Common::copy(srcP, srcP + xSize, &buffer[0]);
			// Shift the remainder of the line over the given area
			Common::copy(srcP + xSize, srcP + width(), srcP);
			// Move buffered area to the end of the line
			Common::copy(&buffer[0], &buffer[xSize], srcP + width() - xSize);
		} else {
			// Copy area to be overwritten
			Common::copy_backward(srcP + width() - xSize, srcP + width(), &buffer[80]);
			// Shift the remainder of the line over the given area
			Common::copy_backward(srcP, srcP + width() - xSize, srcP + width());
			// Move buffered area to the start of the line
			Common::copy_backward(&buffer[80 - xSize], &buffer[80], srcP + xSize);
		}
	}
}

void M4Surface::scrollY(int yAmount) {
	if (yAmount == 0)
		return;

	int direction = (yAmount > 0) ? 1 : -1;
	int ySize = ABS(yAmount);
	assert(ySize < (height() / 2));
	assert(width() == pitch);

	int blockSize = ySize * width();
	byte *tempData = (byte *)malloc(blockSize);
	byte *pixelsP = (byte *)getBasePtr(0, 0);

	if (direction > 0) {
		// Buffer the lines to be overwritten
		byte *srcP = (byte *)getBasePtr(0, height() - ySize);
		Common::copy(srcP, srcP + (pitch * ySize), tempData);
		// Vertically shift all the lines
		Common::copy_backward(pixelsP, pixelsP + (pitch * (height() - ySize)),
			pixelsP + (pitch * height()));
		// Transfer the buffered lines top the top of the screen
		Common::copy(tempData, tempData + blockSize, pixelsP);
	} else {
		// Buffer the lines to be overwritten
		Common::copy(pixelsP, pixelsP + (pitch * ySize), tempData);
		// Vertically shift all the lines
		Common::copy(pixelsP + (pitch * ySize), pixelsP + (pitch * height()), pixelsP);
		// Transfer the buffered lines to the bottom of the screen
		Common::copy(tempData, tempData + blockSize, pixelsP + (pitch * (height() - ySize))); 
	}

	::free(tempData);
}


void M4Surface::translate(RGBList *list, bool isTransparent) {
	byte *p = getBasePtr(0, 0);
	byte *palIndexes = list->palIndexes();

	for (int i = 0; i < width() * height(); ++i, ++p) {
		if (!isTransparent || (*p != TRANSPARENT_COLOUR_INDEX)) {
			if (*p < list->size())
				*p = palIndexes[*p];
			else
				warning("Pal index %d exceeds list size %d", *p, list->size());
		}
	}

	freeData();
}

M4Surface *M4Surface::flipHorizontal() const {
	M4Surface *dest = new M4Surface(width(), height());
	dest->_rgbList = (this->_rgbList == NULL) ? NULL : this->_rgbList->clone();
	
	byte *destP = dest->getBasePtr();

	for (int y = 0; y < height(); ++y) {
		const byte *srcP = getBasePtr(width() - 1, y);
		for (int x = 0; x < width(); ++x)
			*destP++ = *srcP--;
	}

	return dest;
}

//--------------------------------------------------------------------------
// Palette class
//

#define GREEN_START 32
#define NUM_GREENS 32
#define GREEN_END (GREEN_START + NUM_GREENS - 1)
#define NORMAL_START 64
#define NORMAL_END 255
#define NUM_NORMAL (NORMAL_END - NORMAL_START + 1)

// Support function for creating a list of palette indexes to change entries in the shaded range to

static void makeTranslationList(RGB8 *palData, byte transList[NUM_GREENS]) {
	int i, j, minDistance;
	byte bestIndex;

	for (i = 0; i < NUM_GREENS; ++i) {
		bestIndex = NORMAL_START;
		minDistance = 255;

		uint8 findCol = palData[GREEN_START + i].g;

		// Find the closest matching palette color
		for (j = NORMAL_START; j <= NORMAL_END; ++j) {
			int greenVal = palData[j].g;
			if (ABS(findCol - greenVal) < minDistance) {
				minDistance = ABS(findCol - greenVal);
				bestIndex = j;
			}

			if (minDistance == 0)
				break;
		}

		transList[i] = bestIndex;
	}
}

// Support function for fading in or out

static void fadeRange(MadsM4Engine *vm, RGB8 *srcPal, RGB8 *destPal,  int startIndex, int endIndex,
					 int numSteps, uint delayAmount) {
	RGB8 tempPal[256];

	// perform the fade
	for (int stepCtr = 1; stepCtr <= numSteps; ++stepCtr) {
		// Delay the specified amount
		uint32 startTime = g_system->getMillis();
		while ((g_system->getMillis() - startTime) < delayAmount) {
			vm->_events->handleEvents();
			g_system->delayMillis(10);
		}

		for (int i = startIndex; i <= endIndex; ++i) {
			// Handle the intermediate rgb values for fading
			tempPal[i].r = (byte) (srcPal[i].r + (destPal[i].r - srcPal[i].r) * stepCtr / numSteps);
			tempPal[i].g = (byte) (srcPal[i].g + (destPal[i].g - srcPal[i].g) * stepCtr / numSteps);
			tempPal[i].b = (byte) (srcPal[i].b + (destPal[i].b - srcPal[i].b) * stepCtr / numSteps);
		}

		vm->_palette->setPalette(&tempPal[startIndex], startIndex, endIndex - startIndex + 1);
		vm->_viewManager->refreshAll();
	}

	// Make sure the end palette exactly matches what is wanted
	vm->_palette->setPalette(&destPal[startIndex], startIndex, endIndex - startIndex + 1);
}

Palette::Palette(MadsM4Engine *vm) : _vm(vm) {
	reset();
	_fading_in_progress = false;
	Common::set_to(&_usageCount[0], &_usageCount[256], 0);
}

void Palette::setPalette(const byte *colors, uint start, uint num) {
	g_system->getPaletteManager()->setPalette(colors, start, num);
	reset();
}

void Palette::setPalette(const RGB8 *colors, uint start, uint num) {
	g_system->getPaletteManager()->setPalette((const byte *)colors, start, num);
	reset();
}

void Palette::grabPalette(byte *colors, uint start, uint num) {
	g_system->getPaletteManager()->grabPalette(colors, start, num);
}

void Palette::setEntry(uint index, uint8 r, uint8 g, uint8 b) {
	RGB8 c;
	c.r = r;
	c.g = g;
	c.b = b;
	c.u = 255;
	g_system->getPaletteManager()->setPalette((const byte *)&c, index, 1);
}

uint8 Palette::palIndexFromRgb(byte r, byte g, byte b, RGB8 *paletteData) {
	byte index = 0;
	int32 minDist = 0x7fffffff;
	RGB8 palData[256];
	int Rdiff, Gdiff, Bdiff;

	if (paletteData == NULL) {
		g_system->getPaletteManager()->grabPalette((byte *)palData, 0, 256);
		paletteData = &palData[0];
	}

	for (int palIndex = 0; palIndex < 256; ++palIndex) {
		Rdiff = r - paletteData[palIndex].r;
		Gdiff = g - paletteData[palIndex].g;
		Bdiff = b - paletteData[palIndex].b;

		if (Rdiff * Rdiff + Gdiff * Gdiff + Bdiff * Bdiff < minDist) {
			minDist = Rdiff * Rdiff + Gdiff * Gdiff + Bdiff * Bdiff;
			index = (uint8)palIndex;
		}
	}

	return (uint8)index;
}

void Palette::reset() {
	RGB8 palData[256];
	g_system->getPaletteManager()->grabPalette((byte *)palData, 0, 256);

	BLACK = palIndexFromRgb(0, 0, 0, palData);
	BLUE = palIndexFromRgb(0, 0, 255, palData);
	GREEN = palIndexFromRgb(0, 255, 0, palData);
	CYAN = palIndexFromRgb(0, 255, 255, palData);
	RED = palIndexFromRgb(255, 0, 0, palData);
	VIOLET = palIndexFromRgb(255, 0, 255, palData);
	BROWN = palIndexFromRgb(168, 84, 84, palData);
	LIGHT_GRAY = palIndexFromRgb(168, 168, 168, palData);
	DARK_GRAY = palIndexFromRgb(84, 84, 84, palData);
	LIGHT_BLUE = palIndexFromRgb(0, 0, 127, palData);
	LIGHT_GREEN = palIndexFromRgb(0, 127, 0, palData);
	LIGHT_CYAN = palIndexFromRgb(0, 127, 127, palData);
	LIGHT_RED = palIndexFromRgb(84, 0, 0, palData);
	PINK = palIndexFromRgb(84, 0, 0, palData);
	YELLOW = palIndexFromRgb(0, 84, 84, palData);
	WHITE = palIndexFromRgb(255, 255, 255, palData);
}

void Palette::fadeToGreen(int numSteps, uint delayAmount) {
	if (_fading_in_progress)
		return;
	_fading_in_progress = true;
	byte translationList[NUM_GREENS];

	int i;
	byte *tempP;
	uint8 greenAmount = 0;
	RGB8 *srcPalette = (RGB8 *) &_originalPalette[0];
	RGB8 *destPalette = (RGB8 *) &_fadedPalette[0];

	_vm->_palette->grabPalette(srcPalette, 0, 256);

	// Create the destination 'greenish' palette to fade to by setting the green component
	// to the average of the RGB bytes, and leaving the Red and Blue parts as 0

	Common::copy(&srcPalette[0], &srcPalette[256], &destPalette[0]);
	for (i = 32; i < 256; ++i) {
		byte luminance = (byte)((destPalette[i].r + destPalette[i].g + destPalette[i].b) / 3);
		destPalette[i].g = MIN((byte)255, luminance);
		destPalette[i].r = destPalette[i].b = 0;
	}

	// Handle the actual fading
	fadeRange(_vm, srcPalette, destPalette, 21, 255, numSteps, delayAmount);

	// Create a translation table to be used in translating pixels in the game surface
	// using palette indexes in the range the range #32-63 into values from #64-255

	makeTranslationList(destPalette, translationList);

	// Use palette indexes from #32-63 for the range of possible shades

	for (i = GREEN_START; i <= GREEN_END; ++i, greenAmount += 8) {
		destPalette[i].g = greenAmount;
		destPalette[i].r = destPalette[i].b = 0;
	}

	// Remap all pixels into the #32-63 range

	tempP = _vm->_scene->getBasePtr();
	for (int pixelCtr = 0; pixelCtr < _vm->_scene->width() * _vm->_scene->height();
			++pixelCtr, ++tempP) {
		// If pixel is in #32-63 range already, remap to higher palette entries
		if ((*tempP >= GREEN_START) && (*tempP <= GREEN_END))
			*tempP = translationList[*tempP - GREEN_START];

		*tempP = (uint8) (GREEN_START + (destPalette[*tempP].g >> 3));
	}

	_vm->_palette->setPalette(&destPalette[GREEN_START], GREEN_START, NUM_GREENS);
	_vm->_viewManager->refreshAll();
	_fading_in_progress = false;
}

void Palette::fadeFromGreen(int numSteps, uint delayAmount, bool fadeToBlack) {
	if (_fading_in_progress)
		return;
	_fading_in_progress = true;
	RGB8 blackPalette[256];
	RGB8 *fadedPalette = (RGB8 *) &_fadedPalette[0];
	RGB8 *destPalette = (RGB8 *) &_originalPalette[0];

	if (fadeToBlack) {
		Common::set_to((byte *)&blackPalette[0], (byte *)&blackPalette[256], 0);
		destPalette = &blackPalette[0];
	}

	// Initially restore the faded palette
	_vm->_palette->setPalette(fadedPalette, 0, 256);
	_vm->_viewManager->refreshAll();

	// Restore the pixel data from the original screen
	_vm->_scene->update();

	// Handle the actual fading
	fadeRange(_vm, fadedPalette, destPalette, GREEN_START, NORMAL_END, numSteps, delayAmount);

	_fading_in_progress = false;
}

void Palette::fadeIn(int numSteps, uint delayAmount, RGBList *destPalette) {
	fadeIn(numSteps, delayAmount, destPalette->data(), destPalette->size());
}

void Palette::fadeIn(int numSteps, uint delayAmount, RGB8 *destPalette, int numColors) {
	if (_fading_in_progress)
		return;

	_fading_in_progress = true;
	RGB8 blackPalette[256];
	Common::set_to((byte *)&blackPalette[0], (byte *)&blackPalette[256], 0);

	// Initially set the black palette
	_vm->_palette->setPalette(blackPalette, 0, numColors);

	// Handle the actual fading
	fadeRange(_vm, blackPalette, destPalette, 0, numColors - 1, numSteps, delayAmount);

	_fading_in_progress = false;
}

RGB8 *Palette::decodeMadsPalette(Common::SeekableReadStream *palStream, int *numColors) {
	*numColors = palStream->readUint16LE();
	assert(*numColors <= 252);

	RGB8 *palData = new RGB8[*numColors];
	Common::set_to((byte *)&palData[0], (byte *)&palData[*numColors], 0);

	for (int i = 0; i < *numColors; ++i) {
		byte r = palStream->readByte();
		byte g = palStream->readByte();
		byte b = palStream->readByte();
		palData[i].r = VGA_COLOR_TRANS(r);
		palData[i].g = VGA_COLOR_TRANS(g);
		palData[i].b = VGA_COLOR_TRANS(b);

		// The next 3 bytes are unused
		palStream->skip(3);
	}

	return palData;
}

int Palette::setMadsPalette(Common::SeekableReadStream *palStream, int indexStart) {
	int colorCount;
	RGB8 *palData = Palette::decodeMadsPalette(palStream, &colorCount);
	_vm->_palette->setPalette(palData, indexStart, colorCount);
	delete palData;
	return colorCount;
}

void Palette::setMadsSystemPalette() {
	// Rex Nebular default system palette
	resetColorCounts();

	RGB8 palData[4];
	palData[0].r = palData[0].g = palData[0].b = 0;
	palData[1].r = palData[1].g = palData[1].b = 0x54;
	palData[2].r = palData[2].g = palData[2].b = 0xb4;
	palData[3].r = palData[3].g = palData[3].b = 0xff;

	setPalette(palData, 0, 4);
	blockRange(0, 4);
}

void Palette::resetColorCounts() {
	Common::set_to(&_usageCount[0], &_usageCount[256], 0);
}

void Palette::blockRange(int startIndex, int size) {
	// Use a reference count of -1 to signal a palette index shouldn't be used
	Common::set_to(&_usageCount[startIndex], &_usageCount[startIndex + size], -1);
}

void Palette::addRange(RGBList *list) {
	RGB8 *data = list->data();
	byte *palIndexes = list->palIndexes();
	RGB8 palData[256];
	g_system->getPaletteManager()->grabPalette((byte *)&palData[0], 0, 256);
	bool paletteChanged = false;

	for (int colIndex = 0; colIndex < list->size(); ++colIndex) {
		// Scan through for an existing copy of the RGB value
		int palIndex = -1;
		while (++palIndex < 256) {
			if (_usageCount[palIndex] <= 0)
				// Palette index is to be skipped
				continue;

			if ((palData[palIndex].r == data[colIndex].r) &&
				(palData[palIndex].g == data[colIndex].g) &&
				(palData[palIndex].b == data[colIndex].b))
				// Match found
				break;
		}

		if (palIndex == 256) {
			// No match found, so find a free slot to use
			palIndex = -1;
			while (++palIndex < 256) {
				if (_usageCount[palIndex] == 0)
					break;
			}

			if (palIndex == 256)
				error("addRange - Ran out of palette space to allocate");

			palData[palIndex].r = data[colIndex].r;
			palData[palIndex].g = data[colIndex].g;
			palData[palIndex].b = data[colIndex].b;
			paletteChanged = true;
		}

		palIndexes[colIndex] = palIndex;
		++_usageCount[palIndex];
	}

	if (paletteChanged) {
		g_system->getPaletteManager()->setPalette((byte *)&palData[0], 0, 256);
		reset();
	}
}

void Palette::deleteRange(RGBList *list) {
	// Release the reference count on each of the palette entries
	for (int colIndex = 0; colIndex < list->size(); ++colIndex) {
		int palIndex = list->palIndexes()[colIndex];
		assert(_usageCount[palIndex] > 0);
		--_usageCount[palIndex];
	}
}

void Palette::deleteAllRanges() {
	for (int colIndex = 0; colIndex < 255; ++colIndex)
		_usageCount[colIndex] = 0;
}

//--------------------------------------------------------------------------
// Support methods

void decompressRle(byte *rleData, int rleSize, byte *celData, int w, int h) {
	byte *src = rleData;
	byte *dst = celData;
	byte len;
	while (1) {
		len = *src++;
		if (len == 0) {
			len = *src++;
			if (len <= 2) {
				if (len == 1) // end of sprite marker
					break;
			} else {
				while (len--)
					*dst++ = *src++;
			}
		} else {
			while (len--)
				*dst++ = *src;
			src++;
		}
	}
}

int scaleValue(int value, int scale, int err) {
	int scaled = 0;
	while (value--) {
		err -= scale;
		while (err < 0) {
			scaled++;
			err += 100;
		}
	}
	return scaled;
}

} // End of namespace M4