/* 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 * Copyright 2020 Google
 *
 */

#include "common/file.h"
#include "hadesch/video.h"
#include "hadesch/pod_image.h"
#include "hadesch/tag_file.h"
#include "hadesch/hadesch.h"
#include "common/system.h"
#include "video/smk_decoder.h"
#include "audio/decoders/aiff.h"
#include "hadesch/pod_file.h"
#include "hadesch/baptr.h"
#include "common/translation.h"

static const int kVideoMaxW = 1280;
static const int kVideoMaxH = 480;

namespace Hadesch {

Common::String LayerId::getDebug() const {
	if (_idx == -1 && _qualifier == "")
		return "[" + _name + "]";
	return Common::String::format("[%s/%s/%d]",
				      _name.c_str(), _qualifier.c_str(), _idx);
}

bool LayerId::operator== (const LayerId &b) const {
	return _name == b._name && _idx == b._idx && _qualifier == b._qualifier;
}

PlayAnimParams PlayAnimParams::loop() {
	return PlayAnimParams(true, true);
}

PlayAnimParams PlayAnimParams::keepLastFrame() {
	return PlayAnimParams(false, true);
}

PlayAnimParams PlayAnimParams::disappear() {
	return PlayAnimParams(false, false);
}

bool PlayAnimParams::getKeepLastFrame() {
	return _keepLastFrame;
}

bool PlayAnimParams::isLoop() {
	return _loop;
}

int PlayAnimParams::getSpeed() {
	return _msperframe;
}

int PlayAnimParams::getFirstFrame() {
	return _firstFrame;
}

int PlayAnimParams::getLastFrame() {
	return _lastFrame;
}

PlayAnimParams PlayAnimParams::partial(int first, int last) const {
	PlayAnimParams ret(*this);
	ret._firstFrame = first;
	ret._lastFrame = last;
	return ret;
}

PlayAnimParams PlayAnimParams::speed(int msperframe) const {
	PlayAnimParams ret(*this);
	ret._msperframe = msperframe;
	return ret;
}

PlayAnimParams PlayAnimParams::backwards() const {
	PlayAnimParams ret(*this);
	ret._firstFrame = _lastFrame;
	ret._lastFrame = _firstFrame;
	return ret;
}

PlayAnimParams::PlayAnimParams(bool isLoop, bool isKeepLastFrame) {
	_loop = isLoop;
	_keepLastFrame = isKeepLastFrame;
	_firstFrame = 0;
	_lastFrame = -1;
	_msperframe = kDefaultSpeed;
}

int VideoRoom::layerComparator(const Layer &a, const Layer &b) {
	if (a.zValue == b.zValue)
		return a.genCounter - b.genCounter;
	return b.zValue - a.zValue;
}

VideoRoom::VideoRoom(const Common::String &dir, const Common::String &pod,
		     const Common::String &assetMapFile) : _layers(layerComparator) {
	Common::String podPath = g_vm->getCDScenesPath() + dir + "/" + pod + ".pod";
	_podFile = Common::SharedPtr<PodFile>(new PodFile(podPath));
	_podFile->openStore(podPath);
	_smkPath = g_vm->getCDScenesPath() + dir;
	Common::SharedPtr<Common::SeekableReadStream> assetMapStream(assetMapFile != "" ? openFile(assetMapFile) : nullptr);
	if (assetMapStream) {
		_assetMap = TextTable(assetMapStream, 2);
	}

	_videoPixels = sharedPtrByteAlloc(kVideoMaxW * kVideoMaxH);
	_hotZone = -1;
	_mouseEnabled = true;
	_videoOffset = Common::Point(0, 0);
	_videoSurfOffset = Common::Point(0, 0);
	_pan = 0;
	_panSpeed = 0;
	_panCallback = -1;
	_pannable = false;
	_leftEdge = false;
	_rightEdge = false;
	_heroBeltEnabled = true;
	_userPanStartLeftCallback = -1;
	_userPanStartRightCallback = -1;
	_userPanEndLeftCallback = -1;
	_userPanEndRightCallback = -1;
	_videoZ = 0;
	_layerGenCounter = 0;
	_finalFade = 0x100;
	_finalFadeSpeed = 0;
	_draggingPtr = 0;
	_isDragging = 0;
}

VideoRoom::~VideoRoom() {
	if (_videoDecoder) {
		_videoDecoder->stop();
		_videoDecoder.reset();
	}
	for (unsigned i = 0; i < _anims.size(); i++)
		g_system->getMixer()->stopHandle(_anims[i]._soundHandle);
}

void VideoRoom::setPannable(bool val) {
	_pannable = val;
}

void VideoRoom::setHotzoneEnabled(const Common::String &name, bool enabled) {
	   _hotZones.setHotzoneEnabled(name, enabled);
}

void VideoRoom::enableHotzone(const Common::String &name) {
	_hotZones.setHotzoneEnabled(name, true);
}

void VideoRoom::disableHotzone(const Common::String &name) {
	_hotZones.setHotzoneEnabled(name, false);
}

void VideoRoom::setLayerEnabled(const LayerId &name, bool val) {
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end(); it++) {
		if (it->name == name)
			it->isEnabled = val;
	}
}

