/* 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.
 *
 * $URL: https://scummvm.svn.sourceforge.net/svnroot/scummvm/scummvm/tags/release-0-12-0/engines/kyra/vqa.cpp $
 * $Id: vqa.cpp 32152 2008-05-17 15:12:20Z lordhoto $
 *
 */

// 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 "common/events.h"
#include "common/system.h"
#include "sound/audiostream.h"
#include "sound/mixer.h"
#include "kyra/sound.h"
#include "kyra/screen.h"
#include "kyra/vqa.h"

namespace Kyra {

VQAMovie::VQAMovie(KyraEngine_v1 *vm, OSystem *system) {
	_system = system;
	_vm = vm;
	_opened = false;
	_x = _y = _drawPage = -1;
}

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

void VQAMovie::initBuffers() {
	for (int i = 0; i < ARRAYSIZE(_buffers); i++) {
		_buffers[i].data = 0;
		_buffers[i].size = 0;
	}
}

void *VQAMovie::allocBuffer(int num, uint32 size) {
	assert(num >= 0 && num < ARRAYSIZE(_buffers));
	assert(size > 0);

	if (size > _buffers[num].size) {
		/*
		 * We could use realloc() here, but we don't actually need the
		 * old contents of the buffer.
		 */
		delete[] _buffers[num].data;
		_buffers[num].data = new uint8[size];
		_buffers[num].size = size;
	}

	assert(_buffers[num].data);

	return _buffers[num].data;
}

void VQAMovie::freeBuffers() {
	for (int i = 0; i < ARRAYSIZE(_buffers); i++) {
		delete[] _buffers[i].data;
		_buffers[i].data = NULL;
		_buffers[i].size = 0;
	}
}

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

	uint32 tag = _file.readUint32BE();

	if (!(tag & 0xFF000000)) {
		tag = (tag << 8) | _file.readByte();
	}

	return tag;
}

void VQAMovie::decodeSND1(byte *inbuf, uint32 insize, byte *outbuf, uint32 outsize) {
	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 code;
	int8 count;
	uint16 input;

	int16 curSample = 0x80;

	while (outsize > 0) {
		input = *inbuf++ << 2;
		code = (input >> 8) & 0xff;
		count = (input & 0xff) >> 2;

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

				curSample += WSTable4Bit[code & 0x0f];
				curSample = CLIP<int16>(curSample, 0, 255);
				*outbuf++ = curSample;

				curSample += WSTable4Bit[code >> 4];
				curSample = CLIP<int16>(curSample, 0, 255);
				*outbuf++ = curSample;

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

				curSample += WSTable2Bit[code & 0x03];
				curSample = CLIP<int16>(curSample, 0, 255);
				*outbuf++ = curSample;

				curSample += WSTable2Bit[(code >> 2) & 0x03];
				curSample = CLIP<int16>(curSample, 0, 255);
				*outbuf++ = curSample;

				curSample += WSTable2Bit[(code >> 4) & 0x03];
				curSample = CLIP<int16>(curSample, 0, 255);
				*outbuf++ = curSample;

				curSample += WSTable2Bit[(code >> 6) & 0x03];
				curSample = CLIP<int16>(curSample, 0, 255);
				*outbuf++ = curSample;

				outsize -= 4;
			}
			break;
		default:
			for (; count >= 0; count--) {
				*outbuf++ = curSample;
				outsize--;
			}
		}
	}
}

