
/* 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 "common/system.h"
#include "common/file.h"
#include "common/textconsole.h"
#include "graphics/palette.h"
#include "graphics/primitives.h"
#include "engines/util.h"

#include "parallaction/input.h"
#include "parallaction/parallaction.h"


namespace Parallaction {

// this is the size of the receiving buffer for unpacked frames,
// since BRA uses some insanely big animations (the largest is
// part0/ani/dino.ani).
#define MAXIMUM_UNPACKED_BITMAP_SIZE	641*401


#define	LABEL_TRANSPARENT_COLOR 0xFF

void halfbritePixel(int x, int y, int color, void *data) {
	Graphics::Surface *surf = (Graphics::Surface *)data;
	byte *pixel = (byte *)surf->getBasePtr(x, y);
	*pixel &= ~0x20;
}

void drawCircleLine(int xCenter, int yCenter, int x, int y, int color, void (*plotProc)(int, int, int, void *), void *data){
	Graphics::drawLine(xCenter + x, yCenter + y, xCenter - x, yCenter + y, color, plotProc, data);
	Graphics::drawLine(xCenter + x, yCenter - y, xCenter - x, yCenter - y, color, plotProc, data);
	Graphics::drawLine(xCenter + y, yCenter + x, xCenter - y, yCenter + x, color, plotProc, data);
	Graphics::drawLine(xCenter + y, yCenter - x, xCenter - y, yCenter - x, color, plotProc, data);
}

void drawCircle(int xCenter, int yCenter, int radius, int color, void (*plotProc)(int, int, int, void *), void *data) {
	int x = 0;
	int y = radius;
	int p = 1 - radius;

	/* Plot first set of points */
	drawCircleLine(xCenter, yCenter, x, y, color, plotProc, data);

	while (x < y) {
		x++;
		if (p < 0)
			p += 2*x + 1;
		else {
			y--;
			p += 2 * (x-y) + 1;
		}
		drawCircleLine(xCenter, yCenter, x, y, color, plotProc, data);
	}
}




Palette::Palette() {

	int gameType = g_vm->getGameType();

	if (gameType == GType_Nippon) {
		_colors = 32;
		_hb = (g_vm->getPlatform() == Common::kPlatformAmiga);
	} else
	if (gameType == GType_BRA) {
		_colors = 256;
		_hb = false;
	} else
		error("can't create palette for id = '%i'", gameType);

	_size = _colors * 3;

	makeBlack();
}

Palette::Palette(const Palette &pal) {
	clone(pal);
}

void Palette::clone(const Palette &pal) {
	_colors = pal._colors;
	_hb = pal._hb;
	_size = pal._size;
	memcpy(_data, pal._data, _size);
}


void Palette::makeBlack() {
	memset(_data, 0, _size);
}

void Palette::setEntry(uint index, int red, int green, int blue) {
	assert(index < _colors);

	if (red >= 0)
		_data[index*3] = red & 0xFF;

	if (green >= 0)
		_data[index*3+1] = green & 0xFF;

	if (blue >= 0)
		_data[index*3+2] = blue & 0xFF;
}

void Palette::getEntry(uint index, int &red, int &green, int &blue) {
	assert(index < _colors);
	red   = _data[index*3];
	green = _data[index*3+1];
	blue  = _data[index*3+2];
}

void Palette::makeGrayscale() {
	byte v;
	for (uint16 i = 0; i < _colors; i++) {
		v = MAX(_data[i*3+1], _data[i*3+2]);
		v = MAX(v, _data[i*3]);
		setEntry(i, v, v, v);
	}
}

void Palette::fadeTo(const Palette& target, uint step) {

	if (step == 0)
		return;

	for (uint16 i = 0; i < _size; i++) {
		if (_data[i] == target._data[i]) continue;

		if (_data[i] < target._data[i])
			_data[i] = CLIP(_data[i] + (int)step, (int)0, (int)target._data[i]);
		else
			_data[i] = CLIP(_data[i] - (int)step, (int)target._data[i], (int)255);
	}

	return;
}