void VideoRoom::setLayerParallax(const LayerId &name, int val) {
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end(); it++) {
		if (it->name == name)
			it->parallax = val;
	}
}

void VideoRoom::setColorScale(const LayerId &name, int val) {
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end(); it++) {
		if (it->name == name)
			it->colorScale = val;
	}
}

void VideoRoom::setScale(const LayerId &name, int val) {
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end(); it++) {
		if (it->name == name)
			it->scale = val;
	}
}

int VideoRoom::getNumFrames(const LayerId &name) {
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end(); it++) {
		if (it->name == name)
			return it->renderable->getNumFrames();
	}

	return 0;
}

void VideoRoom::startAnimationInternal(const LayerId &name, int zValue, int msperframe, bool loop,
				       bool fixedFrame,
				       int first, int last, Common::Point offset) {
	int32 startms = g_vm->getCurrentTime();
	Common::Array<Layer> modifiedZ;
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end();) {
		if (it->name == name) {
			it->isEnabled = true;
			if (fixedFrame)
				it->renderable->selectFrame(first);
			else
				it->renderable->startAnimation(startms, msperframe, loop, first, last);
			it->offset = offset;
			if (it->zValue != zValue) {
				Layer l = *it;
				l.zValue = zValue;
				modifiedZ.push_back(l);
				it = _layers.erase(it);
			} else {
				it++;
			}
		} else {
			it++;
		}
	}
	// This is slow but should rarely happen
	if (!modifiedZ.empty()) {
		for (Common::Array<Layer>::iterator it = modifiedZ.begin(); it != modifiedZ.end(); it++) {
			_layers.insert(*it);
		}
	}
}

void VideoRoom::selectFrame(const LayerId &name, int zValue, int frame, Common::Point offset) {
  	if (!doesLayerExist(name)) {
		addAnimLayerInternal(name, zValue);
	}

	startAnimationInternal(name, zValue, kDefaultSpeed, true, true, frame, frame, offset);
}

PodImage VideoRoom::getLayerFrame(const Hadesch::LayerId &name) {
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end(); it++) {
		if (it->name == name) {
			return it->renderable->getFrame(g_vm->getCurrentTime());
		}
	}

	return PodImage();
}

int VideoRoom::getAnimFrameNum(const Hadesch::LayerId &name) {
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end(); it++) {
		if (it->name == name) {
			return it->renderable->getAnimationFrameNum(g_vm->getCurrentTime());
		}
	}

	return -1;
}

void VideoRoom::stopAnim(const LayerId &name) {
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end(); it++) {
		if (it->name == name) {
			it->isEnabled = false;
		}
	}
	for (unsigned i = 0; i < _anims.size(); i++) {
		if (_anims[i]._animName == name) {
			g_system->getMixer()->stopHandle(_anims[i]._soundHandle);
			_anims[i]._finished = true;
		}
	}
}

void VideoRoom::purgeAnim(const LayerId &name) {
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end();) {
		if (it->name == name) {
			it = _layers.erase(it);
		} else
			it++;
	}
}

void VideoRoom::dumpLayers() {
	debug("Current layers:");
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end(); it++) {
		debug("   %s %s", it->name.getDebug().c_str(), it->isEnabled ? "enabled" : "disabled");
	}
}

void VideoRoom::finish() {
	for (unsigned i = 0; i < _anims.size(); i++) {
		g_system->getMixer()->stopHandle(_anims[i]._soundHandle);
		_anims[i]._finished = true;
	}
}

void VideoRoom::pause() {
	for (unsigned i = 0; i < _anims.size(); i++) {
		g_system->getMixer()->pauseHandle(_anims[i]._soundHandle, true);
	}
}

void VideoRoom::unpause() {
	for (unsigned i = 0; i < _anims.size(); i++) {
		g_system->getMixer()->pauseHandle(_anims[i]._soundHandle, false);
	}
}

bool VideoRoom::doesLayerExist(const LayerId &name) {
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end(); it++) {
		if (it->name == name) {
			return true;
		}
	}
	return false;
}

bool VideoRoom::isAnimationFinished(const LayerId &name, int time) {
	for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end(); it++) {
		if (it->name == name) {
			return it->renderable->isAnimationFinished(time);
		}
	}

	return true;
}