bool VQAMovie::open(const char *filename) {
	debugC(9, kDebugLevelMovie, "VQAMovie::open('%s')", filename);
	close();

	if (!_file.open(filename))
		return false;

	if (_file.readUint32BE() != MKID_BE('FORM')) {
		warning("VQAMovie::open: Cannot find `FORM' tag");
		return false;
	}

	// For now, we ignore the size of the FORM chunk.
	_file.readUint32BE();

	if (_file.readUint32BE() != MKID_BE('WVQA')) {
		warning("WQAMovie::open: Cannot find `WVQA' tag");
		return false;
	}

	bool foundHeader = false;
	bool foundFrameInfo = false;

	// 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 (!foundHeader || !foundFrameInfo) {
		uint32 tag = readTag();
		uint32 size = _file.readUint32BE();

		switch (tag) {
		case MKID_BE('VQHD'):	// VQA header
			_header.version     = _file.readUint16LE();
			_header.flags       = _file.readUint16LE();
			_header.numFrames   = _file.readUint16LE();
			_header.width       = _file.readUint16LE();
			_header.height      = _file.readUint16LE();
			_header.blockW      = _file.readByte();
			_header.blockH      = _file.readByte();
			_header.frameRate   = _file.readByte();
			_header.cbParts     = _file.readByte();
			_header.colors      = _file.readUint16LE();
			_header.maxBlocks   = _file.readUint16LE();
			_header.unk1        = _file.readUint32LE();
			_header.unk2        = _file.readUint16LE();
			_header.freq        = _file.readUint16LE();
			_header.channels    = _file.readByte();
			_header.bits        = _file.readByte();
			_header.unk3        = _file.readUint32LE();
			_header.unk4        = _file.readUint16LE();
			_header.maxCBFZSize = _file.readUint32LE();
			_header.unk5        = _file.readUint32LE();

			// 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;
			}

			setX((Screen::SCREEN_W - _header.width) / 2);
			setY((Screen::SCREEN_H - _header.height) / 2);

			_frameInfo = new uint32[_header.numFrames];
			_frame = new byte[_header.width * _header.height];

			_codeBookSize = 0xf00 * _header.blockW * _header.blockH;
			_codeBook = new byte[_codeBookSize];
			_partialCodeBook = new byte[_codeBookSize];
			memset(_codeBook, 0, _codeBookSize);
			memset(_partialCodeBook, 0, _codeBookSize);

			_numVectorPointers = (_header.width / _header.blockW) * (_header.height * _header.blockH);
			_vectorPointers = new uint16[_numVectorPointers];
			memset(_vectorPointers, 0, _numVectorPointers * sizeof(uint16));

			_partialCodeBookSize = 0;
			_numPartialCodeBooks = 0;

			if (_header.flags & 1) {
				// This VQA movie has sound. 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);

				_stream = Audio::makeAppendableAudioStream(_header.freq, Audio::Mixer::FLAG_UNSIGNED);
			} else {
				_stream = NULL;
			}

			foundHeader = true;
			break;

		case MKID_BE('FINF'):	// Frame info
			if (!foundHeader) {
				warning("VQAMovie::open: Found `FINF' before `VQHD'");
				return false;
			}

			if (size != 4 * (uint32)_header.numFrames) {
				warning("VQAMovie::open: Expected size %d for `FINF' chunk, but got %u", 4 * _header.numFrames, size);
				return false;
			}

			foundFrameInfo = true;

			for (int i = 0; i < _header.numFrames; i++) {
				_frameInfo[i] = 2 * _file.readUint32LE();
			}

			// HACK: This flag is set in jung2.vqa, and its
			// purpose, if it has one, is unknown. It can't be a
			// general purpose flag, because in large movies the
			// frame offsets 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 = _file.pos();

				while (1) {
					uint32 scanTag = readTag();
					uint32 scanSize = _file.readUint32BE();

					if (_file.eof())
						break;

					if (scanTag == MKID_BE('VQFR')) {
						_frameInfo[0] = (_file.pos() - 8) | 0x80000000;
						break;
					}

					_file.seek(scanSize, SEEK_CUR);
				}

				_file.seek(oldPos);
			}

			break;

		default:
			warning("VQAMovie::open: Unknown tag `%c%c%c%c'", char((tag >> 24) & 0xFF), char((tag >> 16) & 0xFF), char((tag >> 8) & 0xFF), char(tag & 0xFF));
			_file.seek(size, SEEK_CUR);
			break;
		}
	}

	initBuffers();

	_opened = true;
	return true;
}

void VQAMovie::close() {
	debugC(9, kDebugLevelMovie, "VQAMovie::close()");
	if (_opened) {
		delete[] _frameInfo;
		delete[] _frame;
		delete[] _codeBook;
		delete[] _partialCodeBook;
		delete[] _vectorPointers;

		if (_vm->_mixer->isSoundHandleActive(_sound))
			_vm->_mixer->stopHandle(_sound);

		_frameInfo = NULL;
		_frame = NULL;
		_codeBookSize = 0;
		_codeBook = NULL;
		_partialCodeBook = NULL;
		_vectorPointers = NULL;
		_stream = NULL;

		if (_file.isOpen())
			_file.close();

		freeBuffers();

		_opened = false;
	}
}