uint Palette::fillRGB(byte *rgb) {

	byte r, g, b;
	byte *hbPal = rgb + _colors * 3;

	for (uint32 i = 0; i < _colors; i++) {
		r = (_data[i*3]   << 2) | (_data[i*3]   >> 4);
		g = (_data[i*3+1] << 2) | (_data[i*3+1] >> 4);
		b = (_data[i*3+2] << 2) | (_data[i*3+2] >> 4);

		rgb[i*3]   = r;
		rgb[i*3+1] = g;
		rgb[i*3+2] = b;

		if (_hb) {
			hbPal[i*3]   = r >> 1;
			hbPal[i*3+1] = g >> 1;
			hbPal[i*3+2] = b >> 1;
		}

	}

	return ((_hb) ? 2 : 1) * _colors;
}

void Palette::rotate(uint first, uint last, bool forward) {

	byte tmp[3];

	if (forward) {					// forward

		tmp[0] = _data[first * 3];
		tmp[1] = _data[first * 3 + 1];
		tmp[2] = _data[first * 3 + 2];

		memmove(_data+first*3, _data+(first+1)*3, (last - first)*3);

		_data[last * 3]	 = tmp[0];
		_data[last * 3 + 1] = tmp[1];
		_data[last * 3 + 2] = tmp[2];

	} else {											// backward

		tmp[0] = _data[last * 3];
		tmp[1] = _data[last * 3 + 1];
		tmp[2] = _data[last * 3 + 2];

		memmove(_data+(first+1)*3, _data+first*3, (last - first)*3);

		_data[first * 3]	  = tmp[0];
		_data[first * 3 + 1] = tmp[1];
		_data[first * 3 + 2] = tmp[2];

	}

}



void Gfx::setPalette(Palette &pal) {
	byte sysPal[256*3];

	uint n = pal.fillRGB(sysPal);
	_vm->_system->getPaletteManager()->setPalette(sysPal, 0, n);
}

void Gfx::setBlackPalette() {
	Palette pal;
	setPalette(pal);
}




void Gfx::animatePalette() {

	// avoid forcing setPalette when not needed
	bool done = false;

	PaletteFxRange *range;
	for (uint16 i = 0; i < 4; i++) {
		range = &_backgroundInfo->ranges[i];

		if ((range->_flags & 1) == 0) continue;		// animated palette
		range->_timer += range->_step * 2;	// update timer

		if (range->_timer < 0x4000) continue;		// check timeout

		range->_timer = 0;							// reset timer

		_palette.rotate(range->_first, range->_last, (range->_flags & 2) != 0);

		done = true;
	}

	if (done) {
		setPalette(_palette);
	}

	return;
}





void Gfx::setHalfbriteMode(bool enable) {
	if (_vm->getPlatform() != Common::kPlatformAmiga) return;
	if (enable == _halfbrite) return;

	_halfbrite = !_halfbrite;
}

#define HALFBRITE_CIRCLE_RADIUS		48
void Gfx::setProjectorPos(int x, int y) {
	_hbCircleRadius = HALFBRITE_CIRCLE_RADIUS;
	_hbCirclePos.x = x + _hbCircleRadius;
	_hbCirclePos.y = y + _hbCircleRadius;
}

void Gfx::setProjectorProgram(int16 *data) {
	if (_nextProjectorPos == 0) {
		_nextProjectorPos = data;
	}
}

void Gfx::drawInventory() {
/*
	if ((_engineFlags & kEngineInventory) == 0) {
		return;
	}
*/
	if (_vm->_input->_inputMode != Input::kInputModeInventory) {
		return;
	}

	Common::Rect r;
	_vm->_inventoryRenderer->getRect(r);
	byte *data = _vm->_inventoryRenderer->getData();

	copyRectToScreen(data, r.width(), r.left, r.top, r.width(), r.height());
}

void Gfx::drawList(Graphics::Surface &surface, GfxObjArray &list) {
	if (list.size() == 0) {
		return;
	}

	for (uint i = 0; i < list.size(); i++) {
		drawGfxObject(list[i], surface);
	}
}