void VideoRoom::addLayer(Renderable *rend, const LayerId &name, int zValue,
			 bool isEnabled, Common::Point offset) {
	Layer l;
	l.renderable = Common::SharedPtr<Renderable>(rend);
	l.name = name;
	l.isEnabled = isEnabled;
	l.offset = offset;
	l.zValue = zValue;
	l.parallax = 0;
	l.colorScale = 0x100;
	l.scale = 100;
	l.genCounter = _layerGenCounter++;
	_layers.insert(l);
}

void VideoRoom::computeHotZone(int time, Common::Point mousePos) {
	bool wasEdge = _leftEdge || _rightEdge;
	_leftEdge = false;
	_rightEdge = false;

	Common::SharedPtr<HeroBelt> belt = g_vm->getHeroBelt();

	if (_heroBeltEnabled)
		belt->computeHotZone(time, mousePos, _mouseEnabled);
	if (_heroBeltEnabled && belt->isOverHeroBelt()) {
		_hotZone = -1;
		return;
	}
	if (!_mouseEnabled) {
		_hotZone = -1;
		return;
	}
	Common::Point canvasPos = mousePos + Common::Point(_pan, 0);
	int wasHotZone = _hotZone;
	_hotZone = _hotZones.pointToIndex(canvasPos);
	if (_hotZone >= 0) {
		if (wasHotZone < 0) {
			_startHotTime = time;
		}
		return;
	}
	_leftEdge = (mousePos.x < 20) && _pan > 0;
	_rightEdge = (mousePos.x > 620) && _pan < 640;
	if ((!wasEdge && (_leftEdge || _rightEdge)))
		_edgeStartTime = time;
	_hotZone = -1;
}

void VideoRoom::setHotZoneOffset(const Common::String &name, Common::Point offset) {
	_hotZones.setHotZoneOffset(name, offset);
}

Common::String VideoRoom::mapClick(Common::Point mousePos) {
	if (!_mouseEnabled) {
		return "";
	}
	Common::Point canvasPos = mousePos + Common::Point(_pan, 0) + _viewportOffset;
	return _hotZones.pointToName(canvasPos);
}

Common::String VideoRoom::getHotZone() {
	return _hotZones.indexToName(_hotZone);
}

int VideoRoom::getCursorAnimationFrame(int time) {
	if (_hotZone < 0)
		return -1;
	if (_hotZones.indexToICSH(_hotZone) != 0)
		return -1;
	return (time - _startHotTime) / kDefaultSpeed;
}

bool VideoRoom::isVideoPlaying() {
	return !!_videoDecoder;
}

void VideoRoom::fadeOut(int ms, const EventHandlerWrapper &callback) {
	_finalFadeCallback = callback;
	_finalFadeSpeed = -(kDefaultSpeed * 0x100) / ms;
}