void VQAMovie::displayFrame(uint frameNum) {
	debugC(9, kDebugLevelMovie, "VQAMovie::displayFrame(%d)", frameNum);
	if (frameNum >= _header.numFrames || !_opened)
		return;

	bool foundSound = _stream ? false : true;
	bool foundFrame = false;
	uint i;

	_file.seek(_frameInfo[frameNum] & 0x7FFFFFFF);

	while (!foundSound || !foundFrame) {
		uint32 tag = readTag();
		uint32 size = _file.readUint32BE();

		if (_file.eof()) {
			// This happens at the last frame. Apparently it has
			// no sound?
			break;
		}

		byte *inbuf, *outbuf;
		uint32 insize, outsize;
		uint32 end;

		switch (tag) {
		case MKID_BE('SND0'):	// Uncompressed sound
			foundSound = true;
			inbuf = new byte[size];
			_file.read(inbuf, size);
			assert(_stream);
			_stream->queueBuffer(inbuf, size);
			break;

		case MKID_BE('SND1'):	// Compressed sound, almost like AUD
			foundSound = true;
			outsize = _file.readUint16LE();
			insize = _file.readUint16LE();

			inbuf = new byte[insize];
			_file.read(inbuf, insize);

			if (insize == outsize) {
				assert(_stream);
				_stream->queueBuffer(inbuf, insize);
			} else {
				outbuf = new byte[outsize];
				decodeSND1(inbuf, insize, outbuf, outsize);
				assert(_stream);
				_stream->queueBuffer(outbuf, outsize);
				delete[] inbuf;
			}
			break;

		case MKID_BE('SND2'):	// Compressed sound
			foundSound = true;
			warning("VQAMovie::displayFrame: `SND2' is not implemented");
			_file.seek(size, SEEK_CUR);
			break;

		case MKID_BE('VQFR'):
			foundFrame = true;
			end = _file.pos() + size - 8;

			while (_file.pos() < end) {
				tag = readTag();
				size = _file.readUint32BE();

				switch (tag) {
				case MKID_BE('CBF0'):	// Full codebook
					_file.read(_codeBook, size);
					break;

				case MKID_BE('CBFZ'):	// Full codebook
					inbuf = (byte *)allocBuffer(0, size);
					_file.read(inbuf, size);
					Screen::decodeFrame4(inbuf, _codeBook, _codeBookSize);
					break;

				case MKID_BE('CBP0'):	// Partial codebook
					_compressedCodeBook = false;
					_file.read(_partialCodeBook + _partialCodeBookSize, size);
					_partialCodeBookSize += size;
					_numPartialCodeBooks++;
					break;

				case MKID_BE('CBPZ'):	// Partial codebook
					_compressedCodeBook = true;
					_file.read(_partialCodeBook + _partialCodeBookSize, size);
					_partialCodeBookSize += size;
					_numPartialCodeBooks++;
					break;

				case MKID_BE('CPL0'):	// Palette
					assert(size <= 3 * 256);
					_file.read(_vm->screen()->_currentPalette, size);
					break;

				case MKID_BE('CPLZ'):	// Palette
					inbuf = (byte *)allocBuffer(0, size);
					_file.read(inbuf, size);
					Screen::decodeFrame4(inbuf, _vm->screen()->_currentPalette, 768);
					break;

				case MKID_BE('VPT0'):	// Frame data
					assert(size / 2 <= _numVectorPointers);

					for (i = 0; i < size / 2; i++)
						_vectorPointers[i] = _file.readUint16LE();
					break;

				case MKID_BE('VPTZ'):	// Frame data
					inbuf = (byte *)allocBuffer(0, size);
					outbuf = (byte *)allocBuffer(1, 2 * _numVectorPointers);

					_file.read(inbuf, size);
					size = Screen::decodeFrame4(inbuf, outbuf, 2 * _numVectorPointers);

					assert(size / 2 <= _numVectorPointers);

					for (i = 0; i < size / 2; i++)
						_vectorPointers[i] = READ_LE_UINT16(outbuf + 2 * i);
					break;

				default:
					warning("VQAMovie::displayFrame: Unknown `VQFR' sub-tag `%c%c%c%c'", char((tag >> 24) & 0xFF), char((tag >> 16) & 0xFF), char((tag >> 8) & 0xFF), char(tag & 0xFF));
					_file.seek(size, SEEK_CUR);
					break;
				}

			}

			break;

		default:
			warning("VQAMovie::displayFrame: Unknown tag `%c%c%c%c'", char((tag >> 24) & 0xFF), char((tag >> 16) & 0xFF), char((tag >> 8) & 0xFF), char(tag & 0xFF));
			_file.seek(size, SEEK_CUR);
			break;
		}
	}

	// The frame has been decoded

	if (_frameInfo[frameNum] & 0x80000000) {
		_vm->screen()->setScreenPalette(_vm->screen()->_currentPalette);
	}

	int blockPitch = _header.width / _header.blockW;

	for (int by = 0; by < _header.height / _header.blockH; by++) {
		for (int bx = 0; bx < blockPitch; bx++) {
			byte *dst = _frame + by * _header.width * _header.blockH + bx * _header.blockW;
			int val = _vectorPointers[by * blockPitch + bx];

			if ((val & 0xFF00) == 0xFF00) {
				// Solid color
				for (i = 0; i < _header.blockH; i++) {
					memset(dst, 255 - (val & 0xFF), _header.blockW);
					dst += _header.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) * _header.blockW * _header.blockH];

				for (i = 0; i < _header.blockH; i++) {
					memcpy(dst, src, _header.blockW);
					src += _header.blockW;
					dst += _header.width;
				}
			}
		}
	}

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

	_vm->screen()->copyBlockToPage(_drawPage, _x, _y, _header.width, _header.height, _frame);
}