void Gfx::copyRectToScreen(const byte *buf, int pitch, int x, int y, int w, int h) {
	if (_doubleBuffering) {
		if (_overlayMode) {
			x += _scrollPosX;
			y += _scrollPosY;
		}

		byte *dst = (byte *)_backBuffer.getBasePtr(x, y);
		for (int i = 0; i < h; i++) {
			memcpy(dst, buf, w);
			buf += pitch;
			dst += _backBuffer.pitch;
		}
	} else {
		_vm->_system->copyRectToScreen(buf, pitch, x, y, w, h);
	}
}

void Gfx::clearScreen() {
	if (_doubleBuffering) {
		if (_backBuffer.getPixels()) {
			Common::Rect r(_backBuffer.w, _backBuffer.h);
			_backBuffer.fillRect(r, 0);
		}
	} else {
		_vm->_system->fillScreen(0);
	}
}

Graphics::Surface *Gfx::lockScreen() {
	if (_doubleBuffering) {
		return &_backBuffer;
	}
	return _vm->_system->lockScreen();
}

void Gfx::unlockScreen() {
	if (_doubleBuffering) {
		return;
	}
	_vm->_system->unlockScreen();
}

void Gfx::updateScreenIntern() {
	if (_doubleBuffering) {
		byte *data = (byte *)_backBuffer.getBasePtr(_scrollPosX, _scrollPosY);
		_vm->_system->copyRectToScreen(data, _backBuffer.pitch, 0, 0, _vm->_screenWidth, _vm->_screenHeight);
	}

	_vm->_system->updateScreen();
}

void Gfx::getScrollPos(Common::Point &p) {
	p.x = _scrollPosX;
	p.y = _scrollPosY;
}

void Gfx::setScrollPosX(int scrollX) {
	_scrollPosX = CLIP(scrollX, _minScrollX, _maxScrollX);
}

void Gfx::setScrollPosY(int scrollY) {
	_scrollPosY = CLIP(scrollY, _minScrollY, _maxScrollY);
}

void Gfx::initiateScroll(int deltaX, int deltaY) {
	if (deltaX != 0) {
		_requestedHScrollDir = deltaX > 0 ? 1 : -1;
		deltaX *= _requestedHScrollDir;
		_requestedHScrollSteps = ((deltaX+31)/32) / _requestedHScrollDir;
	}

	if (deltaY != 0) {
		_requestedVScrollDir = deltaY > 0 ? 1 : -1;
		deltaY *= _requestedVScrollDir;
		_requestedVScrollSteps = ((deltaY+7)/8) / _requestedVScrollDir;
	}
}

void Gfx::scroll() {
	int32 x = _scrollPosX, y = _scrollPosY;

	if (_requestedHScrollSteps) {
		x += 32*_requestedHScrollDir;	// scroll 32 pixels at a time
		_requestedHScrollSteps--;
	}

	if (_requestedVScrollSteps) {
		y += 8*_requestedVScrollDir;	// scroll 8 pixel at a time
		_requestedVScrollSteps--;
	}

	setScrollPosX(x);
	setScrollPosY(y);
}

void Gfx::beginFrame() {
	resetSceneDrawList();
	scroll();
}

void Gfx::updateScreen() {

	// the scene is calculated in game coordinates, so no translation
	// is needed
	_overlayMode = false;

	bool skipBackground = (_backgroundInfo->bg.getPixels() == 0);	// don't render frame if background is missing

	if (!skipBackground) {
		// background may not cover the whole screen, so adjust bulk update size
		uint w = _backgroundInfo->width;
		uint h = _backgroundInfo->height;
		byte *backgroundData = (byte *)_backgroundInfo->bg.getPixels();
		uint16 backgroundPitch = _backgroundInfo->bg.pitch;
		copyRectToScreen(backgroundData, backgroundPitch, _backgroundInfo->_x, _backgroundInfo->_y, w, h);
	}

	sortScene();
	Graphics::Surface *surf = lockScreen();
		// draws animations frames and other game items
		drawList(*surf, _sceneObjects);

		// special effects
		applyHalfbriteEffect_NS(*surf);

		// draws inventory, labels and dialogue items
		drawOverlay(*surf);
	unlockScreen();

	updateScreenIntern();
}