void VideoRoom::nextFrame(Common::SharedPtr<GfxContext> context, int time, bool stopVideo) {
	context->clear();

	Common::SharedPtr<HeroBelt> belt = g_vm->getHeroBelt();

	if (!_mouseEnabled) {
		_cursor = (time / 200) % 10 + 3;
	} else if (_heroBeltEnabled && belt->isOverHeroBelt()) {
		_cursor = belt->getCursor(time);
	} else if (g_vm->getPersistent()->_currentRoomId == kMonsterPuzzle) {
		_cursor = 17;
	} else if (_hotZone >= 0) {
		_cursor = _hotZones.indexToCursor(_hotZone, (time - _startHotTime) / kDefaultSpeed);
	} else if (_leftEdge && _pannable) {
		_cursor = 14;
	} else if (_rightEdge && _pannable) {
		_cursor = 16;
	} else
		_cursor = 0;

	if (_leftEdge && (time > _edgeStartTime + 2000) && _panSpeed == 0 && _pannable && _mouseEnabled) {
		g_vm->handleEvent(_userPanStartLeftCallback);
		panLeftAnim(_userPanEndLeftCallback);
	}
	if (_rightEdge && (time > _edgeStartTime + 2000) && _panSpeed == 0 && _pannable && _mouseEnabled) {
		g_vm->handleEvent(_userPanStartRightCallback);
		panRightAnim(_userPanEndRightCallback);
	}

	if (_panSpeed != 0) {
		_pan += _panSpeed;
		if (_pan <= 0) {
			g_vm->handleEvent(_panCallback);
			_panCallback = -1;
			_pan = 0;
			_panSpeed = 0;
		}
		if (_pan >= 640) {
			g_vm->handleEvent(_panCallback);
			_panCallback = -1;
			_pan = 640;
			_panSpeed = 0;
		}
	}

	if (_finalFadeSpeed != 0) {
		_finalFade += _finalFadeSpeed;
		if (_finalFade <= 0 && _finalFadeSpeed < 0) {
			_finalFade = 0;
			_finalFadeSpeed = 0;
			_finalFadeCallback();
		}
		if (_finalFade >= 0x100 && _finalFadeSpeed > 0) {
			_finalFade = 0x100;
			_finalFadeSpeed = 0;
			_finalFadeCallback();
		}
	}

	Common::SortedArray<Layer>::iterator layersIterator = _layers.begin();

	for (; layersIterator != _layers.end() && layersIterator->zValue > _videoZ; layersIterator++) {
		if (!layersIterator->isEnabled)
			continue;
		const PodImage &pi = layersIterator->renderable->getFrame(time);

		pi.render (context,
			   layersIterator->offset + Common::Point(_pan * layersIterator->parallax / 640, 0) + kZeroPoint,
			   layersIterator->colorScale, layersIterator->scale);
	}

	if (_videoDecoder && !_videoDecoder->endOfVideo() && _videoDecoder->needsUpdate()) {
		const Graphics::Surface *surf = _videoDecoder->decodeNextFrame();
		_videoW = MIN<int>(surf->w, kVideoMaxW);
		_videoH = MIN<int>(surf->h, kVideoMaxH);
		_videoSurfOffset = _videoOffset;
		memcpy(_videoPixels.get(),
		       surf->getPixels(), _videoW * _videoH);
		const byte *pal = _videoDecoder->getPalette();
		if (pal)
			memcpy(_videoPalette, pal, 256 * 3);
	}

	if (_videoDecoder && _videoW && _videoH) {
		context->blitVideo(_videoPixels.get(), _videoW, _videoW, _videoH, _videoPalette, _videoSurfOffset + kZeroPoint);
	}

	for (; layersIterator != _layers.end(); layersIterator++) {
		if (!layersIterator->isEnabled)
			continue;
		const PodImage &pi = layersIterator->renderable->getFrame(time);

		pi.render (context,
			   layersIterator->offset + Common::Point(_pan * layersIterator->parallax / 640, 0) + kZeroPoint,
			   layersIterator->colorScale, layersIterator->scale);
	}

	if (stopVideo) {
		_subtitles.clear();
		_countQueuedSubtitles.clear();
	}

	while (!_subtitles.empty() && time > _subtitles.front().maxTime) {
		_countQueuedSubtitles[_subtitles.front().ID]--;
		_subtitles.pop();
	}

	if (_subtitles.empty())
		_countQueuedSubtitles.clear();

	if (_videoDecoder && (_videoDecoder->endOfVideo() || (stopVideo && !_mouseEnabled))) {
		debug("videoEnd: %s", _videoDecoderEndEvent.getDebugString().c_str());
		_videoDecoder.reset();
		g_vm->handleEvent(_videoDecoderEndEvent);
	}

	for (unsigned i = 0; i < _anims.size(); i++) {
		if (_anims[i]._finished)
			continue;

		bool soundFinished = !g_system->getMixer()->isSoundHandleActive(_anims[i]._soundHandle);
		const LayerId &animName = _anims[i]._animName;
		bool animFinished = isAnimationFinished(animName, time);
		bool subFinished = (_countQueuedSubtitles.empty() || _countQueuedSubtitles[_anims[i]._subtitleID] == 0);
		bool stopped = stopVideo && _anims[i]._skippable;

		if (stopped) {
			g_system->getMixer()->stopHandle(_anims[i]._soundHandle);
			if (_anims[i]._keepLastFrame)
				for (Common::SortedArray<Layer>::iterator it = _layers.begin(); it != _layers.end(); it++)
					if (it->name == _anims[i]._animName)
						it->renderable->selectFrame(-1);
		}

		if ((soundFinished && animFinished && subFinished) || stopped) {
			_anims[i]._finished = true;
			if (!_anims[i]._keepLastFrame)
				setLayerEnabled(animName, false);
			g_vm->handleEvent(_anims[i]._callbackEvent);
			// Release the reference
			_anims[i]._callbackEvent = EventHandlerWrapper();
		}
	}

	Common::Point viewPoint = Common::Point(_pan, 0) + kZeroPoint + _viewportOffset;

	if (_heroBeltEnabled) {
		belt->render(context, time, viewPoint);
	}

	context->fade(_finalFade);
	if (!_subtitles.empty())
		context->renderSubtitle(_subtitles.front().line, viewPoint);

	context->renderToScreen(viewPoint);
}

void VideoRoom::panLeftAnim(EventHandlerWrapper callback) {
	_panSpeed = -6; // TODO: check this speed
	_panCallback = callback;
}

void VideoRoom::panRightAnim(EventHandlerWrapper callback) {
	_panSpeed = +6; // TODO: check this speed
	_panCallback = callback;
}

void VideoRoom::panRightInstant() {
	_pan = 640;
}

