/* 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.
 *
 */

// Player for Kyrandia 3 VQA movies, based on the information found at
// http://multimedia.cx/VQA_INFO.TXT
//
// The benchl.vqa movie (or whatever it is) is not supported. It does not have
// a FINF chunk.
//
// The jung2.vqa movie does work, but only thanks to a grotesque hack.

#include "engines/kyra/kyra_v1.h"
#include "engines/kyra/vqa.h"
#include "engines/kyra/screen.h"

#include "audio/audiostream.h"
#include "audio/decoders/raw.h"

#include "common/system.h"
#include "common/events.h"

#include "graphics/palette.h"
#include "graphics/surface.h"

namespace Kyra {

static uint32 readTag(Common::SeekableReadStream *stream) {
	// Some tags have to be on an even offset, so they are padded with a
	// zero byte. Skip that.

	uint32 tag = stream->readUint32BE();

	if (stream->eos())
		return 0;

	if (!(tag & 0xFF000000)) {
		tag = (tag << 8) | stream->readByte();
	}

	return tag;
}

VQADecoder::VQADecoder() {
	memset(&_header, 0, sizeof(_header));
}

VQADecoder::~VQADecoder() {
	close();
	delete[] _frameInfo;
}

bool VQADecoder::loadStream(Common::SeekableReadStream *stream) {
	close();
	_fileStream = stream;

	if (_fileStream->readUint32BE() != MKTAG('F','O','R','M')) {
		warning("VQADecoder::loadStream(): Cannot find `FORM' tag");
		return false;
	}

	// Ignore the size of the FORM chunk. We're only interested in its
	// children.
	_fileStream->readUint32BE();

	if (_fileStream->readUint32BE() != MKTAG('W','V','Q','A')) {
		warning("VQADecoder::loadStream(): Cannot find `WVQA' tag");
		return false;
	}

	// We want to find both a VQHD chunk containing the header, and a FINF
	// chunk containing the frame offsets.

	bool foundVQHD = false;
	bool foundFINF = false;

	VQAAudioTrack *audioTrack = NULL;

	// The information we need is stored in two chunks: VQHD and FINF. We
	// need both of them before we can begin decoding the movie.

	while (!foundVQHD || !foundFINF) {
		uint32 tag = readTag(stream);
		uint32 size = _fileStream->readUint32BE();

		switch (tag) {
		case MKTAG('V','Q','H','D'):
			handleVQHD(_fileStream);
			if (_header.flags & 1) {
				audioTrack = new VQAAudioTrack(&_header);
				addTrack(audioTrack);
			}
			foundVQHD = true;
			break;
		case MKTAG('F','I','N','F'):
			if (!foundVQHD) {
				warning("VQADecoder::loadStream(): Found `FINF' before `VQHD'");
				return false;
			}
			if (size != 4 * getFrameCount()) {
				warning("VQADecoder::loadStream(): Expected size %d for `FINF' chunk, but got %u", 4 * getFrameCount(), size);
				return false;
			}
			handleFINF(_fileStream);
			foundFINF = true;
			break;
		default:
			warning("VQADecoder::loadStream(): Unknown tag `%s'", tag2str(tag));
			_fileStream->seek(size, SEEK_CUR);
			break;
		}
	}

	return true;
}

void VQADecoder::handleVQHD(Common::SeekableReadStream *stream) {
	_header.version     = stream->readUint16LE();
	_header.flags       = stream->readUint16LE();
	_header.numFrames   = stream->readUint16LE();
	_header.width       = stream->readUint16LE();
	_header.height      = stream->readUint16LE();
	_header.blockW      = stream->readByte();
	_header.blockH      = stream->readByte();
	_header.frameRate   = stream->readByte();
	_header.cbParts     = stream->readByte();
	_header.colors      = stream->readUint16LE();
	_header.maxBlocks   = stream->readUint16LE();
	_header.unk1        = stream->readUint32LE();
	_header.unk2        = stream->readUint16LE();
	_header.freq        = stream->readUint16LE();
	_header.channels    = stream->readByte();
	_header.bits        = stream->readByte();
	_header.unk3        = stream->readUint32LE();
	_header.unk4        = stream->readUint16LE();
	_header.maxCBFZSize = stream->readUint32LE();
	_header.unk5        = stream->readUint32LE();

	_frameInfo = new uint32[_header.numFrames + 1];

	VQAVideoTrack *videoTrack = new VQAVideoTrack(&_header);
	addTrack(videoTrack);

	// Kyrandia 3 uses version 1 VQA files, and is the only known game to
	// do so. This version of the format has some implicit default values.

	if (_header.version == 1) {
		if (_header.freq == 0)
			_header.freq = 22050;
		if (_header.channels == 0)
			_header.channels = 1;
		if (_header.bits == 0)
			_header.bits = 8;
	}

	if (_header.flags & 1) {
		// Kyrandia 3 uses 8-bit sound, and so far testing indicates
		// that it's all mono.
		//
		// This is good, because it means we won't have to worry about
		// the confusing parts of the VQA spec, where 8- and 16-bit
		// data have different signedness and stereo sample layout
		// varies between different games.

		assert(_header.bits == 8);
		assert(_header.channels == 1);
	}
}

void VQADecoder::handleFINF(Common::SeekableReadStream *stream) {
	for (int i = 0; i < _header.numFrames; i++) {
		_frameInfo[i] = 2 * stream->readUint32LE();
	}

	// HACK: This flag is set in jung2.vqa, and its purpose - if it has
	// one - is currently unknown. It can't be a general purpose flag,
	// because in large movies the frame offset can be large enough to
	// set this flag, though of course never for the first frame.
	//
	// At least in my copy of Kyrandia 3, _frameInfo[0] is 0x81000098, and
	// the desired index is 0x4716. So the value should be 0x80004716, but
	// I don't want to hard-code it. Instead, scan the file for the offset
	// to the first VQFR chunk.

	if (_frameInfo[0] & 0x01000000) {
		uint32 oldPos = stream->pos();

		while (1) {
			uint32 scanTag = readTag(stream);
			uint32 scanSize = stream->readUint32BE();

			if (stream->eos())
				break;

			if (scanTag == MKTAG('V','Q','F','R')) {
				_frameInfo[0] = (stream->pos() - 8) | 0x80000000;
				break;
			}

			stream->seek(scanSize, SEEK_CUR);
		}

		stream->seek(oldPos);
	}

	_frameInfo[_header.numFrames] = 0x7FFFFFFF;
}

void VQADecoder::readNextPacket() {
	VQAVideoTrack *videoTrack = (VQAVideoTrack *)getTrack(0);
	VQAAudioTrack *audioTrack = (VQAAudioTrack *)getTrack(1);

	assert(videoTrack);

	int curFrame = videoTrack->getCurFrame();

	// Stop if reading the tag is enough to put us ahead of the next frame
	int32 end = (_frameInfo[curFrame + 1] & 0x7FFFFFFF) - 7;

	// At this point, we probably only need to adjust for the offset in the
	// stream to be even. But we may as well do this to really make sure
	// we have the correct offset.
	if (curFrame >= 0) {
		_fileStream->seek(_frameInfo[curFrame] & 0x7FFFFFFF);
		if (_frameInfo[curFrame] & 0x80000000) {
			videoTrack->setHasDirtyPalette();
		}
	}

	while (!_fileStream->eos() && _fileStream->pos() < end) {
		uint32 tag = readTag(_fileStream);
		uint32 size;

		switch (tag) {
		case MKTAG('S','N','D','0'):	// Uncompressed sound
			assert(audioTrack);
			audioTrack->handleSND0(_fileStream);
			break;
		case MKTAG('S','N','D','1'):	// Compressed sound, almost like AUD
			assert(audioTrack);
			audioTrack->handleSND1(_fileStream);
			break;
		case MKTAG('S','N','D','2'):	// Compressed sound
			assert(audioTrack);
			audioTrack->handleSND2(_fileStream);
			break;
		case MKTAG('V','Q','F','R'):
			videoTrack->handleVQFR(_fileStream);
			break;
		case MKTAG('C','M','D','S'):
			// The purpose of this is unknown, but it's known to
			// exist so don't warn about it.
			size = _fileStream->readUint32BE();
			_fileStream->seek(size, SEEK_CUR);
			break;
		default:
			warning("VQADecoder::readNextPacket(): Unknown tag `%s'", tag2str(tag));
			size = _fileStream->readUint32BE();
			_fileStream->seek(size, SEEK_CUR);
			break;
		}
	}
}

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

VQADecoder::VQAAudioTrack::VQAAudioTrack(const VQAHeader *header) {
	_audioStream = Audio::makeQueuingAudioStream(header->freq, false);
}

VQADecoder::VQAAudioTrack::~VQAAudioTrack() {
	delete _audioStream;
}

Audio::AudioStream *VQADecoder::VQAAudioTrack::getAudioStream() const {
	return _audioStream;
}

void VQADecoder::VQAAudioTrack::handleSND0(Common::SeekableReadStream *stream) {
	uint32 size = stream->readUint32BE();
	byte *buf = (byte *)malloc(size);
	stream->read(buf, size);
	_audioStream->queueBuffer(buf, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
}

void VQADecoder::VQAAudioTrack::handleSND1(Common::SeekableReadStream *stream) {
	stream->readUint32BE();
	uint16 outsize = stream->readUint16LE();
	uint16 insize = stream->readUint16LE();
	byte *inbuf = (byte *)malloc(insize);

	stream->read(inbuf, insize);

	if (insize == outsize) {
		_audioStream->queueBuffer(inbuf, insize, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
	} else {
		const int8 WSTable2Bit[] = { -2, -1, 0, 1 };
		const int8 WSTable4Bit[] = {
			-9, -8, -6, -5, -4, -3, -2, -1,
			 0,  1,  2,  3,  4,  5,  6,  8
		};

		byte *outbuf = (byte *)malloc(outsize);
		byte *in = inbuf;
		byte *out = outbuf;
		int16 curSample = 0x80;
		uint16 bytesLeft = outsize;

		while (bytesLeft > 0) {
			uint16 input = *in++ << 2;
			byte code = (input >> 8) & 0xFF;
			int8 count = (input & 0xFF) >> 2;
			int i;

			switch (code) {
			case 2:
				if (count & 0x20) {
					/* NOTE: count is signed! */
					count <<= 3;
					curSample += (count >> 3);
					*out++ = curSample;
					bytesLeft--;
				} else {
					for (; count >= 0; count--) {
						*out++ = *in++;
						bytesLeft--;
					}
					curSample = *(out - 1);
				}
				break;
			case 1:
				for (; count >= 0; count--) {
					code = *in++;

					for (i = 0; i < 2; i++) {
						curSample += WSTable4Bit[code & 0x0F];
						curSample = CLIP<int16>(curSample, 0, 255);
						code >>= 4;
						*out++ = curSample;
					}

					bytesLeft -= 2;
				}
				break;
			case 0:
				for (; count >= 0; count--) {
					code = *in++;

					for (i = 0; i < 4; i++) {
						curSample += WSTable2Bit[code & 0x03];
						curSample = CLIP<int16>(curSample, 0, 255);
						code >>= 2;
						*out++ = curSample;
					}

					bytesLeft -= 4;
				}
				break;
			default:
				for (; count >= 0; count--) {
					*out++ = curSample;
					bytesLeft--;
				}
				break;
			}
		}
		_audioStream->queueBuffer(outbuf, outsize, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
		free(inbuf);
	}
}

void VQADecoder::VQAAudioTrack::handleSND2(Common::SeekableReadStream *stream) {
	uint32 size = stream->readUint32BE();
	warning("VQADecoder::VQAAudioTrack::handleSND2(): `SND2' is not implemented");
	stream->seek(size, SEEK_CUR);
}

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

VQADecoder::VQAVideoTrack::VQAVideoTrack(const VQAHeader *header) {
	memset(_palette, 0, sizeof(_palette));
	_dirtyPalette = false;

	_width = header->width;
	_height = header->height;
	_blockW = header->blockW;
	_blockH = header->blockH;
	_cbParts = header->cbParts;

	_newFrame = false;

	_curFrame = -1;
	_frameCount = header->numFrames;
	_frameRate = header->frameRate;

	_codeBookSize = 0xF00 * header->blockW * header->blockH;
	_compressedCodeBook = false;
	_codeBook = new byte[_codeBookSize];
	_partialCodeBookSize = 0;
	_numPartialCodeBooks = 0;
	_partialCodeBook = new byte[_codeBookSize];
	_numVectorPointers = (header->width / header->blockW) * (header->height * header->blockH);
	_vectorPointers = new uint16[_numVectorPointers];

	memset(_codeBook, 0, _codeBookSize);
	memset(_partialCodeBook, 0, _codeBookSize);
	memset(_vectorPointers, 0, _numVectorPointers);

	_surface = new Graphics::Surface();
	_surface->create(header->width, header->height, Graphics::PixelFormat::createFormatCLUT8());
}

VQADecoder::VQAVideoTrack::~VQAVideoTrack() {
	_surface->free();
	delete _surface;
	delete[] _codeBook;
	delete[] _partialCodeBook;
	delete[] _vectorPointers;
}

uint16 VQADecoder::VQAVideoTrack::getWidth() const {
	return _width;
}

uint16 VQADecoder::VQAVideoTrack::getHeight() const {
	return _height;
}

Graphics::PixelFormat VQADecoder::VQAVideoTrack::getPixelFormat() const {
	return _surface->format;
}

int VQADecoder::VQAVideoTrack::getCurFrame() const {
	return _curFrame;
}

int VQADecoder::VQAVideoTrack::getFrameCount() const {
	return _frameCount;
}

Common::Rational VQADecoder::VQAVideoTrack::getFrameRate() const {
	return _frameRate;
}

void VQADecoder::VQAVideoTrack::setHasDirtyPalette() {
	_dirtyPalette = true;
}

bool VQADecoder::VQAVideoTrack::hasDirtyPalette() const {
	return _dirtyPalette;
}

const byte *VQADecoder::VQAVideoTrack::getPalette() const {
	_dirtyPalette = false;
	return _palette;
}

const Graphics::Surface *VQADecoder::VQAVideoTrack::decodeNextFrame() {
	if (_newFrame) {
		_newFrame = false;

		int blockPitch = _width / _blockW;

		for (int by = 0; by < _height / _blockH; by++) {
			for (int bx = 0; bx < blockPitch; bx++) {
				byte *dst = (byte *)_surface->getBasePtr(bx * _blockW, by * _blockH);
				int val = _vectorPointers[by * blockPitch + bx];
				int i;

				if ((val & 0xFF00) == 0xFF00) {
					// Solid color
					for (i = 0; i < _blockH; i++) {
						memset(dst, 255 - (val & 0xFF), _blockW);
						dst += _width;
					}
				} else {
					// Copy data from _vectorPointers. I'm not sure
					// why we don't use the three least significant
					// bits of 'val'.
					byte *src = &_codeBook[(val >> 3) * _blockW * _blockH];

					for (i = 0; i < _blockH; i++) {
						memcpy(dst, src, _blockW);
						src += _blockW;
						dst += _width;
					}
				}
			}
		}

		if (_numPartialCodeBooks == _cbParts) {
			if (_compressedCodeBook) {
				Screen::decodeFrame4(_partialCodeBook, _codeBook, _codeBookSize);
			} else {
				memcpy(_codeBook, _partialCodeBook, _partialCodeBookSize);
			}
			_numPartialCodeBooks = 0;
			_partialCodeBookSize = 0;
		}
	}

	_curFrame++;
	return _surface;
}

void VQADecoder::VQAVideoTrack::handleVQFR(Common::SeekableReadStream *stream) {
	uint32 size = stream->readUint32BE();
	int32 end = stream->pos() + size - 8;
	byte *inbuf;

	_newFrame = true;

	while (stream->pos() < end) {
		uint32 tag = readTag(stream);
		uint32 i;
		size = stream->readUint32BE();

		switch (tag) {
		case MKTAG('C','B','F','0'):	// Full codebook
			stream->read(_codeBook, size);
			break;
		case MKTAG('C','B','F','Z'):	// Full codebook
			inbuf = (byte *)malloc(size);
			stream->read(inbuf, size);
			Screen::decodeFrame4(inbuf, _codeBook, _codeBookSize);
			free(inbuf);
			break;
		case MKTAG('C','B','P','0'):	// Partial codebook
			_compressedCodeBook = false;
			stream->read(_partialCodeBook + _partialCodeBookSize, size);
			_partialCodeBookSize += size;
			_numPartialCodeBooks++;
			break;
		case MKTAG('C','B','P','Z'):	// Partial codebook
			_compressedCodeBook = true;
			stream->read(_partialCodeBook + _partialCodeBookSize, size);
			_partialCodeBookSize += size;
			_numPartialCodeBooks++;
			break;
		case MKTAG('C','P','L','0'):	// Palette
			assert(size <= 3 * 256);
			stream->read(_palette, size);
			break;
		case MKTAG('C','P','L','Z'):	// Palette
			inbuf = (byte *)malloc(size);
			stream->read(inbuf, size);
			Screen::decodeFrame4(inbuf, _palette, 3 * 256);
			free(inbuf);
			break;
		case MKTAG('V','P','T','0'):	// Frame data
			assert(size / 2 <= _numVectorPointers);
			for (i = 0; i < size / 2; i++)
				_vectorPointers[i] = stream->readUint16LE();
			break;
		case MKTAG('V','P','T','Z'):	// Frame data
			inbuf = (byte *)malloc(size);
			stream->read(inbuf, size);
			size = Screen::decodeFrame4(inbuf, (uint8 *)_vectorPointers, 2 * _numVectorPointers);
			for (i = 0; i < size / 2; i++)
				_vectorPointers[i] = TO_LE_16(_vectorPointers[i]);
			free(inbuf);
			break;
		default:
			warning("VQADecoder::VQAVideoTrack::handleVQFR(): Unknown `VQFR' sub-tag `%s'", tag2str(tag));
			stream->seek(size, SEEK_CUR);
			break;
		}
	}
}

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

VQAMovie::VQAMovie(KyraEngine_v1 *vm, OSystem *system) {
	_system = system;
	_vm = vm;
	_screen = _vm->screen();
	_decoder = new VQADecoder();
}

VQAMovie::~VQAMovie() {
	close();
	delete _decoder;
}

bool VQAMovie::open(const char *filename) {
	if (_file.open(filename)) {
		return true;
	}
	return false;
}

void VQAMovie::close() {
	if (_file.isOpen()) {
		_file.close();
	}
}

void VQAMovie::play() {
	if (_decoder->loadStream(&_file)) {
		Common::EventManager *eventMan = _vm->getEventManager();
		int width = _decoder->getWidth();
		int height = _decoder->getHeight();
		int x = (Screen::SCREEN_W - width) / 2;
		int y = (Screen::SCREEN_H - height) / 2;

		_decoder->start();

		// Note that decoding starts at frame -1. That's because there
		// is usually sound data before the first frame, probably to
		// avoid sound underflow.

		while (!_decoder->endOfVideo()) {
			Common::Event event;
			while (eventMan->pollEvent(event)) {
				switch (event.type) {
				case Common::EVENT_KEYDOWN:
					if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
						return;
					break;
				case Common::EVENT_RTL:
				case Common::EVENT_QUIT:
					return;
				default:
					break;
				}
			}

			if (_decoder->needsUpdate()) {
				const Graphics::Surface *surface = _decoder->decodeNextFrame();
				if (_decoder->hasDirtyPalette()) {
					const byte *decoderPalette = _decoder->getPalette();
					byte systemPalette[256 * 3];
					for (int i = 0; i < ARRAYSIZE(systemPalette); i++) {
						systemPalette[i] = (decoderPalette[i] * 0xFF) / 0x3F;
					}
					_system->getPaletteManager()->setPalette(systemPalette, 0, 256);
				}

				_system->copyRectToScreen((const byte *)surface->getBasePtr(0, 0), surface->pitch, x, y, width, height);
			}

			_system->updateScreen();
			_system->delayMillis(10);
		}
	}
}

} // End of namespace Kyra