void Gfx::applyHalfbriteEffect_NS(Graphics::Surface &surf) {
	if (!_halfbrite) {
		return;
	}

	byte *buf = (byte *)surf.getPixels();
	for (int i = 0; i < surf.w*surf.h; i++) {
		*buf++ |= 0x20;
	}

	if (_nextProjectorPos) {
		int16 x = *_nextProjectorPos;
		int16 y = *(_nextProjectorPos + 1);
		if (x != -1 && y != -1) {
			_nextProjectorPos += 2;
			setProjectorPos(x, y);
		}
	}
	if (_hbCircleRadius > 0) {
		drawCircle(_hbCirclePos.x, _hbCirclePos.y, _hbCircleRadius, 0, &halfbritePixel, &surf);
	}
}

void Gfx::drawOverlay(Graphics::Surface &surf) {
	// the following items are handled in screen coordinates, so they need translation before
	// being drawn
	_overlayMode = true;

	drawInventory();

	updateFloatingLabel();

	drawList(surf, _items);
	drawList(surf, _balloons);
	drawList(surf, _labels);
}

//
//	graphic primitives
//


void Gfx::patchBackground(Graphics::Surface &surf, int16 x, int16 y, bool mask) {

	Common::Rect r(surf.w, surf.h);
	r.moveTo(x, y);

	uint16 z = (mask) ? _backgroundInfo->getMaskLayer(y) : LAYER_FOREGROUND;
	blt(r, (byte *)surf.getPixels(), &_backgroundInfo->bg, z, 100, 0);
}

void Gfx::fillBackground(const Common::Rect& r, byte color) {
	_backgroundInfo->bg.fillRect(r, color);
}

void Gfx::invertBackground(const Common::Rect& r) {

	byte *d = (byte *)_backgroundInfo->bg.getBasePtr(r.left, r.top);

	for (int i = 0; i < r.height(); i++) {
		for (int j = 0; j < r.width(); j++) {
			*d ^= 0x1F;
			d++;
		}

		d += (_backgroundInfo->bg.pitch - r.width());
	}

}





void setupLabelSurface(Graphics::Surface &surf, uint w, uint h) {
	surf.create(w, h, Graphics::PixelFormat::createFormatCLUT8());
	surf.fillRect(Common::Rect(w,h), LABEL_TRANSPARENT_COLOR);
}

GfxObj *Gfx::renderFloatingLabel(Font *font, char *text) {

	Graphics::Surface *cnv = new Graphics::Surface;

	uint w, h;
	if (_vm->getPlatform() == Common::kPlatformAmiga) {
		w = font->getStringWidth(text) + 16;
		h = font->height() + 2;

		setupLabelSurface(*cnv, w, h);

		font->setColor((_gameType == GType_BRA) ? 0 : 7);
		font->drawString((byte *)cnv->getBasePtr(1, 0), cnv->w, text);
		font->drawString((byte *)cnv->getBasePtr(1, 2), cnv->w, text);
		font->drawString((byte *)cnv->getBasePtr(0, 1), cnv->w, text);
		font->drawString((byte *)cnv->getBasePtr(2, 1), cnv->w, text);
		font->setColor((_gameType == GType_BRA) ? 11 : 1);
		font->drawString((byte *)cnv->getBasePtr(1, 1), cnv->w, text);
	} else {
		w = font->getStringWidth(text);
		h = font->height();

		setupLabelSurface(*cnv, w, h);

		drawText(font, cnv, 0, 0, text, 0);
	}

	GfxObj *obj = new GfxObj(kGfxObjTypeLabel, new SurfaceToFrames(cnv), "floatingLabel");
	obj->transparentKey = LABEL_TRANSPARENT_COLOR;
	obj->layer = LAYER_FOREGROUND;

	return obj;
}

void Gfx::showFloatingLabel(GfxObj *label) {
	hideFloatingLabel();

	if (label) {
		label->x = -1000;
		label->y = -1000;
		label->setFlags(kGfxObjVisible);

		_floatingLabel = label;
		_labels.push_back(label);
	}
}

void Gfx::hideFloatingLabel() {
	if (_floatingLabel != 0) {
		_floatingLabel->clearFlags(kGfxObjVisible);
	}
	_floatingLabel = 0;
}