void VQAMovie::play() {
	uint32 startTick;

	if (!_opened)
		return;

	startTick = _system->getMillis();

	// First, handle any sound chunk that appears before the first frame.
	// At least in some versions, it will contain half a second of audio,
	// presumably to lessen the risk of audio underflow.
	//
	// In most movies, we will find a CMDS tag. The purpose of this is
	// currently unknown.
	//
	// In cow1_0.vqa, cow1_1.vqa, jung0.vqa, and jung1.vqa we will find a
	// VQFR tag. A frame before the first frame? Weird. It doesn't seem to
	// be needed, though.

	byte *inbuf, *outbuf;
	uint32 insize, outsize;

	if (_stream) {
		while (_file.pos() < (_frameInfo[0] & 0x7FFFFFFF)) {
			uint32 tag = readTag();
			uint32 size = _file.readUint32BE();

			if (_file.eof()) {
				warning("VQAMovie::play: Unexpected EOF");
				break;
			}

			switch (tag) {
			case MKID_BE('SND0'):	// Uncompressed sound
				inbuf = new byte[size];
				_file.read(inbuf, size);
				_stream->queueBuffer(inbuf, size);
				break;

			case MKID_BE('SND1'):	// Compressed sound
				outsize = _file.readUint16LE();
				insize = _file.readUint16LE();

				inbuf = new byte[insize];
				_file.read(inbuf, insize);

				if (insize == outsize) {
					_stream->queueBuffer(inbuf, insize);
				} else {
					outbuf = new byte[outsize];
					decodeSND1(inbuf, insize, outbuf, outsize);
					_stream->queueBuffer(outbuf, outsize);
					delete[] inbuf;
				}
				break;

			case MKID_BE('SND2'):	// Compressed sound
				warning("VQAMovie::play: `SND2' is not implemented");
				_file.seek(size, SEEK_CUR);
				break;

			case MKID_BE('CMDS'):	// Unused tag, always empty in kyra3
				debugC(9, kDebugLevelMovie, "VQAMovie::play: skipping CMDS tag");
				_file.seek(size, SEEK_CUR);
				break;

			default:
				warning("VQAMovie::play: Unknown tag `%c%c%c%c'", char((tag >> 24) & 0xFF), char((tag >> 16) & 0xFF), char((tag >> 8) & 0xFF), char(tag & 0xFF));
				_file.seek(size, SEEK_CUR);
				break;
			}
		}
	}

	_vm->_mixer->playInputStream(Audio::Mixer::kSFXSoundType, &_sound, _stream);

	for (uint i = 0; i < _header.numFrames; i++) {
		displayFrame(i);

		// TODO: Implement frame skipping?

		while (1) {
			uint32 elapsedTime;

			if (_vm->_mixer->isSoundHandleActive(_sound))
				elapsedTime = _vm->_mixer->getSoundElapsedTime(_sound);
			else 
				elapsedTime = _system->getMillis() - startTick;

			if (elapsedTime >= (i * 1000) / _header.frameRate)
				break;

			Common::Event event;

			Common::EventManager *eventMan = _system->getEventManager();
			while (eventMan->pollEvent(event)) {
				switch (event.type) {
				case Common::EVENT_KEYDOWN:
					if (event.kbd.ascii == 27)
						return;
					break;
				case Common::EVENT_QUIT:
					_vm->quitGame();
					return;
				default:
					break;
				}
			}

			_system->delayMillis(10);
		}

		_vm->screen()->updateScreen();
	}

	// TODO: Wait for the sound to finish?
}

} // end of namespace Kyra