void VideoRoom::addStaticLayer(const LayerId &name, int zValue, Common::Point offset) {
	PodFile pf2(name.getDebug());
	if (!pf2.openStore(Common::SharedPtr<Common::SeekableReadStream>(openFile(mapAsset(name) + ".pod")))) {
		debug("Animation %s isn't found", name.getDebug().c_str());
		return;
	}
	for (int idx = 1; ; idx++) {
		PodImage pi;
		if (!pi.loadImage(pf2, idx)) {
			break;
		}
		Common::Array <PodImage> arr;
		arr.push_back(pi);
		addLayer(new Renderable(arr), name, zValue, true, offset);
	}
}

void VideoRoom::addAnimLayerInternal(const LayerId &name, int zValue, Common::Point offset) {
	Common::SharedPtr<Common::SeekableReadStream> rs(openFile(mapAsset(name) + ".pod"));
	if (!rs) {
		debug("Animation %s isn't found", name.getDebug().c_str());
		return;
	}

	PodFile pf2(name.getDebug());
	pf2.openStore(rs);

	addLayer(new Renderable(pf2.loadImageArray()), name, zValue, false,
		 offset);
}

Renderable::Renderable(Common::Array<PodImage> images) {
	_images = images;
	_msperframe = kDefaultSpeed;
	_loop = false;
	_first = 0;
	_last = 0;
	_startms = 0;
}

int Renderable::getLen() {
	return _first < _last ? _last - _first + 1 : _first - _last + 1;
}

int math_mod(int a, int b) {
	int m = a % b;
	while (m < 0)
		m += ABS(b);
	return m;
}

const PodImage &Renderable::getFrame(int time) {
	return _images[getAnimationFrameNum(time)];
}

int Renderable::getAnimationFrameNum(int time) {
	int f = ((time - _startms) / _msperframe);
	if (f < 0)
		f = 0;
	if (_loop)
		f %= getLen();
	if (f >= getLen())
		f = getLen() - 1;
	if (_first <= _last)
		return math_mod(_first + f, _images.size());
	else
		return math_mod(_first - f, _images.size());
}

void Renderable::startAnimation(int startms, int msperframe, bool loop, int first, int last) {
	_loop = loop;
	_msperframe = msperframe;
	_startms = startms;
	_first = math_mod(first, _images.size());
	_last = math_mod(last, _images.size());
}

void Renderable::selectFrame(int frame) {
	_loop = true;
	_msperframe = kDefaultSpeed;
	_startms = 0;
	frame = math_mod(frame, _images.size());
	_first = frame;
	_last = frame;
}

bool Renderable::isAnimationFinished(int time) {
	return !_loop && time > _startms + getLen() * _msperframe;
}

void VideoRoom::loadHotZones(const Common::String &hotzoneFile, bool enable, Common::Point offset) {
	Common::SharedPtr<Common::SeekableReadStream> hzFile(openFile(hotzoneFile));
	if (!hzFile) {
		debug("Couldn't open %s", hotzoneFile.c_str());
		return;
	}

	_hotZones.readHotzones(hzFile, enable, offset);
}

void VideoRoom::pushHotZones(const Common::String &hotzoneFile, bool enable,
			     Common::Point offset) {
	// TODO: optimize this? maybe. Or maybe not
	_hotZoneStack.push_back(_hotZones);
	_hotZones = HotZoneArray();
	loadHotZones(hotzoneFile, enable, offset);
}

void VideoRoom::popHotZones() {
	// TODO: optimize this? maybe. Or maybe not
	_hotZones = _hotZoneStack.back();
	_hotZoneStack.pop_back();
}

int VideoRoom::getCursor() {
	return _cursor;
}

PodImage *VideoRoom::getDragged() {
	if (_isDragging)
		return &_draggedImage[_draggingPtr];
	else
		return nullptr;
}

void VideoRoom::clearDrag() {
	_isDragging = false;
}

void VideoRoom::drag(const Common::String &name, int frame, Common::Point hotspot) {
	Common::SharedPtr<Common::SeekableReadStream> rs(openFile(mapAsset(name) + ".pod"));
	if (!rs) {
		debug("Animation %s isn't found", name.c_str());
		return;
	}

	PodFile pf2(name);
	pf2.openStore(rs);
	PodImage pi;

	pi.loadImage(pf2, 1 + frame);
	pi.setHotspot(hotspot);

	_draggingPtr = (_draggingPtr + 1) % ARRAYSIZE(_draggedImage);

	_draggedImage[_draggingPtr] = pi;
	_isDragging = true;
}

void VideoRoom::cancelVideo() {
	if (_videoDecoder) {
		_videoDecoder.reset();
		_videoDecoderEndEvent = 0;
		_videoW = 0;
		_videoH = 0;
	}
}