void Gfx::updateFloatingLabel() {
	if (_floatingLabel == 0) {
		return;
	}

	struct FloatingLabelTraits {
		Common::Point _offsetWithItem;
		Common::Point _offsetWithoutItem;
		int	_minX;
		int _minY;
		int	_maxX;
		int _maxY;
	} *traits;

	Common::Rect r;
	_floatingLabel->getRect(0, r);

	FloatingLabelTraits traits_NS = {
		Common::Point(16 - r.width()/2, 34),
		Common::Point(8 - r.width()/2, 21),
		0, 0, _vm->_screenWidth - r.width(), 190
	};

	// FIXME: _maxY for BRA is not constant (390), but depends on _vm->_subtitleY
	FloatingLabelTraits traits_BR = {
		Common::Point(34 - r.width()/2, 70),
		Common::Point(16 - r.width()/2, 37),
		0, 0, _vm->_screenWidth - r.width(), 390
	};

	if (_gameType == GType_Nippon) {
		traits = &traits_NS;
	} else {
		traits = &traits_BR;
	}

	Common::Point	cursor;
	_vm->_input->getCursorPos(cursor);
	Common::Point offset = (_vm->_input->_activeItem._id) ? traits->_offsetWithItem : traits->_offsetWithoutItem;

	_floatingLabel->x = CLIP(cursor.x + offset.x, traits->_minX, traits->_maxX);
	_floatingLabel->y = CLIP(cursor.y + offset.y, traits->_minY, traits->_maxY);
}




GfxObj *Gfx::createLabel(Font *font, const char *text, byte color) {
	Graphics::Surface *cnv = new Graphics::Surface;

	uint w, h;

	if (_vm->getPlatform() == Common::kPlatformAmiga) {
		w = font->getStringWidth(text) + 2;
		h = font->height() + 2;

		setupLabelSurface(*cnv, w, h);

		drawText(font, cnv, 0, 2, text, 0);
		drawText(font, cnv, 2, 0, text, color);
	} else {
		w = font->getStringWidth(text);
		h = font->height();

		setupLabelSurface(*cnv, w, h);

		drawText(font, cnv, 0, 0, text, color);
	}

	GfxObj *obj = new GfxObj(kGfxObjTypeLabel, new SurfaceToFrames(cnv), "label");
	obj->transparentKey = LABEL_TRANSPARENT_COLOR;
	obj->layer = LAYER_FOREGROUND;

	return obj;
}

void Gfx::showLabel(GfxObj *label, int16 x, int16 y) {
	if (!label) {
		return;
	}

	label->setFlags(kGfxObjVisible);

	Common::Rect r;
	label->getRect(0, r);

	if (x == CENTER_LABEL_HORIZONTAL) {
		x = CLIP<int16>((_backgroundInfo->width - r.width()) / 2, 0, _backgroundInfo->width/2);
	}

	if (y == CENTER_LABEL_VERTICAL) {
		y = CLIP<int16>((_vm->_screenHeight - r.height()) / 2, 0, _vm->_screenHeight/2);
	}

	label->x = x;
	label->y = y;

	_labels.push_back(label);
}

void Gfx::hideLabel(GfxObj *label) {
	if (label) {
		label->clearFlags(kGfxObjVisible);
		unregisterLabel(label);
	}
}

void Gfx::freeLabels() {
	_labels.clear();
	_floatingLabel = 0;
}

void Gfx::unregisterLabel(GfxObj *label) {
	for (uint i = 0; i < _labels.size(); i++) {
		if (_labels[i] == label) {
			_labels.remove_at(i);
			break;
		}
	}
}


void Gfx::copyRect(const Common::Rect &r, Graphics::Surface &src, Graphics::Surface &dst) {

	byte *s = (byte *)src.getBasePtr(r.left, r.top);
	byte *d = (byte *)dst.getPixels();

	for (uint16 i = 0; i < r.height(); i++) {
		memcpy(d, s, r.width());

		s += src.pitch;
		d += dst.pitch;
	}

	return;
}

void Gfx::grabBackground(const Common::Rect& r, Graphics::Surface &dst) {
	copyRect(r, _backgroundInfo->bg, dst);
}


