/* 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 "audio/mixer.h"                 // for Audio::Mixer::kSFXSoundType
#include "common/config-manager.h"       // for ConfMan
#include "common/textconsole.h"          // for warning, error
#ifndef USE_RGB_COLOR
#include "common/translation.h"          // for _
#endif
#include "common/util.h"                 // for ARRAYSIZE
#include "common/system.h"               // for g_system
#include "engine.h"                      // for Engine, g_engine
#include "graphics/colormasks.h"         // for createPixelFormat
#include "graphics/palette.h"            // for PaletteManager
#include "graphics/transparent_surface.h" // for TransparentSurface
#include "sci/console.h"                 // for Console
#include "sci/engine/features.h"         // for GameFeatures
#include "sci/engine/state.h"            // for EngineState
#include "sci/engine/vm_types.h"         // for reg_t
#include "sci/event.h"                   // for SciEvent, EventManager, SCI_...
#include "sci/graphics/celobj32.h"       // for CelInfo32, ::kLowResX, ::kLo...
#include "sci/graphics/cursor32.h"       // for GfxCursor32
#include "sci/graphics/frameout.h"       // for GfxFrameout
#include "sci/graphics/helpers.h"        // for Color, Palette
#include "sci/graphics/palette32.h"      // for GfxPalette32
#include "sci/graphics/plane32.h"        // for Plane, PlanePictureCodes::kP...
#include "sci/graphics/screen_item32.h"  // for ScaleInfo, ScreenItem, Scale...
#include "sci/resource.h"                // for ResourceManager, ResourceId,...
#include "sci/sci.h"                     // for SciEngine, g_sci, getSciVersion
#include "sci/sound/audio32.h"           // for Audio32
#include "sci/video/seq_decoder.h"       // for SEQDecoder
#include "video/avi_decoder.h"           // for AVIDecoder
#include "video/coktel_decoder.h"        // for AdvancedVMDDecoder
#include "sci/graphics/video32.h"

namespace Graphics { struct Surface; }

namespace Sci {

bool VideoPlayer::open(const Common::String &fileName) {
	if (!_decoder->loadFile(fileName)) {
		warning("Failed to load %s", fileName.c_str());
		return false;
	}

#ifndef USE_RGB_COLOR
	// KQ7 2.00b videos are compressed in 24bpp Cinepak, so cannot play on a
	// system with no RGB support
	if (_decoder->getPixelFormat().bytesPerPixel != 1) {
		void showScummVMDialog(const Common::String &message);
		showScummVMDialog(Common::String::format(_("Cannot play back %dbpp video on a system with maximum color depth of 8bpp"), _decoder->getPixelFormat().bpp()));
		_decoder->close();
		return false;
	}
#endif

	return true;
}

bool VideoPlayer::startHQVideo() {
#ifdef USE_RGB_COLOR
	// Optimize rendering performance for unscaled videos, and allow
	// better-than-NN interpolation for videos that are scaled
	if (shouldStartHQVideo()) {
		// TODO: Search for and use the best supported format (which may be
		// lower than 32bpp) once the scaling code in Graphics supports
		// 16bpp/24bpp, and once the SDL backend can correctly communicate
		// supported pixel formats above whatever format is currently used by
		// _hwsurface. Right now, this will either show an error dialog (OpenGL)
		// or just crash entirely (SDL) if the backend does not support this
		// 32bpp pixel format, which sucks since this code really ought to be
		// able to fall back to NN scaling for games with 256-color videos
		// without any error.
		const Graphics::PixelFormat format(4, 8, 8, 8, 8, 24, 16, 8, 0);
		g_sci->_gfxFrameout->setPixelFormat(format);
		_hqVideoMode = (g_system->getScreenFormat() == format);
		return _hqVideoMode;
	} else {
		_hqVideoMode = false;
	}
#endif

	return false;
}

bool VideoPlayer::endHQVideo() {
#ifdef USE_RGB_COLOR
	if (g_system->getScreenFormat().bytesPerPixel != 1) {
		const Graphics::PixelFormat format = Graphics::PixelFormat::createFormatCLUT8();
		g_sci->_gfxFrameout->setPixelFormat(format);
		assert(g_system->getScreenFormat() == format);
		_hqVideoMode = false;
		return true;
	}
#endif

	return false;
}

VideoPlayer::EventFlags VideoPlayer::playUntilEvent(const EventFlags flags, const uint32 maxSleepMs) {
	// Flushing all the keyboard and mouse events out of the event manager keeps
	// events queued from before the start of playback from accidentally
	// activating a video stop flag
	_eventMan->flushEvents();

	_decoder->start();

	EventFlags stopFlag = kEventFlagNone;
	for (;;) {
		g_sci->sleep(MIN(_decoder->getTimeToNextFrame(), maxSleepMs));

		const Graphics::Surface *nextFrame = nullptr;
		// If a decoder needs more than one update per loop, this means we are
		// running behind and should skip rendering these frames (but must still
		// submit any palettes from skipped frames)
		while (_decoder->needsUpdate()) {
			nextFrame = _decoder->decodeNextFrame();
			if (_decoder->hasDirtyPalette()) {
				submitPalette(_decoder->getPalette());
			}
		}

		// Some frames may contain only audio and/or palette data; this occurs
		// with Duck videos and is not an error
		if (nextFrame) {
			renderFrame(*nextFrame);
		}

		// Event checks must only happen *after* the decoder is updated (1) and
		// frame rendered (2), otherwise (1) interval yields will get stuck
		// forever on the current frame, and (2) other events will end up
		// dropping the new frame entirely
		stopFlag = checkForEvent(flags);
		if (stopFlag != kEventFlagNone) {
			break;
		}

		// Only call to update the screen after the event check, otherwise
		// whatever the game scripts try to change when the player yields to
		// them will not make it into the hardware buffer until the next tick
		g_sci->_gfxFrameout->updateScreen();
	}

	return stopFlag;
}

VideoPlayer::EventFlags VideoPlayer::checkForEvent(const EventFlags flags) {
	if (g_engine->shouldQuit() || _decoder->endOfVideo()) {
		return kEventFlagEnd;
	}

	SciEvent event = _eventMan->getSciEvent(kSciEventMousePress | kSciEventPeek);
	if ((flags & kEventFlagMouseDown) && event.type == kSciEventMousePress) {
		return kEventFlagMouseDown;
	}

	event = _eventMan->getSciEvent(kSciEventKeyDown | kSciEventPeek);
	if ((flags & kEventFlagEscapeKey) && event.type == kSciEventKeyDown) {
		if (getSciVersion() < SCI_VERSION_3) {
			while ((event = _eventMan->getSciEvent(kSciEventKeyDown)),
				   event.type != kSciEventNone) {
				if (event.character == kSciKeyEsc) {
					return kEventFlagEscapeKey;
				}
			}
		} else if (event.character == kSciKeyEsc) {
			return kEventFlagEscapeKey;
		}
	}

	return kEventFlagNone;
}

void VideoPlayer::submitPalette(const uint8 palette[256 * 3]) const {
#ifdef USE_RGB_COLOR
	if (g_system->getScreenFormat().bytesPerPixel != 1) {
		return;
	}
#endif

	assert(palette);
	g_system->getPaletteManager()->setPalette(palette, 0, 256);

	// KQ7 1.x has videos encoded using Microsoft Video 1 where palette 0 is
	// white and 255 is black, which is basically the opposite of DOS/Win SCI
	// palettes. So, when drawing to an 8bpp hwscreen, whenever a new palette is
	// seen, the screen must be re-filled with the new black entry to ensure
	// areas outside the video are always black and not some other color
	for (int color = 0; color < 256; ++color) {
		if (palette[0] == 0 && palette[1] == 0 && palette[2] == 0) {
			g_system->fillScreen(color);
			break;
		}
		palette += 3;
	}
}

void VideoPlayer::renderFrame(const Graphics::Surface &nextFrame) const {
	bool freeConvertedFrame;
	Graphics::Surface *convertedFrame;
	// Avoid creating a duplicate copy of the surface when it is not necessary
	if (_decoder->getPixelFormat() == g_system->getScreenFormat()) {
		freeConvertedFrame = false;
		convertedFrame = const_cast<Graphics::Surface *>(&nextFrame);
	} else {
		freeConvertedFrame = true;
		convertedFrame = nextFrame.convertTo(g_system->getScreenFormat(), _decoder->getPalette());
	}
	assert(convertedFrame);

	if (_decoder->getWidth() != _drawRect.width() || _decoder->getHeight() != _drawRect.height()) {
		Graphics::Surface *const unscaledFrame(convertedFrame);
		// TODO: The only reason TransparentSurface is used here because it is
		// where common scaler code is right now, which should just be part of
		// Graphics::Surface (or some free functions).
		const Graphics::TransparentSurface tsUnscaledFrame(*unscaledFrame);
#ifdef USE_RGB_COLOR
		if (_hqVideoMode) {
			convertedFrame = tsUnscaledFrame.scaleT<Graphics::FILTER_BILINEAR>(_drawRect.width(), _drawRect.height());
		} else {
#elif 1
		{
#else
		}
#endif
			convertedFrame = tsUnscaledFrame.scaleT<Graphics::FILTER_NEAREST>(_drawRect.width(), _drawRect.height());
		}
		assert(convertedFrame);
		if (freeConvertedFrame) {
			unscaledFrame->free();
			delete unscaledFrame;
		}
		freeConvertedFrame = true;
	}

	g_system->copyRectToScreen(convertedFrame->getPixels(), convertedFrame->pitch, _drawRect.left, _drawRect.top, _drawRect.width(), _drawRect.height());
	g_sci->_gfxFrameout->updateScreen();

	if (freeConvertedFrame) {
		convertedFrame->free();
		delete convertedFrame;
	}
}

template <typename PixelType>
void VideoPlayer::renderLQToSurface(Graphics::Surface &out, const Graphics::Surface &nextFrame, const bool doublePixels, const bool blackLines) const {

	const int lineCount = blackLines ? 2 : 1;
	if (doublePixels) {
		for (int16 y = 0; y < nextFrame.h * 2; y += lineCount) {
			const PixelType *source = (const PixelType *)nextFrame.getBasePtr(0, y >> 1);
			PixelType *target = (PixelType *)out.getBasePtr(0, y);
			for (int16 x = 0; x < nextFrame.w; ++x) {
				*target++ = *source;
				*target++ = *source++;
			}
		}
	} else if (blackLines) {
		for (int16 y = 0; y < nextFrame.h; y += lineCount) {
			const PixelType *source = (const PixelType *)nextFrame.getBasePtr(0, y);
			PixelType *target = (PixelType *)out.getBasePtr(0, y);
			memcpy(target, source, out.w * sizeof(PixelType));
		}
	} else {
		out.copyRectToSurface(nextFrame.getPixels(), nextFrame.pitch, 0, 0, nextFrame.w, nextFrame.h);
	}
}

void VideoPlayer::setDrawRect(const int16 x, const int16 y, const int16 width, const int16 height) {
	_drawRect = Common::Rect(x, y, x + width, y + height);
	if (_drawRect.right > g_system->getWidth() || _drawRect.bottom > g_system->getHeight()) {
		warning("Draw rect (%d, %d, %d, %d) is out of bounds of the screen; clipping it", PRINT_RECT(_drawRect));
		_drawRect.clip(g_system->getWidth(), g_system->getHeight());
	}
}

#pragma mark SEQPlayer

SEQPlayer::SEQPlayer(EventManager *eventMan) :
	VideoPlayer(eventMan) {}

void SEQPlayer::play(const Common::String &fileName, const int16 numTicks, const int16, const int16) {

	_decoder.reset(new SEQDecoder(numTicks));

	if (!VideoPlayer::open(fileName)) {
		_decoder.reset();
		return;
	}

	const int16 scriptWidth = g_sci->_gfxFrameout->getScriptWidth();
	const int16 scriptHeight = g_sci->_gfxFrameout->getScriptHeight();
	const int16 screenWidth = g_sci->_gfxFrameout->getScreenWidth();
	const int16 screenHeight = g_sci->_gfxFrameout->getScreenHeight();

	const int16 scaledWidth = (_decoder->getWidth() * Ratio(screenWidth, scriptWidth)).toInt();
	const int16 scaledHeight = (_decoder->getHeight() * Ratio(screenHeight, scriptHeight)).toInt();

	// Normally we would use the coordinates passed into the play function to
	// position the video, but since we are scaling the video (which SSCI did
	// not do), the coordinates are not correct. Since videos are always
	// intended to play in the center of the screen, we just recalculate the
	// origin here.
	_drawRect.left = (screenWidth - scaledWidth) / 2;
	_drawRect.top = (screenHeight - scaledHeight) / 2;
	_drawRect.setWidth(scaledWidth);
	_drawRect.setHeight(scaledHeight);

	startHQVideo();
	playUntilEvent(kEventFlagMouseDown | kEventFlagEscapeKey);
	endHQVideo();
	g_system->fillScreen(0);
	_decoder.reset();
}

#pragma mark -
#pragma mark AVIPlayer

AVIPlayer::AVIPlayer(EventManager *eventMan) :
	VideoPlayer(eventMan, new Video::AVIDecoder()),
	_status(kAVINotOpen) {
	_decoder->setSoundType(Audio::Mixer::kSFXSoundType);
}

AVIPlayer::IOStatus AVIPlayer::open(const Common::String &fileName) {
	if (_status != kAVINotOpen) {
		close();
	}

	if (!VideoPlayer::open(fileName)) {
		return kIOFileNotFound;
	}

	_status = kAVIOpen;
	return kIOSuccess;
}

AVIPlayer::IOStatus AVIPlayer::init(const bool doublePixels) {
	// Calls to initialize the AVI player in SCI can be made in a few ways:
	//
	// * kShowMovie(WinInit, x, y) to render the video at (x,y) using its
	//   original resolution, or
	// * kShowMovie(WinInit, x, y, w, h) to render the video at (x,y) with
	//   rescaling to the given width and height, or
	// * kShowMovie(WinInitDouble, x, y) to render the video at (x,y) with
	//   rescaling to double the original resolution.
	//
	// Unfortunately, the values passed by game scripts are frequently wrong:
	//
	// * KQ7 passes origin coordinates that cause videos to be misaligned on the
	//   Y-axis;
	// * GK1 passes width and height that change the aspect ratio of the videos,
	//   even though they were rendered with square pixels (and in the case of
	//   CREDITS.AVI, cause the video to be badly downscaled);
	// * The GK2 demo does all of these things at the same time.
	//
	// Fortunately, whenever all of these games play an AVI, they are just
	// trying to play a video at the center of the screen. So, we ignore the
	// values that the game sends, and instead calculate the correct dimensions
	// and origin based on the video data, only allowing games to specify
	// whether or not the videos should be scaled up 2x.

	if (_status == kAVINotOpen) {
		return kIOFileNotFound;
	}

	g_sci->_gfxCursor32->hide();

	int16 width = _decoder->getWidth();
	int16 height = _decoder->getHeight();
	if (doublePixels) {
		width *= 2;
		height *= 2;
	}

	const int16 screenWidth = g_sci->_gfxFrameout->getScreenWidth();
	const int16 screenHeight = g_sci->_gfxFrameout->getScreenHeight();

	// When scaling videos, they must not grow larger than the hardware screen
	// or else the engine will crash. This is particularly important for the GK1
	// CREDITS.AVI since the game sends extra width/height arguments, causing it
	// to be treated as needing upscaling even though it does not.
	width = MIN<int16>(width, screenWidth);
	height = MIN<int16>(height, screenHeight);

	_drawRect.left = (screenWidth - width) / 2;
	_drawRect.top = (screenHeight - height) / 2;
	_drawRect.setWidth(width);
	_drawRect.setHeight(height);

	if (!startHQVideo() && _decoder->getPixelFormat().bytesPerPixel != 1) {
		const Common::List<Graphics::PixelFormat> outFormats = g_system->getSupportedFormats();
		Graphics::PixelFormat inFormat = _decoder->getPixelFormat();
		Graphics::PixelFormat bestFormat = outFormats.front();
		Common::List<Graphics::PixelFormat>::const_iterator it;
		for (it = outFormats.begin(); it != outFormats.end(); ++it) {
			if (*it == inFormat) {
				bestFormat = inFormat;
				break;
			}
		}

		if (bestFormat.bytesPerPixel != 2 && bestFormat.bytesPerPixel != 4) {
			error("Failed to find any valid output pixel format");
		}

		g_sci->_gfxFrameout->setPixelFormat(bestFormat);
	}

	return kIOSuccess;
}

AVIPlayer::IOStatus AVIPlayer::play(const int16 from, const int16 to, const int16, const bool async) {
	if (_status == kAVINotOpen) {
		return kIOFileNotFound;
	}

	if (from >= 0 && to > 0 && from <= to) {
		_decoder->seekToFrame(from);
		_decoder->setEndFrame(to);
	}

	if (!async || getSciVersion() == SCI_VERSION_2_1_EARLY) {
		playUntilEvent(kEventFlagNone);
	} else {
		_status = kAVIPlaying;
	}

	return kIOSuccess;
}

AVIPlayer::EventFlags AVIPlayer::playUntilEvent(const EventFlags flags, const uint32 maxSleepMs) {
	// In SSCI, whether or not a video could be skipped was controlled by game
	// scripts; here, we always allow skipping video with the mouse or escape
	// key, to improve the user experience
	return VideoPlayer::playUntilEvent(flags | kEventFlagMouseDown | kEventFlagEscapeKey, maxSleepMs);
}

AVIPlayer::IOStatus AVIPlayer::close() {
	if (_status == kAVINotOpen) {
		return kIOSuccess;
	}

	if (!endHQVideo()) {
		// This fixes a single-frame white flash after playback of the KQ7 1.x
		// videos, which replace palette entry 0 with white
		const uint8 black[3] = { 0, 0, 0 };
		g_system->getPaletteManager()->setPalette(black, 0, 1);
	}

	g_system->fillScreen(0);
	g_sci->_gfxCursor32->unhide();
	_decoder->close();
	_status = kAVINotOpen;
	return kIOSuccess;
}

AVIPlayer::IOStatus AVIPlayer::cue(const uint16 frameNo) {
	if (!_decoder->seekToFrame(frameNo)) {
		return kIOSeekFailed;
	}

	_status = kAVIPaused;
	return kIOSuccess;
}

uint16 AVIPlayer::getDuration() const {
	if (_status == kAVINotOpen) {
		return 0;
	}

	return _decoder->getFrameCount();
}

#pragma mark -
#pragma mark VMDPlayer

VMDPlayer::VMDPlayer(EventManager *eventMan, SegManager *segMan) :
	VideoPlayer(eventMan, new Video::AdvancedVMDDecoder(Audio::Mixer::kSFXSoundType)),
	_segMan(segMan),

	_isOpen(false),
	_isInitialized(false),
	_bundledVmd(nullptr),
	_yieldFrame(0),
	_yieldInterval(0),
	_lastYieldedFrameNo(0),

	_plane(nullptr),
	_screenItem(nullptr),
	_planeIsOwned(true),
	_priority(0),
	_doublePixels(false),
	_stretchVertical(false),
	_blackLines(false),
	_leaveScreenBlack(false),
	_leaveLastFrame(false),
	_ignorePalettes(false),

	_blackoutPlane(nullptr),

	_startColor(0),
	_endColor(255),
#ifdef SCI_VMD_BLACK_PALETTE
	_blackPalette(false),
#endif
	_boostPercent(100),
	_boostStartColor(0),
	_boostEndColor(255),

	_showCursor(false) {}

VMDPlayer::~VMDPlayer() {
	close();
}

#pragma mark -
#pragma mark VMDPlayer - Playback

VMDPlayer::IOStatus VMDPlayer::open(const Common::String &fileName, const OpenFlags flags) {
	if (_isOpen) {
		error("Attempted to play %s, but another VMD was loaded", fileName.c_str());
	}

	if (g_sci->_features->VMDOpenStopsAudio()) {
		g_sci->_audio32->stop(kAllChannels);
	}

	Resource *bundledVmd = g_sci->getResMan()->findResource(ResourceId(kResourceTypeVMD, fileName.asUint64()), true);

	if (bundledVmd != nullptr) {
		Common::SeekableReadStream *stream = bundledVmd->makeStream();
		if (_decoder->loadStream(stream)) {
			_bundledVmd = bundledVmd;
			_isOpen = true;
		} else {
			delete stream;
			g_sci->getResMan()->unlockResource(bundledVmd);
		}
	} else if (_decoder->loadFile(fileName)) {
		_isOpen = true;
	}

	if (_isOpen) {
		if (flags & kOpenFlagMute) {
			_decoder->setVolume(0);
		}
		return kIOSuccess;
	}

	return kIOError;
}

void VMDPlayer::init(int16 x, int16 y, const PlayFlags flags, const int16 boostPercent, const int16 boostStartColor, const int16 boostEndColor) {
	const int16 screenWidth = g_sci->_gfxFrameout->getScreenWidth();
	const int16 screenHeight = g_sci->_gfxFrameout->getScreenHeight();
	const bool upscaleVideos = ConfMan.hasKey("enable_video_upscale") ? ConfMan.getBool("enable_video_upscale") : false;

	_doublePixels = (flags & kPlayFlagDoublePixels) || upscaleVideos;
	_stretchVertical = flags & kPlayFlagStretchVertical;

	const int16 width = _decoder->getWidth() << (_doublePixels ? 1 : 0);
	const int16 height = _decoder->getHeight() << (_doublePixels || _stretchVertical ? 1 : 0);

	if (getSciVersion() < SCI_VERSION_3) {
		x &= ~1;
	}
	
	if (upscaleVideos) {
		x = (screenWidth - width) / 2;
		y = (screenHeight - height) / 2;
	}

	_blackLines = ConfMan.getBool("enable_black_lined_video") && (flags & kPlayFlagBlackLines);
	// If ScummVM has been configured to disable black lines on video playback,
	// the boosts need to be ignored too or else the brightness of the video
	// will be too high
	_boostPercent = 100 + (_blackLines && (flags & kPlayFlagBoost) ? boostPercent : 0);
	_boostStartColor = CLIP<int16>(boostStartColor, 0, 255);
	_boostEndColor = CLIP<int16>(boostEndColor, 0, 255);
	_leaveScreenBlack = flags & kPlayFlagLeaveScreenBlack;
	_leaveLastFrame = flags & kPlayFlagLeaveLastFrame;
#ifdef SCI_VMD_BLACK_PALETTE
	_blackPalette = flags & kPlayFlagBlackPalette;
#endif

	setDrawRect(x, y, width, height);
}

VMDPlayer::IOStatus VMDPlayer::close() {
	if (!_isOpen) {
		return kIOSuccess;
	}

	if (_isInitialized) {
		if (_isComposited) {
			closeComposited();
		} else {
			closeOverlay();
		}

		if (_blackoutPlane != nullptr) {
			g_sci->_gfxFrameout->deletePlane(*_blackoutPlane);
			_blackoutPlane = nullptr;
		}

		if (!_leaveLastFrame && !_leaveScreenBlack) {
			// This call *actually* deletes the blackout plane
			g_sci->_gfxFrameout->frameOut(true);
		}

		if (!_showCursor) {
			g_sci->_gfxCursor32->unhide();
		}
	}

	_decoder->close();

	if (_bundledVmd) {
		g_sci->getResMan()->unlockResource(_bundledVmd);
		_bundledVmd = nullptr;
	}

	_isOpen = false;
	_isInitialized = false;
	_ignorePalettes = false;
	_lastYieldedFrameNo = 0;
	_planeIsOwned = true;
	_priority = 0;
	_drawRect = Common::Rect();
	return kIOSuccess;
}

VMDPlayer::VMDStatus VMDPlayer::getStatus() const {
	if (!_isOpen) {
		return kVMDNotOpen;
	}
	if (_decoder->isPaused()) {
		return kVMDPaused;
	}
	if (_decoder->isPlaying()) {
		return kVMDPlaying;
	}
	if (_decoder->endOfVideo()) {
		return kVMDFinished;
	}
	return kVMDOpen;
}

VMDPlayer::EventFlags VMDPlayer::kernelPlayUntilEvent(const EventFlags flags, const int16 lastFrameNo, const int16 yieldInterval) {
	assert(lastFrameNo >= -1);

	const int32 maxFrameNo = _decoder->getFrameCount() - 1;

	if (flags & kEventFlagToFrame) {
		_yieldFrame = MIN<int32>(lastFrameNo, maxFrameNo);
	} else {
		_yieldFrame = maxFrameNo;
	}

	if (flags & kEventFlagYieldToVM) {
		_yieldInterval = 3;
		if (yieldInterval == -1 && !(flags & kEventFlagToFrame)) {
			_yieldInterval = lastFrameNo;
		} else if (yieldInterval != -1) {
			_yieldInterval = MIN<int32>(yieldInterval, maxFrameNo);
		}
	} else {
		_yieldInterval = maxFrameNo;
	}

	return playUntilEvent(flags);
}

VMDPlayer::EventFlags VMDPlayer::playUntilEvent(const EventFlags flags, const uint32) {
	if (flags & kEventFlagReverse) {
		// This flag may not work properly since SSCI does not care if a video
		// has audio, but the VMD decoder does.
		warning("VMD reverse playback flag was set. Please report this event to the bug tracker");
		const bool success = _decoder->setReverse(true);
		assert(success);
		_decoder->setVolume(0);
	}

	if (!_isInitialized) {
		_isInitialized = true;

		if (!_showCursor) {
			g_sci->_gfxCursor32->hide();
		}

		if (!_blackoutRect.isEmpty() && _planeIsOwned) {
			_blackoutPlane = new Plane(_blackoutRect);
			g_sci->_gfxFrameout->addPlane(_blackoutPlane);
		}

		if (shouldUseCompositing()) {
			_isComposited = true;
			initComposited();
		} else {
			_isComposited = false;
			initOverlay();
		}
	}

	// Sleeping any more than 1/60th of a second will make the mouse feel
	// very sluggish during VMD action sequences because the frame rate of
	// VMDs is usually only 15fps
	return VideoPlayer::playUntilEvent(flags, 10);
}

VMDPlayer::EventFlags VMDPlayer::checkForEvent(const EventFlags flags) {
	const int currentFrameNo = _decoder->getCurFrame();

	if (currentFrameNo == _yieldFrame) {
		return kEventFlagEnd;
	}

	if (_yieldInterval > 0 &&
		currentFrameNo != _lastYieldedFrameNo &&
		(currentFrameNo % _yieldInterval) == 0) {

		_lastYieldedFrameNo = currentFrameNo;
		return kEventFlagYieldToVM;
	}

	EventFlags stopFlag = VideoPlayer::checkForEvent(flags);
	if (stopFlag != kEventFlagNone) {
		return stopFlag;
	}

	const SciEvent event = _eventMan->getSciEvent(kSciEventHotRectangle | kSciEventPeek);
	if ((flags & kEventFlagHotRectangle) && event.type == kSciEventHotRectangle) {
		return kEventFlagHotRectangle;
	}

	return kEventFlagNone;
}

void VMDPlayer::initOverlay() {
	// Composited videos forced through the overlay renderer (due to HQ video
	// mode) still need to occlude whatever is behind them in the renderer (as
	// in composited mode) to prevent palette glitches caused by premature
	// submission of occluded screen items (e.g. leaving the lava room sphere in
	// the volcano in Lighthouse, the pic after the video finishes playing will
	// be rendered with the wrong palette)
	if (isNormallyComposited() && _planeIsOwned) {
		_plane = new Plane(_drawRect, kPlanePicColored);
		if (_priority) {
			_plane->_priority = _priority;
		}
		g_sci->_gfxFrameout->addPlane(_plane);
	}

	// Make sure that any pending graphics changes from the game are submitted
	// before starting playback, since if they aren't, and the video player
	// yields back to the VM in the middle of playback, there may be a flash of
	// content that draws over the video. (This happens when subtitles are
	// enabled in Shivers.)
	g_sci->_gfxFrameout->frameOut(true);

#ifdef USE_RGB_COLOR
	// TODO: Allow interpolation for videos where the cursor is drawn, either by
	// writing to an intermediate 4bpp surface and using that surface during
	// cursor drawing, or by promoting the cursor code to use CursorMan, if
	// possible
	if (startHQVideo()) {
		redrawGameScreen();
	}
#endif
}

#ifdef USE_RGB_COLOR
void VMDPlayer::redrawGameScreen() const {
	if (!_hqVideoMode) {
		return;
	}

	g_sci->_gfxFrameout->redrawGameScreen(_drawRect);
}
#endif

void VMDPlayer::renderOverlay(const Graphics::Surface &nextFrame) const {
#ifdef USE_RGB_COLOR
	if (_hqVideoMode) {
		VideoPlayer::renderFrame(nextFrame);
		return;
	}
#endif

	Graphics::Surface out = g_sci->_gfxFrameout->getCurrentBuffer().getSubArea(_drawRect);
	renderLQToSurface<uint8>(out, nextFrame, _doublePixels, _blackLines);
	g_sci->_gfxFrameout->directFrameOut(_drawRect);
}

void VMDPlayer::submitPalette(const uint8 rawPalette[256 * 3]) const {
	if (_ignorePalettes) {
		return;
	}

	Palette palette;
	for (uint16 i = 0; i < _startColor; ++i) {
		palette.colors[i].used = false;
	}
	for (uint16 i = _endColor + 1; i < ARRAYSIZE(palette.colors); ++i) {
		palette.colors[i].used = false;
	}
#if SCI_VMD_BLACK_PALETTE
	if (_blackPalette) {
		for (uint16 i = _startColor; i <= _endColor; ++i) {
			palette.colors[i].r = palette.colors[i].g = palette.colors[i].b = 0;
			palette.colors[i].used = true;
		}
	} else
#endif
		fillPalette(rawPalette, palette);

	if (_isComposited) {
		SciBitmap *bitmap = _segMan->lookupBitmap(_bitmapId);
		bitmap->setPalette(palette);
		// SSCI calls updateScreenItem and frameOut here, but this is not
		// necessary in ScummVM since the new palette gets submitted before the
		// next frame is rendered, and the frame rendering call will perform the
		// same operations.
	} else {
		g_sci->_gfxPalette32->submit(palette);
		g_sci->_gfxPalette32->updateForFrame();
		g_sci->_gfxPalette32->updateHardware();
	}

#if SCI_VMD_BLACK_PALETTE
	if (_blackPalette) {
		fillPalette(rawPalette, palette);
		if (_isComposited) {
			SciBitmap *bitmap = _segMan->lookupBitmap(_bitmapId);
			bitmap->setPalette(palette);
		}
		g_sci->_gfxPalette32->submit(palette);
		g_sci->_gfxPalette32->updateForFrame();
		g_sci->_gfxPalette32->updateHardware();
	}
#endif

#ifdef USE_RGB_COLOR
	// Changes to the palette may affect areas outside of the video; when the
	// engine is rendering video in high color, palette changes will only take
	// effect once the entire screen is redrawn to the high color surface
	redrawGameScreen();
#endif
}

void VMDPlayer::closeOverlay() {
	if (isNormallyComposited() && _planeIsOwned && _plane != nullptr) {
		g_sci->_gfxFrameout->deletePlane(*_plane);
		_plane = nullptr;
	}

#ifdef USE_RGB_COLOR
	if (_hqVideoMode) {
		if (endHQVideo()) {
			g_sci->_gfxFrameout->resetHardware();
		}
		return;
	}
#endif

	g_sci->_gfxFrameout->frameOut(true, _drawRect);
}

void VMDPlayer::initComposited() {
	ScaleInfo vmdScaleInfo;

	if (_doublePixels) {
		vmdScaleInfo.x *= 2;
		vmdScaleInfo.y *= 2;
		vmdScaleInfo.signal = kScaleSignalManual;
	} else if (_stretchVertical) {
		vmdScaleInfo.y *= 2;
		vmdScaleInfo.signal = kScaleSignalManual;
	}

	const uint32 hunkPaletteSize = HunkPalette::calculateHunkPaletteSize(256, false);
	const int16 screenWidth = g_sci->_gfxFrameout->getScreenWidth();
	const int16 screenHeight = g_sci->_gfxFrameout->getScreenHeight();

	SciBitmap &vmdBitmap = *_segMan->allocateBitmap(&_bitmapId, _drawRect.width(), _drawRect.height(), 255, 0, 0, screenWidth, screenHeight, hunkPaletteSize, false, false);
	vmdBitmap.getBuffer().fillRect(Common::Rect(_drawRect.width(), _drawRect.height()), 0);

	CelInfo32 vmdCelInfo;
	vmdCelInfo.bitmap = _bitmapId;

	Video::AdvancedVMDDecoder *decoder = dynamic_cast<Video::AdvancedVMDDecoder *>(_decoder.get());
	assert(decoder);
	decoder->setSurfaceMemory(vmdBitmap.getPixels(), vmdBitmap.getWidth(), vmdBitmap.getHeight(), 1);

	if (_planeIsOwned) {
		_plane = new Plane(_drawRect, kPlanePicColored);
		if (_priority) {
			_plane->_priority = _priority;
		}
		g_sci->_gfxFrameout->addPlane(_plane);
		_screenItem = new ScreenItem(_plane->_object, vmdCelInfo, Common::Point(), vmdScaleInfo);
	} else {
		_screenItem = new ScreenItem(_plane->_object, vmdCelInfo, Common::Point(_drawRect.left, _drawRect.top), vmdScaleInfo);
		if (_priority) {
			_screenItem->_priority = _priority;
		}
	}

	if (_blackLines) {
		_screenItem->_drawBlackLines = true;
	}

	// In SSCI, there was code for positioning the screen item using insetRect
	// here, but none of the game scripts seem to use this functionality.

	g_sci->_gfxFrameout->addScreenItem(*_screenItem);

	// Composited VMDs periodically yield to game scripts which will often call
	// kFrameOut to make changes to other parts of the screen. Since VMDPlayer
	// is responsible for throttling output during these times, GfxFrameout
	// needs to stop throttling kFrameOut calls or else we will drop frames when
	// kFrameOut sleeps right through the next frame
	g_sci->_gfxFrameout->_throttleKernelFrameOut = false;
}

void VMDPlayer::renderComposited() const {
	g_sci->_gfxFrameout->updateScreenItem(*_screenItem);
	g_sci->_gfxFrameout->frameOut(true);
}

void VMDPlayer::closeComposited() {
	if (_bitmapId != NULL_REG) {
		_segMan->freeBitmap(_bitmapId);
		_bitmapId = NULL_REG;
	}

	if (!_planeIsOwned && _screenItem != nullptr) {
		g_sci->_gfxFrameout->deleteScreenItem(*_screenItem);
		_screenItem = nullptr;
	} else if (_plane != nullptr) {
		g_sci->_gfxFrameout->deletePlane(*_plane);
		_plane = nullptr;
	}

	if (!_leaveLastFrame && _leaveScreenBlack) {
		// This call *actually* deletes the plane/screen item
		g_sci->_gfxFrameout->frameOut(true);
	}

	g_sci->_gfxFrameout->_throttleKernelFrameOut = true;
}

#pragma mark -
#pragma mark VMDPlayer - Rendering

void VMDPlayer::renderFrame(const Graphics::Surface &nextFrame) const {
	if (_isComposited) {
		renderComposited();
	} else {
		renderOverlay(nextFrame);
	}
}

void VMDPlayer::fillPalette(const uint8 rawPalette[256 * 3], Palette &outPalette) const {
	const byte *vmdPalette = rawPalette + _startColor * 3;
	for (uint16 i = _startColor; i <= _endColor; ++i) {
		uint8 r = *vmdPalette++;
		uint8 g = *vmdPalette++;
		uint8 b = *vmdPalette++;

		if (_boostPercent != 100 && i >= _boostStartColor && i <= _boostEndColor) {
			r = CLIP(r * _boostPercent / 100, 0, 255);
			g = CLIP(g * _boostPercent / 100, 0, 255);
			b = CLIP(b * _boostPercent / 100, 0, 255);
		}

		outPalette.colors[i].r = r;
		outPalette.colors[i].g = g;
		outPalette.colors[i].b = b;
		outPalette.colors[i].used = true;
	}
}

void VMDPlayer::setPlane(const int16 priority, const reg_t planeId) {
	_priority = priority;
	if (planeId != NULL_REG) {
		_plane = g_sci->_gfxFrameout->getPlanes().findByObject(planeId);
		assert(_plane != nullptr);
		_planeIsOwned = false;
	}
}

#pragma mark -
#pragma mark VMDPlayer - Palette

void VMDPlayer::restrictPalette(const uint8 startColor, const int16 endColor) {
	_startColor = startColor;
	// At least GK2 sends 256 as the end color, which is wrong, but works in
	// SSCI as the storage size is 4 bytes and used values are clamped to 0-255
	_endColor = MIN<int16>(255, endColor);
}

#pragma mark -
#pragma mark DuckPlayer

DuckPlayer::DuckPlayer(EventManager *eventMan, SegManager *segMan) :
	VideoPlayer(eventMan, new Video::AVIDecoder()),
	_plane(nullptr),
	_status(kDuckClosed),
	_volume(Audio::Mixer::kMaxChannelVolume),
	_doFrameOut(false) {
	_decoder->setSoundType(Audio::Mixer::kSFXSoundType);
}

void DuckPlayer::open(const GuiResourceId resourceId, const int displayMode, const int16 x, const int16 y) {
	if (_status != kDuckClosed) {
		error("Attempted to play %u.duk, but another video was loaded", resourceId);
	}

	const Common::String fileName = Common::String::format("%u.duk", resourceId);

	if (!VideoPlayer::open(fileName)) {
		return;
	}

	_decoder->setVolume(_volume);

	_doublePixels = displayMode != 0;
	_blackLines = ConfMan.getBool("enable_black_lined_video") &&
				 (displayMode == 1 || displayMode == 3);

	// SSCI seems to incorrectly calculate the draw rect by scaling the origin
	// in addition to the width/height for the BR point
	setDrawRect(x, y,
				(_decoder->getWidth() << (_doublePixels ? 1 : 0)),
				(_decoder->getHeight() << (_doublePixels ? 1 : 0)));

	g_sci->_gfxCursor32->hide();

	if (_doFrameOut) {
		_plane = new Plane(_drawRect, kPlanePicColored);
		g_sci->_gfxFrameout->addPlane(_plane);
		g_sci->_gfxFrameout->frameOut(true);
	}

	if (!startHQVideo() && _decoder->getPixelFormat().bytesPerPixel != 1) {
		g_sci->_gfxFrameout->setPixelFormat(_decoder->getPixelFormat());
	}

	_status = kDuckOpen;
}

void DuckPlayer::play(const int lastFrameNo) {
	// This status check does not exist in the original interpreter, but is
	// necessary to avoid a crash if the engine cannot find or render the video
	// for playback. Game scripts receive no feedback from the kernel regarding
	// whether or not an attempt to open a Duck video actually succeeded, so
	// they can only assume it always succeeds (and so always call to `play`
	// even if they shouldn't).
	if (_status == kDuckClosed) {
		return;
	}

	if (_status != kDuckPlaying) {
		_status = kDuckPlaying;
	}

	if (lastFrameNo != -1) {
		_decoder->setEndFrame(lastFrameNo);
	}

	playUntilEvent(kEventFlagMouseDown | kEventFlagEscapeKey);
}

void DuckPlayer::close() {
	if (_status == kDuckClosed) {
		return;
	}

	_decoder->close();

	endHQVideo();

	g_sci->_gfxCursor32->unhide();

	if (_doFrameOut) {
		g_sci->_gfxFrameout->deletePlane(*_plane);
		g_sci->_gfxFrameout->frameOut(true);
		_plane = nullptr;
	}

	_drawRect = Common::Rect();
	_status = kDuckClosed;
	_volume = Audio::Mixer::kMaxChannelVolume;
	_doFrameOut = false;
}

void DuckPlayer::renderFrame(const Graphics::Surface &nextFrame) const {
#ifdef USE_RGB_COLOR
	if (_hqVideoMode) {
		VideoPlayer::renderFrame(nextFrame);
		return;
	}
#endif

	Graphics::Surface out;
	out.create(_drawRect.width(), _drawRect.height(), nextFrame.format);
	renderLQToSurface<uint16>(out, nextFrame, _doublePixels, _blackLines);
	if (out.format != g_system->getScreenFormat()) {
		out.convertToInPlace(g_system->getScreenFormat());
	}
	g_system->copyRectToScreen(out.getPixels(), out.pitch, _drawRect.left, _drawRect.top, out.w, out.h);
	out.free();
}

} // End of namespace Sci