void VideoRoom::playVideo(const Common::String &name, int zValue,
			     EventHandlerWrapper callbackEvent,
			     Common::Point offset) {
	cancelVideo();
	Common::SharedPtr<Video::SmackerDecoder> decoder
	  = Common::SharedPtr<Video::SmackerDecoder>(new Video::SmackerDecoder());

	Common::File *file = new Common::File;
	Common::String mappedName = _assetMap.get(name, 1);
	if (mappedName == "") {
		mappedName = name;
	}
	if (!file->open(_smkPath + "/" + mappedName + ".SMK") || !decoder->loadStream(file)) {
		debug("Video file %s can't be opened", name.c_str());
		g_vm->handleEvent(callbackEvent);
		return;
	}

	decoder->start();
	_videoDecoder = decoder;
	_videoDecoderEndEvent = callbackEvent;
	_videoOffset = offset;
	_videoZ = zValue;
}

Common::SeekableReadStream *VideoRoom::openFile(const Common::String &name) {
	Common::SeekableReadStream *stream = _podFile->getFileStream(name);
	if (stream)
		return stream;
	return g_vm->getWdPodFile()->getFileStream(name);
}

Common::String VideoRoom::mapAsset(const Common::String &name) {
	Common::String mappedName = _assetMap.get(name, 1);
	if (mappedName == "") {
		return name;
	}
	return mappedName;
}

Common::String VideoRoom::mapAsset(const LayerId &name) {
	return mapAsset(name.getFilename());
}

Audio::RewindableAudioStream *VideoRoom::getAudioStream(const Common::String &soundName) {
	Common::SeekableReadStream *stream = openFile(
		mapAsset(soundName) + ".aif");
	if (!stream) {
		debug("Audio %s isn't found", soundName.c_str());
		return nullptr;
	}
	return Audio::makeAIFFStream(stream, DisposeAfterUse::YES);
}

void VideoRoom::playSoundInternal(const Common::String &soundName, EventHandlerWrapper callbackEvent, bool loop,
				  bool skippable, Audio::Mixer::SoundType soundType, int subtitleID) {
	Audio::RewindableAudioStream *rewSoundStream;
	Audio::AudioStream *soundStream;
	Animation anim;
	g_system->getMixer()->stopHandle(anim._soundHandle);
	rewSoundStream = getAudioStream(soundName);
	soundStream = loop ? Audio::makeLoopingAudioStream(rewSoundStream, 0) : rewSoundStream;
	anim._animName = soundName;
	anim._callbackEvent = callbackEvent;
	anim._finished = false;
	anim._keepLastFrame = false;
	anim._skippable = skippable;
	anim._subtitleID = subtitleID;
	g_system->getMixer()->playStream(soundType, &anim._soundHandle, soundStream,
					 -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::YES);
	_anims.push_back(anim);
}

void VideoRoom::playSFX(const Common::String &soundName, EventHandlerWrapper callbackEvent) {
	playSoundInternal(soundName, callbackEvent, false, false, Audio::Mixer::kSFXSoundType);
}

void VideoRoom::playMusic(const Common::String &soundName, EventHandlerWrapper callbackEvent) {
	playSoundInternal(soundName, callbackEvent, false, false, Audio::Mixer::kMusicSoundType);
}

void VideoRoom::playSpeech(const TranscribedSound &sound,
				    EventHandlerWrapper callbackEvent) {
	int subID = g_vm->genSubtitleID();
	playSoundInternal(sound.soundName, callbackEvent, false, true, Audio::Mixer::kSpeechSoundType, subID);
	playSubtitles(sound.transcript, subID);
}

void VideoRoom::playSFXLoop(const Common::String &soundName) {
	playSoundInternal(soundName, EventHandlerWrapper(), true, false, Audio::Mixer::kSFXSoundType);
}

void VideoRoom::playMusicLoop(const Common::String &soundName) {
	playSoundInternal(soundName, EventHandlerWrapper(), true, false, Audio::Mixer::kMusicSoundType);
}

void VideoRoom::playAnimWithSoundInternal(const LayerId &animName,
					  const Common::String &soundName,
					  Audio::Mixer::SoundType soundType,
					  int zValue,
					  PlayAnimParams params,
					  EventHandlerWrapper callbackEvent,
					  Common::Point offset, int subtitleID) {
	Audio::AudioStream *soundStream;

	if (!doesLayerExist(animName)) {
		addAnimLayerInternal(animName, zValue);
	}

	startAnimationInternal(animName, zValue, params.getSpeed(), params.isLoop(),
			       false, params.getFirstFrame(), params.getLastFrame(), offset);
	Animation anim;
	g_system->getMixer()->stopHandle(anim._soundHandle);
	if (params.isLoop()) {
		soundStream = Audio::makeLoopingAudioStream(getAudioStream(soundName), 0);
	} else {
		soundStream = getAudioStream(soundName);
	}
	anim._animName = animName;
	anim._callbackEvent = callbackEvent;
	anim._finished = false;
	anim._keepLastFrame = params.getKeepLastFrame();
	anim._skippable = false;
	anim._subtitleID = subtitleID;
	g_system->getMixer()->playStream(soundType, &anim._soundHandle, soundStream,
					 -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::YES);
	_anims.push_back(anim);
}