Gfx::Gfx(Parallaction* vm) :
	_vm(vm), _disk(vm->_disk), _backgroundInfo(0),
	_scrollPosX(0), _scrollPosY(0),_minScrollX(0), _maxScrollX(0),
	_minScrollY(0), _maxScrollY(0),
	_requestedHScrollSteps(0), _requestedVScrollSteps(0),
	_requestedHScrollDir(0), _requestedVScrollDir(0) {

	_gameType = _vm->getGameType();
	_doubleBuffering = _gameType != GType_Nippon;

	initGraphics(_vm->_screenWidth, _vm->_screenHeight, _gameType == GType_BRA);

	setPalette(_palette);

	_floatingLabel = 0;

	_backgroundInfo = 0;

	_halfbrite = false;
	_nextProjectorPos = 0;
	_hbCircleRadius = 0;

	_unpackedBitmap = new byte[MAXIMUM_UNPACKED_BITMAP_SIZE];
	assert(_unpackedBitmap);

	if ((_gameType == GType_BRA) && (_vm->getPlatform() == Common::kPlatformDOS)) {
	// this loads the backup palette needed by the PC version of BRA (see setBackground()).
		BackgroundInfo	paletteInfo;
		_disk->loadSlide(paletteInfo, "pointer");
		_backupPal.clone(paletteInfo.palette);
	}

	resetSceneDrawList();

	return;
}

Gfx::~Gfx() {

	_backBuffer.free();

	delete _backgroundInfo;

	freeLabels();

	delete[] _unpackedBitmap;

	return;
}



int Gfx::setItem(GfxObj* frames, uint16 x, uint16 y, byte transparentColor) {
	int id = _items.size();

	frames->x = x;
	frames->y = y;
	frames->transparentKey = transparentColor;
	frames->layer = LAYER_FOREGROUND;
	frames->setFlags(kGfxObjVisible);

	_items.insert_at(id, frames);

	setItemFrame(id, 0);

	return id;
}

void Gfx::setItemFrame(uint item, uint16 f) {
	_items[item]->frame = f;
}


GfxObj* Gfx::registerBalloon(Frames *frames, const char *text) {

	GfxObj *obj = new GfxObj(kGfxObjTypeBalloon, frames, text);

	obj->layer = LAYER_FOREGROUND;
	obj->frame = 0;
	obj->setFlags(kGfxObjVisible);

	_balloons.push_back(obj);

	return obj;
}

void Gfx::freeDialogueObjects() {
	_items.clear();

	_vm->_balloonMan->reset();

	for (uint i = 0; i < _balloons.size(); i++) {
		delete _balloons[i];
	}
	_balloons.clear();
}

void Gfx::setBackground(uint type, BackgroundInfo *info) {
	if (!info) {
		warning("Gfx::setBackground() called with an null BackgroundInfo");
		return;
	}

	_hbCircleRadius = 0;
	_nextProjectorPos = 0;

	delete _backgroundInfo;
	_backgroundInfo = info;

	if (type == kBackgroundLocation) {
		// The PC version of BRA needs the entries 20-31 of the palette to be constant, but
		// the background resource files are screwed up. The right colors come from an unused
		// bitmap (pointer.bmp). Nothing is known about the Amiga version so far.
		if ((_gameType == GType_BRA) && (_vm->getPlatform() == Common::kPlatformDOS)) {
			int r, g, b;
			for (uint i = 16; i < 32; i++) {
				_backupPal.getEntry(i, r, g, b);
				_backgroundInfo->palette.setEntry(i, r, g, b);
			}
		}

		setPalette(_backgroundInfo->palette);
		_palette.clone(_backgroundInfo->palette);
	} else {
		for (uint i = 0; i < 6; i++)
			_backgroundInfo->ranges[i]._flags = 0;	// disable palette cycling for slides
		setPalette(_backgroundInfo->palette);
	}

	_backgroundInfo->finalizeMask();
	_backgroundInfo->finalizePath();

	if (_gameType == GType_BRA) {
		int width = CLIP(info->width, (int)_vm->_screenWidth, info->width);
		int height = CLIP(info->height, (int)_vm->_screenHeight, info->height);

		if (width != _backBuffer.w || height != _backBuffer.h) {
			_backBuffer.create(width, height, Graphics::PixelFormat::createFormatCLUT8());
		}
	}

	_minScrollX = 0;
	_maxScrollX = MAX<int>(0, _backgroundInfo->width - _vm->_screenWidth);
	_minScrollY = 0;
	_maxScrollY = MAX<int>(0, _backgroundInfo->height - _vm->_screenHeight);
}


