/* 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 "mohawk/resource.h"
#include "mohawk/riven.h"
#include "mohawk/riven_card.h"
#include "mohawk/riven_graphics.h"
#include "mohawk/riven_sound.h"
#include "mohawk/riven_stack.h"
#include "mohawk/riven_video.h"

#include "common/system.h"
#include "engines/util.h"
#include "graphics/colormasks.h"

namespace Mohawk {

class TransitionEffect {
public:
	TransitionEffect(OSystem *system, Graphics::Surface *mainScreen, Graphics::Surface *effectScreen,
		                 RivenTransition type, uint duration, const Common::Rect &rect) :
			_system(system),
			_mainScreen(mainScreen),
			_effectScreen(effectScreen),
			_type(type),
			_duration(duration),
			_timeBased(false),
			_rect(rect) {
	}

	virtual ~TransitionEffect() {}

	bool isTimeBased() const { return _timeBased; }

	virtual bool drawFrame(uint32 elapsed) = 0;

protected:
	Common::Rect makeDirectionalInitalArea() const {
		Common::Rect initialArea = _rect;
		switch (_type) {
			case kRivenTransitionWipeLeft:
			case kRivenTransitionPanLeft:
				initialArea.left = _rect.right;
				break;
			case kRivenTransitionWipeRight:
			case kRivenTransitionPanRight:
				initialArea.right = _rect.left;
				break;
			case kRivenTransitionWipeUp:
			case kRivenTransitionPanUp:
				initialArea.top = _rect.bottom;
				break;
			case kRivenTransitionWipeDown:
			case kRivenTransitionPanDown:
				initialArea.bottom = _rect.top;
				break;
			default:
				error("Unhandled transition type: %d", _type);
		}

		return initialArea;
	}

	OSystem *_system;

	RivenTransition _type;
	uint _duration;
	Common::Rect _rect;
	bool _timeBased;

	Graphics::Surface *_mainScreen;
	Graphics::Surface *_effectScreen;
};

class TransitionEffectWipe : public TransitionEffect {
public:
	TransitionEffectWipe(OSystem *system, Graphics::Surface *mainScreen, Graphics::Surface *effectScreen,
		                     RivenTransition type, uint duration, const Common::Rect &rect) :
			TransitionEffect(system, mainScreen, effectScreen, type, duration, rect) {

		_timeBased = true;
		_lastCopyArea = makeDirectionalInitalArea();
	}

	virtual bool drawFrame(uint32 elapsed) override {
		Common::Rect copyArea;
		switch (_type) {
			case kRivenTransitionWipeLeft:
				copyArea.top = _lastCopyArea.top;
				copyArea.bottom = _lastCopyArea.bottom;
				copyArea.right = _lastCopyArea.left;
				copyArea.left = _rect.width() - elapsed * _rect.width() / _duration;
				break;
			case kRivenTransitionWipeRight:
				copyArea.top = _lastCopyArea.top;
				copyArea.bottom = _lastCopyArea.bottom;
				copyArea.left = _lastCopyArea.right;
				copyArea.right = elapsed * _rect.width() / _duration;
				break;
			case kRivenTransitionWipeUp:
				copyArea.left = _lastCopyArea.left;
				copyArea.right = _lastCopyArea.right;
				copyArea.bottom = _lastCopyArea.top;
				copyArea.top = _rect.height() - elapsed * _rect.height() / _duration;
				break;
			case kRivenTransitionWipeDown:
				copyArea.left = _lastCopyArea.left;
				copyArea.right = _lastCopyArea.right;
				copyArea.top = _lastCopyArea.bottom;
				copyArea.bottom = elapsed * _rect.height() / _duration;
				break;
			default:
				error("Unhandled transition type: %d", _type);
		}

		_lastCopyArea = copyArea;

		if (copyArea.isEmpty()) {
			// Nothing to draw
			return false;
		}

		_effectScreen->copyRectToSurface(*_mainScreen, copyArea.left, copyArea.top, copyArea);
		_system->copyRectToScreen(_effectScreen->getBasePtr(copyArea.left, copyArea.top), _effectScreen->pitch,
		                          copyArea.left, copyArea.top, copyArea.width(), copyArea.height());

		return false;
	}

private:
	Common::Rect _lastCopyArea;
};

class TransitionEffectPan : public TransitionEffect {
public:
	TransitionEffectPan(OSystem *system, Graphics::Surface *mainScreen, Graphics::Surface *effectScreen,
	                    RivenTransition type, uint duration, const Common::Rect &rect, int16 offset) :
			TransitionEffect(system, mainScreen, effectScreen, type, duration, rect) {

		_timeBased = true;
		_offset = offset;
		_initialArea = makeDirectionalInitalArea();
		 complete = false;
	}

	virtual bool drawFrame(uint32 elapsed) override {
		Common::Rect newArea;
		switch (_type) {
			case kRivenTransitionPanLeft:
				newArea.top = _initialArea.top;
				newArea.bottom = _initialArea.bottom;
				newArea.right = _initialArea.right;
				newArea.left = _rect.width() - elapsed * _rect.width() / _duration;
				break;
			case kRivenTransitionPanRight:
				newArea.top = _initialArea.top;
				newArea.bottom = _initialArea.bottom;
				newArea.left = _initialArea.left;
				newArea.right = elapsed * _rect.width() / _duration;
				break;
			case kRivenTransitionPanUp:
				newArea.left = _initialArea.left;
				newArea.right = _initialArea.right;
				newArea.bottom = _initialArea.bottom;
				newArea.top = _rect.height() - elapsed * _rect.height() / _duration;
				break;
			case kRivenTransitionPanDown:
				newArea.left = _initialArea.left;
				newArea.right = _initialArea.right;
				newArea.top = _initialArea.top;
				newArea.bottom = elapsed * _rect.height() / _duration;
				break;
			default:
				error("Unhandled transition type: %d", _type);
		}

		if (newArea.isEmpty()) {
			// Nothing to draw
			return false;
		}

		Common::Rect oldArea;
		if (newArea != _rect) {
			oldArea = Common::Rect(
					newArea.right != _rect.right ? _rect.left + newArea.width() : _rect.left,
					newArea.bottom != _rect.bottom ? _rect.top + newArea.height() : _rect.top,
					newArea.left != _rect.left ? _rect.right - newArea.width() : _rect.right,
					newArea.top != _rect.top ? _rect.bottom - newArea.height() : _rect.bottom
			);
		}

		int oldX = newArea.left != _rect.left ? _rect.left + newArea.width() : _rect.left;
		int oldY = newArea.top != _rect.top ? _rect.top + newArea.height() : _rect.top;

		int newX = newArea.right != _rect.right ? _rect.left + oldArea.width() : _rect.left;
		int newY = newArea.bottom != _rect.bottom ? _rect.top + oldArea.height() : _rect.top;

		if (_offset != -1) {
			if (_type == kRivenTransitionPanDown && oldArea.height() - _offset > 0) {
				newY -= _offset;
			} else if (_type == kRivenTransitionPanUp && newArea.height() + _offset < _rect.height()) {
				newY += _offset;
			} else if (_type == kRivenTransitionPanRight && oldArea.width() - _offset > 0) {
				newX -= _offset;
			} else if (_type == kRivenTransitionPanLeft && newArea.width() + _offset < _rect.width()) {
				newX += _offset;
			} else {
				newX = 0;
				newY = 0;
				newArea = _rect;
				oldArea = Common::Rect();
			}
		}

		if (!oldArea.isEmpty()) {
			_system->copyRectToScreen(_effectScreen->getBasePtr(oldX, oldY), _effectScreen->pitch,
			                          oldArea.left, oldArea.top, oldArea.width(), oldArea.height());
		}

		if (!newArea.isEmpty()) {
			_system->copyRectToScreen(_mainScreen->getBasePtr(newX, newY), _mainScreen->pitch,
			                          newArea.left, newArea.top, newArea.width(), newArea.height());
		}

		if (newArea == _rect) {
			_effectScreen->copyRectToSurface(*_mainScreen, _rect.left, _rect.top, _rect);
			return true; // The transition is complete
		} else {
			return false;
		}
	}

private:
	Common::Rect _initialArea;
	int16 _offset;
	bool complete;
};

class TransitionEffectBlend : public TransitionEffect {
public:
	TransitionEffectBlend(OSystem *system, Graphics::Surface *mainScreen, Graphics::Surface *effectScreen,
	                    RivenTransition type, uint duration, const Common::Rect &rect) :
			TransitionEffect(system, mainScreen, effectScreen, type, duration, rect) {

		_timeBased = false;
	}

	virtual bool drawFrame(uint32 elapsed) override {
		assert(_effectScreen->format == _mainScreen->format);
		assert(_effectScreen->format == _system->getScreenFormat());

		if (elapsed == _duration) {
			_effectScreen->copyRectToSurface(*_mainScreen, 0, 0, Common::Rect(_mainScreen->w, _mainScreen->h));
			_system->copyRectToScreen(_effectScreen->getBasePtr(0, 0), _effectScreen->pitch, 0, 0, _effectScreen->w, _effectScreen->h);
			return true; // The transition is complete
		} else {
			Graphics::Surface *screen = _system->lockScreen();

			uint alpha = elapsed * 255 / _duration;
			for (uint y = 0; y < _mainScreen->h; y++) {
				uint16 *src1 = (uint16 *) _mainScreen->getBasePtr(0, y);
				uint16 *src2 = (uint16 *) _effectScreen->getBasePtr(0, y);
				uint16 *dst = (uint16 *) screen->getBasePtr(0, y);
				for (uint x = 0; x < _mainScreen->w; x++) {
					uint8 r1, g1, b1, r2, g2, b2;
					Graphics::colorToRGB< Graphics::ColorMasks<565> >(*src1++, r1, g1, b1);
					Graphics::colorToRGB< Graphics::ColorMasks<565> >(*src2++, r2, g2, b2);

					uint r = r1 * alpha + r2 * (255 - alpha);
					uint g = g1 * alpha + g2 * (255 - alpha);
					uint b = b1 * alpha + b2 * (255 - alpha);

					r /= 255;
					g /= 255;
					b /= 255;

					*dst++ = (uint16) Graphics::RGBToColor< Graphics::ColorMasks<565> >(r, g, b);
				}
			}

			_system->unlockScreen();
			return false;
		}
	}
};

RivenGraphics::RivenGraphics(MohawkEngine_Riven* vm) : GraphicsManager(), _vm(vm) {
	_bitmapDecoder = new MohawkBitmap();

	// Restrict ourselves to a single pixel format to simplify the effects implementation
	_pixelFormat = Graphics::createPixelFormat<565>();
	initGraphics(608, 436, &_pixelFormat);

	// The actual game graphics only take up the first 392 rows. The inventory
	// occupies the rest of the screen and we don't use the buffer to hold that.
	_mainScreen = new Graphics::Surface();
	_mainScreen->create(608, 392, _pixelFormat);

	_effectScreen = new Graphics::Surface();
	_effectScreen->create(608, 392, _pixelFormat);

	_screenUpdateNesting = 0;
	_screenUpdateRunning = false;
	_enableCardUpdateScript = true;
	_scheduledTransition = kRivenTransitionNone;
	_dirtyScreen = false;

	_creditsImage = 302;
	_creditsPos = 0;

	_transitionMode = kRivenTransitionModeFastest;
	_transitionOffset = -1;
	_waterEffect = nullptr;
	_fliesEffect = nullptr;
}

RivenGraphics::~RivenGraphics() {
	_effectScreen->free();
	delete _effectScreen;
	_mainScreen->free();
	delete _mainScreen;
	delete _bitmapDecoder;
	delete _fliesEffect;
}

MohawkSurface *RivenGraphics::decodeImage(uint16 id) {
	MohawkSurface *surface = _bitmapDecoder->decodeImage(_vm->getResource(ID_TBMP, id));
	surface->convertToTrueColor();
	return surface;
}

void RivenGraphics::copyImageToScreen(uint16 image, uint32 left, uint32 top, uint32 right, uint32 bottom) {
	Graphics::Surface *surface = findImage(image)->getSurface();

	beginScreenUpdate();

	// Clip the width to fit on the screen. Fixes some images.
	if (left + surface->w > 608)
		surface->w = 608 - left;

	for (uint16 i = 0; i < surface->h; i++)
		memcpy(_mainScreen->getBasePtr(left, i + top), surface->getBasePtr(0, i), surface->w * surface->format.bytesPerPixel);

	_dirtyScreen = true;
	applyScreenUpdate();
}

void RivenGraphics::updateScreen() {
	if (_dirtyScreen) {
		// Copy to screen if there's no transition. Otherwise transition.
		if (_scheduledTransition == kRivenTransitionNone
		    || _transitionMode == kRivenTransitionModeDisabled) {
			const Common::Rect updateRect = Common::Rect(0, 0, 608, 392);

			// mainScreen -> effectScreen -> systemScreen
			_effectScreen->copyRectToSurface(*_mainScreen, updateRect.left, updateRect.top, updateRect);
			_vm->_system->copyRectToScreen(_effectScreen->getBasePtr(updateRect.left, updateRect.top), _effectScreen->pitch, updateRect.left, updateRect.top, updateRect.width(), updateRect.height());

			_scheduledTransition = kRivenTransitionNone;
		} else {
			runScheduledTransition();
		}

		_dirtyScreen = false;
	}
}

void RivenGraphics::scheduleWaterEffect(uint16 sfxeID) {
	clearWaterEffect();

	_waterEffect = new WaterEffect(_vm, sfxeID);
}

void RivenGraphics::clearWaterEffect() {
	delete _waterEffect;
	_waterEffect = nullptr;
}

WaterEffect::WaterEffect(MohawkEngine_Riven *vm, uint16 sfxeID) :
		_vm(vm) {
	Common::SeekableReadStream *sfxeStream = _vm->getResource(ID_SFXE, sfxeID);

	if (sfxeStream->readUint16BE() != 'SL')
		error ("Unknown sfxe tag");

	// Read in header info
	uint16 frameCount = sfxeStream->readUint16BE();
	uint32 offsetTablePosition = sfxeStream->readUint32BE();
	_rect.left = sfxeStream->readUint16BE();
	_rect.top = sfxeStream->readUint16BE();
	_rect.right = sfxeStream->readUint16BE();
	_rect.bottom = sfxeStream->readUint16BE();
	_speed = sfxeStream->readUint16BE();
	// Skip the rest of the fields...

	// Read in offsets
	sfxeStream->seek(offsetTablePosition);
	Common::Array<uint32> frameOffsets;
	frameOffsets.resize(frameCount);
	for (uint16 i = 0; i < frameCount; i++)
		frameOffsets[i] = sfxeStream->readUint32BE();

	// Read in the scripts
	sfxeStream->seek(frameOffsets[0]);
	for (uint16 i = 0; i < frameCount; i++) {
		uint scriptLength = (i == frameCount - 1) ? sfxeStream->size() - frameOffsets[i] : frameOffsets[i + 1] - frameOffsets[i];
		_frameScripts.push_back(sfxeStream->readStream(scriptLength));
	}

	// Set it to the first frame
	_curFrame = 0;
	_lastFrameTime = 0;

	delete sfxeStream;
}

void WaterEffect::update() {
	if (_vm->_system->getMillis() <= _lastFrameTime + 1000 / _speed) {
		return; // Nothing to do yet
	}

	// Make sure the script is at the starting point
	Common::SeekableReadStream *script = _frameScripts[_curFrame];
	script->seek(0);

	Graphics::Surface *screen = _vm->_system->lockScreen();
	Graphics::Surface *mainScreen = _vm->_gfx->getBackScreen();
	assert(screen->format == mainScreen->format);

	// Run script
	uint16 curRow = 0;
	for (uint16 op = script->readUint16BE(); op != 4; op = script->readUint16BE()) {
		if (op == 1) {        // Increment Row
			curRow++;
		} else if (op == 3) { // Copy Pixels
			uint16 dstLeft = script->readUint16BE();
			uint16 srcLeft = script->readUint16BE();
			uint16 srcTop = script->readUint16BE();
			uint16 rowWidth = script->readUint16BE();

			byte *src = (byte *)mainScreen->getBasePtr(srcLeft, srcTop);
			byte *dst = (byte *)screen->getBasePtr(dstLeft, curRow + _rect.top);

			memcpy(dst, src, rowWidth * screen->format.bytesPerPixel);
		} else if (op != 4) { // End of Script
			error ("Unknown SFXE opcode %d", op);
		}
	}

	_vm->_system->unlockScreen();

	// Increment frame
	_curFrame++;
	if (_curFrame == _frameScripts.size())
		_curFrame = 0;

	// Set the new time
	_lastFrameTime = _vm->_system->getMillis();
}

WaterEffect::~WaterEffect() {
	for (uint i = 0; i < _frameScripts.size(); i++) {
		delete _frameScripts[i];
	}
}

void RivenGraphics::setTransitionMode(RivenTransitionMode mode) {
	_transitionMode = mode;
	switch (_transitionMode) {
		case kRivenTransitionModeFastest:
			_transitionFrames   = 8;
			_transitionDuration = 300;
			break;
		case kRivenTransitionModeNormal:
			_transitionFrames   = 16;
			_transitionDuration = 500;
			break;
		case kRivenTransitionModeBest:
			_transitionFrames   = 32;
			_transitionDuration = 700;
			break;
		case kRivenTransitionModeDisabled:
			_transitionFrames   = 0;
			_transitionDuration = 0;
			break;
		default:
			error("Unknown transition mode %d", _transitionMode);
	}
}

void RivenGraphics::scheduleTransition(RivenTransition id, const Common::Rect &rect) {
	_scheduledTransition = id;
	_transitionRect = rect;

	RivenHotspot *hotspot = _vm->getCard()->getCurHotspot();
	if (hotspot) {
		_transitionOffset = hotspot->getTransitionOffset();
	} else {
		_transitionOffset = -1;
	}
}

void RivenGraphics::runScheduledTransition() {
	if (_scheduledTransition == kRivenTransitionNone)
		return;

	// Note: Transitions 0-11 are actual transitions, but none are used in-game.
	// There's no point in implementing them if they're not used. These extra
	// transitions were found by hacking scripts.

	TransitionEffect *effect = nullptr;
	switch (_scheduledTransition) {
		case kRivenTransitionWipeLeft:
		case kRivenTransitionWipeRight:
		case kRivenTransitionWipeUp:
		case kRivenTransitionWipeDown: {
			effect = new TransitionEffectWipe(_vm->_system, _mainScreen, _effectScreen,
			                                  _scheduledTransition, _transitionDuration, _transitionRect);
			break;
		}
		case kRivenTransitionPanLeft:
		case kRivenTransitionPanRight:
		case kRivenTransitionPanUp:
		case kRivenTransitionPanDown: {
			effect = new TransitionEffectPan(_vm->_system, _mainScreen, _effectScreen,
			                                 _scheduledTransition, _transitionDuration, _transitionRect, _transitionOffset);
			break;
		}
		case kRivenTransitionBlend:
		case kRivenTransitionBlend2: // (tspit CARD 155)
			effect = new TransitionEffectBlend(_vm->_system, _mainScreen, _effectScreen,
			                                   _scheduledTransition, _transitionFrames, _transitionRect);
			break;
		default:
			error("Unhandled transition type: %d", _scheduledTransition);
	}

	if (effect->isTimeBased()) {
		uint32 startTime = _vm->_system->getMillis();
		uint32 timeElapsed = 0;
		bool transitionComplete = false;
		while (timeElapsed < _transitionDuration && !transitionComplete && !_vm->hasGameEnded()) {
			transitionComplete = effect->drawFrame(timeElapsed);

			_vm->doFrame();
			timeElapsed = _vm->_system->getMillis() - startTime;
		}

		if (!transitionComplete) {
			effect->drawFrame(_transitionDuration);
		}
	} else {
		for (uint frame = 1; frame <= _transitionFrames && !_vm->hasGameEnded(); frame++) {
			effect->drawFrame(frame);

			_vm->doFrame();
		}
	}
	delete effect;

	_scheduledTransition = kRivenTransitionNone; // Clear scheduled transition
	_transitionOffset = -1;
}

void RivenGraphics::clearMainScreen() {
	_mainScreen->fillRect(Common::Rect(0, 0, 608, 392), _pixelFormat.RGBToColor(0, 0, 0));
}

void RivenGraphics::fadeToBlack() {
	// The transition speed is forced to best here
	setTransitionMode(kRivenTransitionModeBest);
	scheduleTransition(kRivenTransitionBlend);
	clearMainScreen();
	runScheduledTransition();
}

void RivenGraphics::drawExtrasImageToScreen(uint16 id, const Common::Rect &rect) {
	MohawkSurface *mhkSurface = _bitmapDecoder->decodeImage(_vm->getExtrasResource(ID_TBMP, id));
	mhkSurface->convertToTrueColor();
	Graphics::Surface *surface = mhkSurface->getSurface();

	_vm->_system->copyRectToScreen(surface->getPixels(), surface->pitch, rect.left, rect.top, surface->w, surface->h);

	delete mhkSurface;
}

void RivenGraphics::drawRect(const Common::Rect &rect, bool active) {
	// Useful with debugging. Shows where hotspots are on the screen and whether or not they're active.
	Graphics::Surface *screen = _vm->_system->lockScreen();

	if (active)
		screen->frameRect(rect, _pixelFormat.RGBToColor(0, 255, 0));
	else
		screen->frameRect(rect, _pixelFormat.RGBToColor(255, 0, 0));

	_vm->_system->unlockScreen();
}

void RivenGraphics::drawImageRect(uint16 id, const Common::Rect &srcRect, const Common::Rect &dstRect) {
	// Draw tBMP id from srcRect to dstRect
	Graphics::Surface *surface = findImage(id)->getSurface();

	assert(srcRect.width() == dstRect.width() && srcRect.height() == dstRect.height());

	for (uint16 i = 0; i < srcRect.height(); i++)
		memcpy(_mainScreen->getBasePtr(dstRect.left, i + dstRect.top), surface->getBasePtr(srcRect.left, i + srcRect.top), srcRect.width() * surface->format.bytesPerPixel);

	_dirtyScreen = true;
}

void RivenGraphics::drawExtrasImage(uint16 id, const Common::Rect &dstRect) {
	MohawkSurface *mhkSurface = _bitmapDecoder->decodeImage(_vm->getExtrasResource(ID_TBMP, id));
	mhkSurface->convertToTrueColor();
	Graphics::Surface *surface = mhkSurface->getSurface();

	assert(dstRect.width() == surface->w);

	for (uint16 i = 0; i < surface->h; i++)
		memcpy(_mainScreen->getBasePtr(dstRect.left, i + dstRect.top), surface->getBasePtr(0, i), surface->pitch);

	delete mhkSurface;
	_dirtyScreen = true;
}

void RivenGraphics::beginCredits() {
	// Clear the old cache
	clearCache();

	// Now cache all the credits images
	for (uint16 i = 302; i <= 320; i++) {
		MohawkSurface *surface = _bitmapDecoder->decodeImage(_vm->getExtrasResource(ID_TBMP, i));
		surface->convertToTrueColor();
		addImageToCache(i, surface);
	}

	// And clear our screen too
	clearMainScreen();
	_effectScreen->fillRect(Common::Rect(0, 0, 608, 392), _pixelFormat.RGBToColor(0, 0, 0));
}

void RivenGraphics::updateCredits() {
	if ((_creditsImage == 303 || _creditsImage == 304) && _creditsPos == 0)
		fadeToBlack();

	if (_creditsImage < 304) {
		// For the first two credit images, they are faded from black to the image and then out again
		scheduleTransition(kRivenTransitionBlend);

		Graphics::Surface *frame = findImage(_creditsImage++)->getSurface();

		for (int y = 0; y < frame->h; y++)
			memcpy(_mainScreen->getBasePtr(124, y), frame->getBasePtr(0, y), frame->pitch);

		runScheduledTransition();
	} else {
		// Otheriwse, we're scrolling
		// Move the screen up one row
		memmove(_mainScreen->getPixels(), _mainScreen->getBasePtr(0, 1), _mainScreen->pitch * (_mainScreen->h - 1));

		// Only update as long as we're not before the last frame
		// Otherwise, we're just moving up a row (which we already did)
		if (_creditsImage <= 320) {
			// Copy the next row to the bottom of the screen
			Graphics::Surface *frame = findImage(_creditsImage)->getSurface();
			memcpy(_mainScreen->getBasePtr(124, _mainScreen->h - 1), frame->getBasePtr(0, _creditsPos), frame->pitch);
			_creditsPos++;

			if (_creditsPos == _mainScreen->h) {
				_creditsImage++;
				_creditsPos = 0;
			}
		}

		// Now flush the new screen
		_vm->_system->copyRectToScreen(_mainScreen->getPixels(), _mainScreen->pitch, 0, 0, _mainScreen->w, _mainScreen->h);
	}
}

void RivenGraphics::beginScreenUpdate() {
	_screenUpdateNesting++;
}

void RivenGraphics::applyScreenUpdate(bool force) {
	if (force) {
		_screenUpdateNesting = 0;
	} else {
		_screenUpdateNesting--;
	}

	// The screen is only updated when the outermost screen update ends
	if (_screenUpdateNesting <= 0 && !_screenUpdateRunning) {
		_screenUpdateRunning = true;

		if (_enableCardUpdateScript) {
			_vm->getCard()->runScript(kCardUpdateScript);
		}
		_vm->_sound->triggerDrawSound();
		updateScreen();

		_screenUpdateNesting = 0;
		_screenUpdateRunning = false;
	}
}

void RivenGraphics::setFliesEffect(uint16 count, bool fireflies) {
	delete _fliesEffect;
	_fliesEffect = new FliesEffect(_vm, count, fireflies);
}

void RivenGraphics::clearFliesEffect() {
	delete _fliesEffect;
	_fliesEffect = nullptr;
}

Graphics::Surface *RivenGraphics::getBackScreen() {
	return _mainScreen;
}

Graphics::Surface *RivenGraphics::getEffectScreen() {
	return _effectScreen;
}

void RivenGraphics::updateEffects() {
	if (_waterEffect && _vm->_vars["waterenabled"] != 0) {
		_waterEffect->update();
	}

	if (_fliesEffect) {
		_fliesEffect->update();
	}
}

void RivenGraphics::copySystemRectToScreen(const Common::Rect &rect) {
	Graphics::Surface *screen = _vm->_system->lockScreen();
	_mainScreen->copyRectToSurface(*screen, rect.left, rect.top, rect);
	_effectScreen->copyRectToSurface(*screen, rect.left, rect.top, rect);
	_vm->_system->unlockScreen();
}

void RivenGraphics::enableCardUpdateScript(bool enable) {
	_enableCardUpdateScript = enable;
}

const FliesEffect::FliesEffectData FliesEffect::_firefliesParameters = {
		true,
		true,
		true,
		true,
		3.0,
		0.7,
		40,
		2.0,
		1.0,
		8447718,
		30,
		10
};

const FliesEffect::FliesEffectData FliesEffect::_fliesParameters = {
		false,
		false,
		false,
		true,
		8.0,
		3.0,
		80,
		3.0,
		1.0,
		661528,
		30,
		10
};

FliesEffect::FliesEffect(MohawkEngine_Riven *vm, uint16 count, bool fireflies) :
		_vm(vm) {

	_effectSurface = _vm->_gfx->getEffectScreen();
	_backSurface = _vm->_gfx->getBackScreen();
	_gameRect = Common::Rect(608, 392);

	if (fireflies) {
		_parameters = &_firefliesParameters;
	} else {
		_parameters = &_fliesParameters;
	}

	_updatePeriodMs = 66;
	_nextUpdateTime = _vm->_system->getMillis();

	initFlies(count);
}

FliesEffect::~FliesEffect() {

}

void FliesEffect::initFlies(uint16 count) {
	_fly.resize(count);
	for (uint16 i = 0; i < _fly.size(); i++) {
		initFlyRandomPosition(i);
	}
}

void FliesEffect::initFlyRandomPosition(uint index) {
	int posX = _vm->_rnd->getRandomNumber(_gameRect.right - 3);
	int posY = _vm->_rnd->getRandomNumber(_gameRect.bottom - 3);

	if (posY < 100) {
		posY = 100;
	}

	initFlyAtPosition(index, posX, posY, 15);
}

int FliesEffect::randomBetween(int min, int max) {
	return _vm->_rnd->getRandomNumber(max - min) + min;
}

void FliesEffect::initFlyAtPosition(uint index, int posX, int posY, int posZ) {
	FliesEffectEntry &fly = _fly[index];

	fly.posX = posX;
	fly.posXFloat = posX;
	fly.posY = posY;
	fly.posYFloat = posY;
	fly.posZ = posZ;
	fly.light = true;

	fly.framesTillLightSwitch = randomBetween(_parameters->minFramesLit, _parameters->minFramesLit + _parameters->maxLightDuration);

	fly.hasBlur = false;
	fly.directionAngleRad = randomBetween(0, 300) / 100.0f;
	fly.directionAngleRadZ = randomBetween(0, 300) / 100.0f;
	fly.speed = randomBetween(0, 100) / 100.0f;
}

void FliesEffect::update() {
	if (_nextUpdateTime <= _vm->_system->getMillis()) {
		_nextUpdateTime = _updatePeriodMs + _vm->_system->getMillis();

		updateFlies();
		draw();
		updateScreen();
	}
}

void FliesEffect::updateFlies() {
	for (uint i = 0; i < _fly.size(); i++) {
		updateFlyPosition(i);

		if (_fly[i].posX < 1 || _fly[i].posX > _gameRect.right - 4 || _fly[i].posY > _gameRect.bottom - 4) {
			initFlyRandomPosition(i);
		}

		if (_parameters->lightable) {
			_fly[i].framesTillLightSwitch--;

			if (_fly[i].framesTillLightSwitch <= 0) {
				_fly[i].light = !_fly[i].light;
				_fly[i].framesTillLightSwitch = randomBetween(_parameters->minFramesLit, _parameters->minFramesLit + _parameters->maxLightDuration);
				_fly[i].hasBlur = false;
			}
		}
	}
}

void FliesEffect::updateFlyPosition(uint index) {
	FliesEffectEntry &fly = _fly[index];

	if (fly.directionAngleRad > 2.0f * M_PI) {
		fly.directionAngleRad = fly.directionAngleRad - 2.0f * M_PI;
	}
	if (fly.directionAngleRad < 0.0f) {
		fly.directionAngleRad = fly.directionAngleRad + 2.0f * M_PI;
	}
	if (fly.directionAngleRadZ > 2.0f * M_PI) {
		fly.directionAngleRadZ = fly.directionAngleRadZ - 2.0f * M_PI;
	}
	if (fly.directionAngleRadZ < 0.0f) {
		fly.directionAngleRadZ = fly.directionAngleRadZ + 2.0f * M_PI;
	}
	fly.posXFloat += cos(fly.directionAngleRad) * fly.speed;
	fly.posYFloat += sin(fly.directionAngleRad) * fly.speed;
	fly.posX = fly.posXFloat;
	fly.posY = fly.posYFloat;
	selectAlphaMap(
			fly.posXFloat - fly.posX >= 0.5,
			fly.posYFloat - fly.posY >= 0.5,
			&fly.alphaMap,
			&fly.width,
			&fly.height);
	fly.posZFloat += cos(fly.directionAngleRadZ) * (fly.speed / 2.0f);
	fly.posZ = fly.posZFloat;
	if (_parameters->canBlur && fly.speed > _parameters->blurSpeedTreshold) {
		fly.hasBlur = true;
		float blurPosXFloat = cos(fly.directionAngleRad + M_PI) * _parameters->blurDistance + fly.posXFloat;
		float blurPosYFloat = sin(fly.directionAngleRad + M_PI) * _parameters->blurDistance + fly.posYFloat;

		fly.blurPosX = blurPosXFloat;
		fly.blurPosY = blurPosYFloat;
		selectAlphaMap(
				blurPosXFloat - fly.blurPosX >= 0.5,
				blurPosYFloat - fly.blurPosY >= 0.5,
				&fly.blurAlphaMap,
				&fly.blurWidth,
				&fly.blurHeight);
	}
	if (fly.posY >= 100) {
		int maxAngularSpeed = _parameters->maxAcceleration;
		if (fly.posZ > 15) {
			maxAngularSpeed /= 2;
		}
		int angularSpeed = randomBetween(-maxAngularSpeed, maxAngularSpeed);
		fly.directionAngleRad += angularSpeed / 100.0f;
	} else {
		// Make the flies go down if they are too high in the screen
		int angularSpeed = randomBetween(0, 50);
		if (fly.directionAngleRad >= M_PI / 2.0f && fly.directionAngleRad <= 3.0f * M_PI / 2.0f) {
			// Going down
			fly.directionAngleRad -= angularSpeed / 100.0f;
		} else {
			// Going up
			fly.directionAngleRad += angularSpeed / 100.0f;
		}
		if (fly.posY < 1) {
			initFlyRandomPosition(index);
		}
	}
	if (fly.posZ >= 0) {
		int distanceToScreenEdge;
		if (fly.posX / 10 >= (_gameRect.right - fly.posX) / 10) {
			distanceToScreenEdge = (_gameRect.right - fly.posX) / 10;
		} else {
			distanceToScreenEdge = fly.posX / 10;
		}
		if (distanceToScreenEdge > (_gameRect.bottom - fly.posY) / 10) {
			distanceToScreenEdge = (_gameRect.bottom - fly.posY) / 10;
		}
		if (distanceToScreenEdge > 30) {
			distanceToScreenEdge = 30;
		}
		if (fly.posZ <= distanceToScreenEdge) {
			fly.directionAngleRadZ += randomBetween(-_parameters->maxAcceleration, _parameters->maxAcceleration) / 100.0f;
		} else {
			fly.posZ = distanceToScreenEdge;
			fly.directionAngleRadZ += M_PI;
		}
	} else {
		fly.posZ = 0;
		fly.directionAngleRadZ += M_PI;
	}
	float minSpeed = _parameters->minSpeed - fly.posZ / 40.0f;
	float maxSpeed = _parameters->maxSpeed - fly.posZ / 20.0f;
	fly.speed += randomBetween(-_parameters->maxAcceleration, _parameters->maxAcceleration) / 100.0f;
	if (fly.speed > maxSpeed) {
		fly.speed -= randomBetween(0, 50) / 100.0f;
	}
	if (fly.speed < minSpeed) {
		fly.speed += randomBetween(0, 50) / 100.0f;
	}
}

void FliesEffect::selectAlphaMap(bool horGridOffset, bool vertGridoffset, const uint16 **alphaMap, uint *width, uint *height) {
	static const uint16 alpha1[12] = {
			 8, 16,  8,
			16, 32, 16,
			 8, 16,  8,
			 0,  0,  0
	};

	static const uint16 alpha2[12] = {
			4, 12, 12, 4,
			8, 24, 24, 8,
			4, 12, 12, 4
	};

	static const uint16 alpha3[12] = {
			 4,  8,  4,
			12, 24, 12,
			12, 24, 12,
			 4,  8,  4
	};

	static const uint16 alpha4[16] = {
			2,  6,  6, 2,
			6, 18, 18, 6,
			6, 18, 18, 6,
			2,  6,  6, 2
	};

	static const uint16 alpha5[12] = {
			4,  8, 4,
			8, 32, 8,
			4,  8, 4,
			0,  0, 0
	};

	static const uint16 alpha6[12] = {
			2,  6,  6, 2,
			4, 24, 24, 4,
			2,  6,  6, 2
	};

	static const uint16 alpha7[12] = {
			2,  4, 2,
			6, 24, 6,
			6, 24, 6,
			2,  4, 2
	};

	static const uint16 alpha8[16] = {
			1,  3,  3, 1,
			3, 18, 18, 3,
			3, 18, 18, 3,
			1,  3,  3, 1
	};

	struct AlphaMap {
		bool horizontalGridOffset;
		bool verticalGridOffset;
		bool isLarge;
		uint16 width;
		uint16 height;
		const uint16 *pixels;
	};

	static const AlphaMap alphaSelector[] = {
			{ true,  true,  true,  4, 4, alpha4 },
			{ true,  true,  false, 4, 4, alpha8 },
			{ true,  false, true,  4, 3, alpha2 },
			{ true,  false, false, 4, 3, alpha6 },
			{ false, true,  true,  3, 4, alpha3 },
			{ false, true,  false, 3, 4, alpha7 },
			{ false, false, true,  3, 3, alpha1 },
			{ false, false, false, 3, 3, alpha5 }
	};

	for (uint i = 0; i < ARRAYSIZE(alphaSelector); i++) {
		if (alphaSelector[i].horizontalGridOffset == horGridOffset
		    && alphaSelector[i].verticalGridOffset == vertGridoffset
		    && alphaSelector[i].isLarge == _parameters->isLarge) {
			*alphaMap = alphaSelector[i].pixels;
			*width = alphaSelector[i].width;
			*height = alphaSelector[i].height;
			return;
		}
	}

	error("Unknown flies alpha map case");
}

void FliesEffect::draw() {
	const Graphics::PixelFormat format = _effectSurface->format;

	for (uint i = 0; i < _fly.size(); i++) {
		FliesEffectEntry &fly = _fly[i];
		uint32 color = _parameters->color32;
		if (!fly.light) {
			color = _fliesParameters.color32;
		}

		bool hoveringBrightBackground = false;
		for (uint y = 0; y < fly.height; y++) {
			uint16 *pixel = (uint16 *) _effectSurface->getBasePtr(fly.posX, fly.posY + y);

			for (uint x = 0; x < fly.width; x++) {
				byte r, g, b;
				format.colorToRGB(*pixel, r, g, b);

				if (_parameters->unlightIfTooBright) {
					if (r >= 192 || g >= 192 || b >= 192) {
						hoveringBrightBackground = true;
					}
				}
				colorBlending(color, r, g, b, fly.alphaMap[fly.width * y + x] - fly.posZ);

				*pixel = format.RGBToColor(r, g, b);
				++pixel;
			}
		}

		Common::Rect drawRect = Common::Rect(fly.width, fly.height);
		drawRect.translate(fly.posX, fly.posY);
		addToScreenDirtyRects(drawRect);
		addToEffectsDirtyRects(drawRect);

		if (fly.hasBlur) {
			for (uint y = 0; y < fly.blurHeight; y++) {
				uint16 *pixel = (uint16 *) _effectSurface->getBasePtr(fly.blurPosX, fly.blurPosY + y);
				for (uint x = 0; x < fly.blurWidth; x++) {
					byte r, g, b;
					format.colorToRGB(*pixel, r, g, b);

					colorBlending(color, r, g, b, fly.blurAlphaMap[fly.blurWidth * y + x] - fly.posZ);

					*pixel = format.RGBToColor(r, g, b);
					++pixel;
				}
			}

			Common::Rect drawRect2 = Common::Rect(fly.blurWidth, fly.blurHeight);
			drawRect2.translate(fly.blurPosX, fly.blurPosY);
			addToScreenDirtyRects(drawRect2);
			addToEffectsDirtyRects(drawRect2);

			fly.hasBlur = false;
		}

		if (hoveringBrightBackground) {
			fly.hasBlur = false;
			if (_parameters->lightable) {
				fly.light = false;
				fly.framesTillLightSwitch = randomBetween(_parameters->minFramesLit, _parameters->minFramesLit + _parameters->maxLightDuration);
			}

			if (_vm->_rnd->getRandomBit()) {
				fly.directionAngleRad += M_PI / 2.0;
			} else {
				fly.directionAngleRad -= M_PI / 2.0;
			}
		}
	}
}

void FliesEffect::colorBlending(uint32 flyColor, byte &r, byte &g, byte &b, int alpha) {
	alpha = CLIP(alpha, 0, 32);
	byte flyR = (flyColor & 0x000000FF) >> 0;
	byte flyG = (flyColor & 0x0000FF00) >> 8;
	byte flyB = (flyColor & 0x00FF0000) >> 16;

	r = (32 * r + alpha * (flyR - r)) / 32;
	g = (32 * g + alpha * (flyG - g)) / 32;
	b = (32 * b + alpha * (flyB - b)) / 32;
}

void FliesEffect::updateScreen() {
	for (uint i = 0; i < _screenSurfaceDirtyRects.size(); i++) {
		const Common::Rect &rect = _screenSurfaceDirtyRects[i];
		_vm->_system->copyRectToScreen(_effectSurface->getBasePtr(rect.left, rect.top),
		                               _effectSurface->pitch, rect.left, rect.top,
		                               rect.width(), rect.height()
		);
	}
	_screenSurfaceDirtyRects.clear();

	restoreEffectsSurface();
}

void FliesEffect::addToScreenDirtyRects(const Common::Rect &rect) {
	for (uint i = 0; i < _screenSurfaceDirtyRects.size(); i++) {
		if (rect.intersects(_screenSurfaceDirtyRects[i])) {
			_screenSurfaceDirtyRects[i].extend(rect);
			return;
		}
	}

	_screenSurfaceDirtyRects.push_back(rect);
}

void FliesEffect::addToEffectsDirtyRects(const Common::Rect &rect) {
	for (uint i = 0; i < _effectsSurfaceDirtyRects.size(); i++) {
		if (rect.intersects(_effectsSurfaceDirtyRects[i])) {
			_effectsSurfaceDirtyRects[i].extend(rect);
			return;
		}
	}

	_effectsSurfaceDirtyRects.push_back(rect);
}

void FliesEffect::restoreEffectsSurface() {
	for (uint i = 0; i < _effectsSurfaceDirtyRects.size(); i++) {
		const Common::Rect &rect = _effectsSurfaceDirtyRects[i];
		_effectSurface->copyRectToSurface(*_backSurface, rect.left, rect.top, rect);
		addToScreenDirtyRects(rect);
	}

	_effectsSurfaceDirtyRects.clear();
}

} // End of namespace Mohawk