void VideoRoom::playAnimWithSpeech(const LayerId &animName,
				   const TranscribedSound &sound,
				   int zValue,
				   PlayAnimParams params,
				   EventHandlerWrapper callbackEvent,
				   Common::Point offset) {
	int subID = g_vm->genSubtitleID();
	playAnimWithSoundInternal(animName, sound.soundName, Audio::Mixer::kSpeechSoundType, zValue, params, callbackEvent, offset, subID);
	playSubtitles(sound.transcript, subID);
}

void VideoRoom::playAnimWithSFX(const LayerId &animName,
				const Common::String &soundName,
				int zValue,
				PlayAnimParams params,
				EventHandlerWrapper callbackEvent,
				Common::Point offset) {
	playAnimWithSoundInternal(animName, soundName, Audio::Mixer::kSFXSoundType, zValue, params, callbackEvent, offset);
}

void VideoRoom::playAnimWithMusic(const LayerId &animName,
				  const Common::String &soundName,
				  int zValue,
				  PlayAnimParams params,
				  EventHandlerWrapper callbackEvent,
				  Common::Point offset) {
	playAnimWithSoundInternal(animName, soundName, Audio::Mixer::kMusicSoundType, zValue, params, callbackEvent, offset);
}

void VideoRoom::playAnim(const LayerId &animName, int zValue,
			 PlayAnimParams params,
			 EventHandlerWrapper callbackEvent,
			 Common::Point offset) {
	if (!doesLayerExist(animName)) {
		addAnimLayerInternal(animName, zValue);
	}

	startAnimationInternal(animName, zValue, params.getSpeed(), params.isLoop(), false,
			       params.getFirstFrame(), params.getLastFrame(), offset);
	Animation anim;
	anim._callbackEvent = callbackEvent;
	anim._finished = false;
	anim._keepLastFrame = params.getKeepLastFrame();
	anim._animName = animName;
	anim._skippable = false;
	_anims.push_back(anim);
}

void VideoRoom::playAnimLoop(const LayerId &animName, int zValue, Common::Point offset) {
	playAnim(animName, zValue, PlayAnimParams::loop(), EventHandlerWrapper(), offset);
}

void VideoRoom::playAnimKeepLastFrame(const LayerId &animName, int zValue, EventHandlerWrapper callbackEvent,
				      Common::Point offset) {
	playAnim(animName, zValue, PlayAnimParams::keepLastFrame(), callbackEvent, offset);
}

void VideoRoom::setUserPanCallback(EventHandlerWrapper leftStart,
				   EventHandlerWrapper leftEnd,
				   EventHandlerWrapper rightStart,
				   EventHandlerWrapper rightEnd) {
	_userPanStartLeftCallback = leftStart;
	_userPanEndLeftCallback = leftEnd;
	_userPanStartRightCallback = rightStart;
	_userPanEndRightCallback = rightEnd;
}

class StatuePlayEnd : public EventHandler {
public:
	void operator()() override {
		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
		room->playAnim(_animName, _zValue,
			       PlayAnimParams::disappear().partial(_lastLoopFrame, -1),
			       EventHandlerWrapper(), _offset);
		room->enableMouse();
	}

	StatuePlayEnd(const LayerId &animName, int zValue, Common::Point offset, int lastLoopFrame) {
		_animName = animName;
		_zValue = zValue;
		_offset = offset;
		_lastLoopFrame = lastLoopFrame;
	}
private:
	LayerId _animName;
	int _zValue;
	int _lastLoopFrame;
	Common::Point _offset;
};

class StatuePlayTwoThirdsLoop : public EventHandler {
public:
	void operator()() override {
		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
		room->playAnim(_animName, _zValue,
			       PlayAnimParams::loop().partial(_firstLoopFrame, _lastLoopFrame),
			       EventHandlerWrapper(), _offset);
	}

	StatuePlayTwoThirdsLoop(const LayerId &animName, int zValue, Common::Point offset,
				int firstLoopFrame, int lastLoopFrame) {
		_animName = animName;
		_zValue = zValue;
		_offset = offset;
		_firstLoopFrame = firstLoopFrame;
		_lastLoopFrame = lastLoopFrame;
	}
private:
	LayerId _animName;
	int _zValue;
	int _firstLoopFrame, _lastLoopFrame;
	Common::Point _offset;
};