BackgroundInfo::BackgroundInfo() : _x(0), _y(0), width(0), height(0), _mask(0), _path(0) {
	layers[0] = layers[1] = layers[2] = layers[3] = 0;
	memset(ranges, 0, sizeof(ranges));
}

BackgroundInfo::~BackgroundInfo() {
	bg.free();
	clearMaskData();
	clearPathData();
}

bool BackgroundInfo::hasMask() {
	return _mask != 0;
}

void BackgroundInfo::clearMaskData() {
	// free mask data
	MaskPatches::iterator it = _maskPatches.begin();
	for ( ; it != _maskPatches.end(); ++it) {
		delete *it;
	}
	_maskPatches.clear();
	delete _mask;
	_mask = 0;
	_maskBackup.free();
}

void BackgroundInfo::finalizeMask() {
	if (_mask) {
		if (_maskPatches.size() > 0) {
			// since no more patches can be added after finalization,
			// avoid creating the backup if there is none
			_maskBackup.clone(*_mask);
		}
	} else {
		clearMaskData();
	}
}

uint BackgroundInfo::addMaskPatch(MaskBuffer *patch) {
	uint id = _maskPatches.size();
	_maskPatches.push_back(patch);
	return id;
}

void BackgroundInfo::toggleMaskPatch(uint id, int x, int y, bool apply) {
	if (!hasMask()) {
		return;
	}
	if (id >= _maskPatches.size()) {
		return;
	}
	MaskBuffer *patch = _maskPatches[id];
	if (apply) {
		_mask->bltOr(x, y, *patch, 0, 0, patch->w, patch->h);
	} else {
		_mask->bltCopy(x, y, _maskBackup, x, y, patch->w, patch->h);
	}
}

uint16 BackgroundInfo::getMaskLayer(uint16 z) const {
	for (uint16 i = 0; i < 3; i++) {
		if (layers[i+1] > z) return i;
	}
	return LAYER_FOREGROUND;
}

void BackgroundInfo::setPaletteRange(int index, const PaletteFxRange& range) {
	assert(index < 6);
	memcpy(&ranges[index], &range, sizeof(PaletteFxRange));
}

bool BackgroundInfo::hasPath() {
	return _path != 0;
}

void BackgroundInfo::clearPathData() {
	// free mask data
	PathPatches::iterator it = _pathPatches.begin();
	for ( ; it != _pathPatches.end(); ++it) {
		delete *it;
	}
	_pathPatches.clear();
	delete _path;
	_path = 0;
	_pathBackup.free();
}

void BackgroundInfo::finalizePath() {
	if (_path) {
		if (_pathPatches.size() > 0) {
			// since no more patches can be added after finalization,
			// avoid creating the backup if there is none
			_pathBackup.clone(*_path);
		}
	} else {
		clearPathData();
	}
}

uint BackgroundInfo::addPathPatch(PathBuffer *patch) {
	uint id = _pathPatches.size();
	_pathPatches.push_back(patch);
	return id;
}

void BackgroundInfo::togglePathPatch(uint id, int x, int y, bool apply) {
	if (!hasPath()) {
		return;
	}
	if (id >= _pathPatches.size()) {
		return;
	}
	PathBuffer *patch = _pathPatches[id];
	if (apply) {
		_path->bltCopy(x, y, *patch, 0, 0, patch->w, patch->h);
	} else {
		_path->bltCopy(x, y, _pathBackup, x, y, patch->w, patch->h);
	}
}

MaskBuffer::MaskBuffer() : w(0), internalWidth(0), h(0), size(0), data(0), bigEndian(true) {
}

MaskBuffer::~MaskBuffer() {
	free();
}

byte* MaskBuffer::getPtr(uint16 x, uint16 y) const {
	return data + (x >> 2) + y * internalWidth;
}