void VideoRoom::playStatueSMK(StatueId id, const LayerId &animName, int zValue,
			      const Common::Array<Common::String> &smkNames,
			      int startOfLoop, int startOfEnd,
			      Common::Point offset) {
	int phase = g_vm->getPersistent()->_statuePhase[id] % smkNames.size();
	playVideo(smkNames[phase], zValue,
		  Common::SharedPtr<EventHandler>(new StatuePlayEnd(animName, zValue, offset, startOfEnd)));
	if (!doesLayerExist(animName)) {
		addAnimLayerInternal(animName, zValue);
		stopAnim(animName);
	}
	playAnim(animName, zValue, PlayAnimParams::keepLastFrame().partial(0, startOfEnd - 1),
		 Common::SharedPtr<EventHandler>(new StatuePlayTwoThirdsLoop(animName, zValue, offset, startOfLoop, startOfEnd - 1)), offset);

	g_vm->getPersistent()->_statuesTouched[id] = true;
	g_vm->getPersistent()->_statuePhase[id] = (phase + 1) % smkNames.size();
	disableMouse();
}

void VideoRoom::resetFade() {
	_finalFade = 0x100;
	_finalFadeSpeed = 0;
	_finalFadeCallback = EventHandlerWrapper();
}

void VideoRoom::resetLayers() {
	_layers.clear();
}

void VideoRoom::hideString(const Common::String &font, size_t maxLen, const Common::String &extraId) {
	Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
	for (unsigned i = 0; i < maxLen; i++) {
		room->stopAnim(LayerId(font, i, extraId));
	}
}

void VideoRoom::renderString(const Common::String &font, const Common::U32String &str, Common::Point startPos, int zVal, int fontDelta, const Common::String &extraId) {
	Common::Point curPos = startPos;
	bool small = font == "smallascii";
	for (unsigned i = 0; i < str.size(); i++) {
		uint32 c = str[i];
		if (c == ' ') {
			curPos += Common::Point(small ? 6 : 20, 0);
			continue;
		}
		LayerId l(font, i, extraId);
		selectFrame(l, zVal, c + fontDelta, curPos);
		PodImage pi(getLayerFrame(l));
		curPos += Common::Point(pi.getWidth() + pi.getOffset().x + (small ? 1 : 3), 0);
	}
}

void VideoRoom::loadFontWidth(const Common::String &font) {
	if (_fontWidths.contains(font)) {
		return;
	}

	Common::SharedPtr<Common::SeekableReadStream> rs(openFile(mapAsset(font) + ".pod"));
	if (!rs) {
		_fontWidths[font].clear();
		debug("Animation %s isn't found", font.c_str());
		return;
	}

	PodFile pf2(font);
	pf2.openStore(rs);

	Common::Array <PodImage> pi = pf2.loadImageArray();
	bool small = font == "smallascii";

	for (unsigned i = 0; i < pi.size(); i++)
		_fontWidths[font].push_back(pi[i].getWidth()+pi[i].getOffset().x + (small ? 1 : 3));
}

int VideoRoom::computeStringWidth(const Common::String &font, const Common::U32String &str, int fontDelta) {
	int width = 0;
	bool small = font == "smallascii";
	if (!_fontWidths.contains(font)) {
		loadFontWidth(font);
	}
	for (unsigned i = 0; i < str.size(); i++) {
		uint32 c = str[i];
		if (c == ' ') {
			width += small ? 6 : 20;
			continue;
		}
		if (c + fontDelta < _fontWidths[font].size())
			width += _fontWidths[font][c + fontDelta];
	}

	return width;
}

void VideoRoom::renderStringCentered(const Common::String &font, const Common::U32String &str, Common::Point centerPos, int zVal, int fontDelta, const Common::String &extraId) {
	int width = computeStringWidth(font, str, fontDelta);
	renderString(font, str, centerPos - Common::Point(width / 2, 0), zVal, fontDelta, extraId);
}

void VideoRoom::playSubtitles(const char *text, int subID) {
	int delay = g_vm->getSubtitleDelayPerChar();
	if (delay <= 0)
		return;
	Common::U32String s = g_vm->translate(text);
	Common::Array<Common::U32String> lines;
	int32 countTime = g_vm->getCurrentTime();
	g_vm->wrapSubtitles(s, lines);
	for (uint i = 0; i < lines.size(); i++) {
		SubtitleLine l;
		l.line = lines[i];
		l.ID = subID;
		countTime += delay * MAX<uint>(l.line.size(), 20);
		l.maxTime = countTime;
		_subtitles.push(l);
		_countQueuedSubtitles[subID]++;
	}
}
}