void MaskBuffer::clone(const MaskBuffer &buf) {
	if (!buf.data)
		return;

	create(buf.w, buf.h);
	bigEndian = buf.bigEndian;
	memcpy(data, buf.data, size);
}

void MaskBuffer::create(uint16 width, uint16 height) {
	free();

	w = width;
	internalWidth = w >> 2;
	h = height;
	size = (internalWidth * h);
	data = (byte *)calloc(size, 1);
}

void MaskBuffer::free() {
	::free(data);
	data = 0;
	w = 0;
	h = 0;
	internalWidth = 0;
	size = 0;
}

byte MaskBuffer::getValue(uint16 x, uint16 y) const {
	byte m = data[(x >> 2) + y * internalWidth];
	uint n;
	if (bigEndian) {
		n = (x & 3) << 1;
	} else {
		n = (3 - (x & 3)) << 1;
	}
	return (m >> n) & 3;
}

void MaskBuffer::bltOr(uint16 dx, uint16 dy, const MaskBuffer &src, uint16 sx, uint16 sy, uint width, uint height) {
	assert((width <= w) && (width <= src.w) && (height <= h) && (height <= src.h));

	byte *s = src.getPtr(sx, sy);
	byte *d = getPtr(dx, dy);

	// this code assumes buffers are aligned on 4-pixels boundaries, as the original does
	uint16 linewidth = width >> 2;
	for (uint16 i = 0; i < height; i++) {
		for (uint16 j = 0; j < linewidth; j++) {
			*d++ |= *s++;
		}
		d += internalWidth - linewidth;
		s += src.internalWidth - linewidth;
	}
}

void MaskBuffer::bltCopy(uint16 dx, uint16 dy, const MaskBuffer &src, uint16 sx, uint16 sy, uint width, uint height) {
	assert((width <= w) && (width <= src.w) && (height <= h) && (height <= src.h));

	byte *s = src.getPtr(sx, sy);
	byte *d = getPtr(dx, dy);

	// this code assumes buffers are aligned on 4-pixels boundaries, as the original does
	for (uint16 i = 0; i < height; i++) {
		memcpy(d, s, (width >> 2));
		d += internalWidth;
		s += src.internalWidth;
	}
}



PathBuffer::PathBuffer() : w(0), internalWidth(0), h(0), size(0), data(0), bigEndian(true) {
}

PathBuffer::~PathBuffer() {
	free();
}

void PathBuffer::clone(const PathBuffer &buf) {
	if (!buf.data)
		return;

	create(buf.w, buf.h);
	bigEndian = buf.bigEndian;
	memcpy(data, buf.data, size);
}

void PathBuffer::create(uint16 width, uint16 height) {
	free();

	w = width;
	internalWidth = w >> 3;
	h = height;
	size = (internalWidth * h);
	data = (byte *)calloc(size, 1);
}

void PathBuffer::free() {
	::free(data);
	data = 0;
	w = 0;
	h = 0;
	internalWidth = 0;
	size = 0;
}

byte PathBuffer::getValue(uint16 x, uint16 y) const {
	byte m = 0;
	if (data) {
		uint index = (x >> 3) + y * internalWidth;
		if (index < size)
			m = data[index];
		else
			warning("PathBuffer::getValue(x: %d, y: %d) outside of data buffer of size %d", x, y, size);
	} else
		warning("PathBuffer::getValue() attempted to use NULL data buffer");
	uint bit = bigEndian ? (x & 7) : (7 - (x & 7));
	return ((1 << bit) & m) >> bit;
}

byte* PathBuffer::getPtr(uint16 x, uint16 y) const {
	return data + (x >> 3) + y * internalWidth;
}

void PathBuffer::bltCopy(uint16 dx, uint16 dy, const PathBuffer &src, uint16 sx, uint16 sy, uint width, uint height) {
	assert((width <= w) && (width <= src.w) && (height <= h) && (height <= src.h));

	byte *s = src.getPtr(sx, sy);
	byte *d = getPtr(dx, dy);

	// this code assumes buffers are aligned on 4-pixels boundaries, as the original does
	for (uint16 i = 0; i < height; i++) {
		memcpy(d, s, (width >> 3));
		d += internalWidth;
		s += src.internalWidth;
	}
}

} // namespace Parallaction
