/* 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 "image/codecs/cinepak.h"
#include "image/codecs/cinepak_tables.h"

#include "common/debug.h"
#include "common/stream.h"
#include "common/system.h"
#include "common/textconsole.h"
#include "common/util.h"

#include "graphics/surface.h"

// Code here partially based off of ffmpeg ;)

namespace Image {

namespace {

inline void convertYUVToRGB(const byte *clipTable, byte y, int8 u, int8 v, byte &r, byte &g, byte &b) {
	r = clipTable[y + (v << 1)];
	g = clipTable[y - (u >> 1) - v];
	b = clipTable[y + (u << 1)];
}

inline uint32 convertYUVToColor(const byte *clipTable, const Graphics::PixelFormat &format, byte y, byte u, byte v) {
	byte r, g, b;
	convertYUVToRGB(clipTable, y, u, v, r, g, b);
	return format.RGBToColor(r, g, b);
}

inline uint16 createDitherTableIndex(const byte *clipTable, byte y, int8 u, int8 v) {
	byte r, g, b;
	convertYUVToRGB(clipTable, y, u, v, r, g, b);
	return ((r & 0xF8) << 6) |
	       ((g & 0xF8) << 1) |
	       ((b & 0xF0) >> 4);
}

/**
 * Put a raw pixel to the destination surface
 */
template<typename PixelInt>
inline void putPixelRaw(PixelInt *dst, const byte *clipTable, const Graphics::PixelFormat &format, byte y, byte u, byte v) {
	*dst = convertYUVToColor(clipTable, format, y, u, v);
}

/**
 * Specialized putPixelRaw for palettized 8bpp output
 */
template<>
inline void putPixelRaw(byte *dst, const byte *clipTable, const Graphics::PixelFormat &format, byte y, byte u, byte v) {
	*dst = y;
}

/**
 * The default codebook converter: raw output.
 */
struct CodebookConverterRaw {
	template<typename PixelInt>
	static inline void decodeBlock1(byte codebookIndex, const CinepakStrip &strip, PixelInt *(&rows)[4], const byte *clipTable, const byte *colorMap, const Graphics::PixelFormat &format) {
		const CinepakCodebook &codebook = strip.v1_codebook[codebookIndex];
		putPixelRaw(rows[0] + 0, clipTable, format, codebook.y[0], codebook.u, codebook.v);
		putPixelRaw(rows[0] + 1, clipTable, format, codebook.y[0], codebook.u, codebook.v);
		putPixelRaw(rows[1] + 0, clipTable, format, codebook.y[0], codebook.u, codebook.v);
		putPixelRaw(rows[1] + 1, clipTable, format, codebook.y[0], codebook.u, codebook.v);

		putPixelRaw(rows[0] + 2, clipTable, format, codebook.y[1], codebook.u, codebook.v);
		putPixelRaw(rows[0] + 3, clipTable, format, codebook.y[1], codebook.u, codebook.v);
		putPixelRaw(rows[1] + 2, clipTable, format, codebook.y[1], codebook.u, codebook.v);
		putPixelRaw(rows[1] + 3, clipTable, format, codebook.y[1], codebook.u, codebook.v);

		putPixelRaw(rows[2] + 0, clipTable, format, codebook.y[2], codebook.u, codebook.v);
		putPixelRaw(rows[2] + 1, clipTable, format, codebook.y[2], codebook.u, codebook.v);
		putPixelRaw(rows[3] + 0, clipTable, format, codebook.y[2], codebook.u, codebook.v);
		putPixelRaw(rows[3] + 1, clipTable, format, codebook.y[2], codebook.u, codebook.v);

		putPixelRaw(rows[2] + 2, clipTable, format, codebook.y[3], codebook.u, codebook.v);
		putPixelRaw(rows[2] + 3, clipTable, format, codebook.y[3], codebook.u, codebook.v);
		putPixelRaw(rows[3] + 2, clipTable, format, codebook.y[3], codebook.u, codebook.v);
		putPixelRaw(rows[3] + 3, clipTable, format, codebook.y[3], codebook.u, codebook.v);
	}

	template<typename PixelInt>
	static inline void decodeBlock4(const byte (&codebookIndex)[4], const CinepakStrip &strip, PixelInt *(&rows)[4], const byte *clipTable, const byte *colorMap, const Graphics::PixelFormat &format) {
		const CinepakCodebook &codebook1 = strip.v4_codebook[codebookIndex[0]];
		putPixelRaw(rows[0] + 0, clipTable, format, codebook1.y[0], codebook1.u, codebook1.v);
		putPixelRaw(rows[0] + 1, clipTable, format, codebook1.y[1], codebook1.u, codebook1.v);
		putPixelRaw(rows[1] + 0, clipTable, format, codebook1.y[2], codebook1.u, codebook1.v);
		putPixelRaw(rows[1] + 1, clipTable, format, codebook1.y[3], codebook1.u, codebook1.v);

		const CinepakCodebook &codebook2 = strip.v4_codebook[codebookIndex[1]];
		putPixelRaw(rows[0] + 2, clipTable, format, codebook2.y[0], codebook2.u, codebook2.v);
		putPixelRaw(rows[0] + 3, clipTable, format, codebook2.y[1], codebook2.u, codebook2.v);
		putPixelRaw(rows[1] + 2, clipTable, format, codebook2.y[2], codebook2.u, codebook2.v);
		putPixelRaw(rows[1] + 3, clipTable, format, codebook2.y[3], codebook2.u, codebook2.v);

		const CinepakCodebook &codebook3 = strip.v4_codebook[codebookIndex[2]];
		putPixelRaw(rows[2] + 0, clipTable, format, codebook3.y[0], codebook3.u, codebook3.v);
		putPixelRaw(rows[2] + 1, clipTable, format, codebook3.y[1], codebook3.u, codebook3.v);
		putPixelRaw(rows[3] + 0, clipTable, format, codebook3.y[2], codebook3.u, codebook3.v);
		putPixelRaw(rows[3] + 1, clipTable, format, codebook3.y[3], codebook3.u, codebook3.v);

		const CinepakCodebook &codebook4 = strip.v4_codebook[codebookIndex[3]];
		putPixelRaw(rows[2] + 2, clipTable, format, codebook4.y[0], codebook4.u, codebook4.v);
		putPixelRaw(rows[2] + 3, clipTable, format, codebook4.y[1], codebook4.u, codebook4.v);
		putPixelRaw(rows[3] + 2, clipTable, format, codebook4.y[2], codebook4.u, codebook4.v);
		putPixelRaw(rows[3] + 3, clipTable, format, codebook4.y[3], codebook4.u, codebook4.v);
	}
};

/**
 * Codebook converter that dithers in VFW-style
 */
struct CodebookConverterDitherVFW {
	static inline void decodeBlock1(byte codebookIndex, const CinepakStrip &strip, byte *(&rows)[4], const byte *clipTable, const byte *colorMap, const Graphics::PixelFormat &format) {
		const CinepakCodebook &codebook = strip.v1_codebook[codebookIndex];
		byte blockBuffer[16];
		ditherCodebookSmooth(codebook, blockBuffer, colorMap);
		rows[0][0] = blockBuffer[0];
		rows[0][1] = blockBuffer[1];
		rows[0][2] = blockBuffer[2];
		rows[0][3] = blockBuffer[3];
		rows[1][0] = blockBuffer[4];
		rows[1][1] = blockBuffer[5];
		rows[1][2] = blockBuffer[6];
		rows[1][3] = blockBuffer[7];
		rows[2][0] = blockBuffer[8];
		rows[2][1] = blockBuffer[9];
		rows[2][2] = blockBuffer[10];
		rows[2][3] = blockBuffer[11];
		rows[3][0] = blockBuffer[12];
		rows[3][1] = blockBuffer[13];
		rows[3][2] = blockBuffer[14];
		rows[3][3] = blockBuffer[15];
	}

	static inline void decodeBlock4(const byte (&codebookIndex)[4], const CinepakStrip &strip, byte *(&rows)[4], const byte *clipTable, const byte *colorMap, const Graphics::PixelFormat &format) {
		byte blockBuffer[16];
		ditherCodebookDetail(strip.v4_codebook[codebookIndex[0]], blockBuffer, colorMap);
		rows[0][0] = blockBuffer[0];
		rows[0][1] = blockBuffer[1];
		rows[1][0] = blockBuffer[4];
		rows[1][1] = blockBuffer[5];

		ditherCodebookDetail(strip.v4_codebook[codebookIndex[1]], blockBuffer, colorMap);
		rows[0][2] = blockBuffer[2];
		rows[0][3] = blockBuffer[3];
		rows[1][2] = blockBuffer[6];
		rows[1][3] = blockBuffer[7];

		ditherCodebookDetail(strip.v4_codebook[codebookIndex[2]], blockBuffer, colorMap);
		rows[2][0] = blockBuffer[8];
		rows[2][1] = blockBuffer[9];
		rows[3][0] = blockBuffer[12];
		rows[3][1] = blockBuffer[13];

		ditherCodebookDetail(strip.v4_codebook[codebookIndex[3]], blockBuffer, colorMap);
		rows[2][2] = blockBuffer[10];
		rows[2][3] = blockBuffer[11];
		rows[3][2] = blockBuffer[14];
		rows[3][3] = blockBuffer[15];
	}

private:
	static inline void ditherCodebookDetail(const CinepakCodebook &codebook, byte *dst, const byte *colorMap) {
		int uLookup = (byte)codebook.u * 2;
		int vLookup = (byte)codebook.v * 2;
		uint32 uv1 = s_uLookup[uLookup] | s_vLookup[vLookup];
		uint32 uv2 = s_uLookup[uLookup + 1] | s_vLookup[vLookup + 1];

		int yLookup1 = codebook.y[0] * 2;
		int yLookup2 = codebook.y[1] * 2;
		int yLookup3 = codebook.y[2] * 2;
		int yLookup4 = codebook.y[3] * 2;

		uint32 pixelGroup1 = uv2 | s_yLookup[yLookup1 + 1];
		uint32 pixelGroup2 = uv2 | s_yLookup[yLookup2 + 1];
		uint32 pixelGroup3 = uv1 | s_yLookup[yLookup3];
		uint32 pixelGroup4 = uv1 | s_yLookup[yLookup4];
		uint32 pixelGroup5 = uv1 | s_yLookup[yLookup1];
		uint32 pixelGroup6 = uv1 | s_yLookup[yLookup2];
		uint32 pixelGroup7 = uv2 | s_yLookup[yLookup3 + 1];
		uint32 pixelGroup8 = uv2 | s_yLookup[yLookup4 + 1];

		dst[0] = getRGBLookupEntry(colorMap, pixelGroup1 & 0xFFFF);
		dst[1] = getRGBLookupEntry(colorMap, pixelGroup2 >> 16);
		dst[2] = getRGBLookupEntry(colorMap, pixelGroup5 & 0xFFFF);
		dst[3] = getRGBLookupEntry(colorMap, pixelGroup6 >> 16);
		dst[4] = getRGBLookupEntry(colorMap, pixelGroup3 & 0xFFFF);
		dst[5] = getRGBLookupEntry(colorMap, pixelGroup4 >> 16);
		dst[6] = getRGBLookupEntry(colorMap, pixelGroup7 & 0xFFFF);
		dst[7] = getRGBLookupEntry(colorMap, pixelGroup8 >> 16);
		dst[8] = getRGBLookupEntry(colorMap, pixelGroup1 >> 16);
		dst[9] = getRGBLookupEntry(colorMap, pixelGroup6 & 0xFFFF);
		dst[10] = getRGBLookupEntry(colorMap, pixelGroup5 >> 16);
		dst[11] = getRGBLookupEntry(colorMap, pixelGroup2 & 0xFFFF);
		dst[12] = getRGBLookupEntry(colorMap, pixelGroup3 >> 16);
		dst[13] = getRGBLookupEntry(colorMap, pixelGroup8 & 0xFFFF);
		dst[14] = getRGBLookupEntry(colorMap, pixelGroup7 >> 16);
		dst[15] = getRGBLookupEntry(colorMap, pixelGroup4 & 0xFFFF);
	}

	static inline void ditherCodebookSmooth(const CinepakCodebook &codebook, byte *dst, const byte *colorMap) {
		int uLookup = (byte)codebook.u * 2;
		int vLookup = (byte)codebook.v * 2;
		uint32 uv1 = s_uLookup[uLookup] | s_vLookup[vLookup];
		uint32 uv2 = s_uLookup[uLookup + 1] | s_vLookup[vLookup + 1];

		int yLookup1 = codebook.y[0] * 2;
		int yLookup2 = codebook.y[1] * 2;
		int yLookup3 = codebook.y[2] * 2;
		int yLookup4 = codebook.y[3] * 2;

		uint32 pixelGroup1 = uv2 | s_yLookup[yLookup1 + 1];
		uint32 pixelGroup2 = uv1 | s_yLookup[yLookup2];
		uint32 pixelGroup3 = uv1 | s_yLookup[yLookup1];
		uint32 pixelGroup4 = uv2 | s_yLookup[yLookup2 + 1];
		uint32 pixelGroup5 = uv2 | s_yLookup[yLookup3 + 1];
		uint32 pixelGroup6 = uv1 | s_yLookup[yLookup3];
		uint32 pixelGroup7 = uv1 | s_yLookup[yLookup4];
		uint32 pixelGroup8 = uv2 | s_yLookup[yLookup4 + 1];

		dst[0] = getRGBLookupEntry(colorMap, pixelGroup1 & 0xFFFF);
		dst[1] = getRGBLookupEntry(colorMap, pixelGroup1 >> 16);
		dst[2] = getRGBLookupEntry(colorMap, pixelGroup2 & 0xFFFF);
		dst[3] = getRGBLookupEntry(colorMap, pixelGroup2 >> 16);
		dst[4] = getRGBLookupEntry(colorMap, pixelGroup3 & 0xFFFF);
		dst[5] = getRGBLookupEntry(colorMap, pixelGroup3 >> 16);
		dst[6] = getRGBLookupEntry(colorMap, pixelGroup4 & 0xFFFF);
		dst[7] = getRGBLookupEntry(colorMap, pixelGroup4 >> 16);
		dst[8] = getRGBLookupEntry(colorMap, pixelGroup5 >> 16);
		dst[9] = getRGBLookupEntry(colorMap, pixelGroup6 & 0xFFFF);
		dst[10] = getRGBLookupEntry(colorMap, pixelGroup7 >> 16);
		dst[11] = getRGBLookupEntry(colorMap, pixelGroup8 & 0xFFFF);
		dst[12] = getRGBLookupEntry(colorMap, pixelGroup6 >> 16);
		dst[13] = getRGBLookupEntry(colorMap, pixelGroup5 & 0xFFFF);
		dst[14] = getRGBLookupEntry(colorMap, pixelGroup8 >> 16);
		dst[15] = getRGBLookupEntry(colorMap, pixelGroup7 & 0xFFFF);
	}

	static inline byte getRGBLookupEntry(const byte *colorMap, uint16 index) {
		return colorMap[s_defaultPaletteLookup[CLIP<int>(index, 0, 1024)]];
	}
};

/**
 * Codebook converter that dithers in QT-style
 */
struct CodebookConverterDitherQT {
	static inline void decodeBlock1(byte codebookIndex, const CinepakStrip &strip, byte *(&rows)[4], const byte *clipTable, const byte *colorMap, const Graphics::PixelFormat &format) {
		const byte *colorPtr = strip.v1_dither + (codebookIndex << 2);
		WRITE_UINT32(rows[0], READ_UINT32(colorPtr));
		WRITE_UINT32(rows[1], READ_UINT32(colorPtr + 1024));
		WRITE_UINT32(rows[2], READ_UINT32(colorPtr + 2048));
		WRITE_UINT32(rows[3], READ_UINT32(colorPtr + 3072));
	}

	static inline void decodeBlock4(const byte (&codebookIndex)[4], const CinepakStrip &strip, byte *(&rows)[4], const byte *clipTable, const byte *colorMap, const Graphics::PixelFormat &format) {
		const byte *colorPtr = strip.v4_dither + (codebookIndex[0] << 2);
		WRITE_UINT16(rows[0] + 0, READ_UINT16(colorPtr + 0));
		WRITE_UINT16(rows[1] + 0, READ_UINT16(colorPtr + 2));

		colorPtr = strip.v4_dither + (codebookIndex[1] << 2);
		WRITE_UINT16(rows[0] + 2, READ_UINT16(colorPtr + 1024));
		WRITE_UINT16(rows[1] + 2, READ_UINT16(colorPtr + 1026));

		colorPtr = strip.v4_dither + (codebookIndex[2] << 2);
		WRITE_UINT16(rows[2] + 0, READ_UINT16(colorPtr + 2048));
		WRITE_UINT16(rows[3] + 0, READ_UINT16(colorPtr + 2050));

		colorPtr = strip.v4_dither + (codebookIndex[3] << 2);
		WRITE_UINT16(rows[2] + 2, READ_UINT16(colorPtr + 3072));
		WRITE_UINT16(rows[3] + 2, READ_UINT16(colorPtr + 3074));
	}
};

template<typename PixelInt, typename CodebookConverter>
void decodeVectorsTmpl(CinepakFrame &frame, const byte *clipTable, const byte *colorMap, Common::SeekableReadStream &stream, uint16 strip, byte chunkID, uint32 chunkSize) {
	uint32 flag = 0, mask = 0;
	PixelInt *iy[4];
	int32 startPos = stream.pos();

	for (uint16 y = frame.strips[strip].rect.top; y < frame.strips[strip].rect.bottom; y += 4) {
		iy[0] = (PixelInt *)frame.surface->getBasePtr(frame.strips[strip].rect.left, + y);
		iy[1] = iy[0] + frame.width;
		iy[2] = iy[1] + frame.width;
		iy[3] = iy[2] + frame.width;

		for (uint16 x = frame.strips[strip].rect.left; x < frame.strips[strip].rect.right; x += 4) {
			if ((chunkID & 0x01) && !(mask >>= 1)) {
				if ((stream.pos() - startPos + 4) > (int32)chunkSize)
					return;

				flag  = stream.readUint32BE();
				mask  = 0x80000000;
			}

			if (!(chunkID & 0x01) || (flag & mask)) {
				if (!(chunkID & 0x02) && !(mask >>= 1)) {
					if ((stream.pos() - startPos + 4) > (int32)chunkSize)
						return;

					flag  = stream.readUint32BE();
					mask  = 0x80000000;
				}

				if ((chunkID & 0x02) || (~flag & mask)) {
					if ((stream.pos() - startPos + 1) > (int32)chunkSize)
						return;

					// Get the codebook
					byte codebook = stream.readByte();
					CodebookConverter::decodeBlock1(codebook, frame.strips[strip], iy, clipTable, colorMap, frame.surface->format);
				} else if (flag & mask) {
					if ((stream.pos() - startPos + 4) > (int32)chunkSize)
						return;

					byte codebook[4];
					stream.read(codebook, 4);
					CodebookConverter::decodeBlock4(codebook, frame.strips[strip], iy, clipTable, colorMap, frame.surface->format);
				}
			}

			for (byte i = 0; i < 4; i++)
				iy[i] += 4;
		}
	}
}

} // End of anonymous namespace

CinepakDecoder::CinepakDecoder(int bitsPerPixel) : Codec(), _bitsPerPixel(bitsPerPixel) {
	_curFrame.surface = 0;
	_curFrame.strips = 0;
	_y = 0;
	_colorMap = 0;
	_ditherPalette = 0;
	_ditherType = kDitherTypeUnknown;

	if (bitsPerPixel == 8) {
		_pixelFormat = Graphics::PixelFormat::createFormatCLUT8();
	} else {
		_pixelFormat = g_system->getScreenFormat();

		// Default to a 32bpp format, if in 8bpp mode
		if (_pixelFormat.bytesPerPixel == 1)
			_pixelFormat = Graphics::PixelFormat(4, 8, 8, 8, 8, 8, 16, 24, 0);
	}

	// Create a lookup for the clip function
	// This dramatically improves the performance of the color conversion
	_clipTableBuf = new byte[1024];

	for (uint i = 0; i < 1024; i++) {
		if (i <= 512)
			_clipTableBuf[i] = 0;
		else if (i >= 768)
			_clipTableBuf[i] = 255;
		else
			_clipTableBuf[i] = i - 512;
	}

	_clipTable = _clipTableBuf + 512;
}

CinepakDecoder::~CinepakDecoder() {
	if (_curFrame.surface) {
		_curFrame.surface->free();
		delete _curFrame.surface;
	}

	delete[] _curFrame.strips;
	delete[] _clipTableBuf;

	delete[] _colorMap;
	delete[] _ditherPalette;
}

const Graphics::Surface *CinepakDecoder::decodeFrame(Common::SeekableReadStream &stream) {
	_curFrame.flags = stream.readByte();
	_curFrame.length = (stream.readByte() << 16);
	_curFrame.length |= stream.readUint16BE();
	_curFrame.width = stream.readUint16BE();
	_curFrame.height = stream.readUint16BE();
	_curFrame.stripCount = stream.readUint16BE();

	if (!_curFrame.strips)
		_curFrame.strips = new CinepakStrip[_curFrame.stripCount];

	debug(4, "Cinepak Frame: Width = %d, Height = %d, Strip Count = %d", _curFrame.width, _curFrame.height, _curFrame.stripCount);

	// Borrowed from FFMPEG. This should cut out the extra data Cinepak for Sega has (which is useless).
	// The theory behind this is that this is here to confuse standard Cinepak decoders. But, we won't let that happen! ;)
	if (_curFrame.length != (uint32)stream.size()) {
		if (stream.readUint16BE() == 0xFE00)
			stream.readUint32BE();
		else if ((stream.size() % _curFrame.length) == 0)
			stream.seek(-2, SEEK_CUR);
	}

	if (!_curFrame.surface) {
		_curFrame.surface = new Graphics::Surface();
		_curFrame.surface->create(_curFrame.width, _curFrame.height, _pixelFormat);
	}

	// Reset the y variable.
	_y = 0;

	for (uint16 i = 0; i < _curFrame.stripCount; i++) {
		if (i > 0 && !(_curFrame.flags & 1)) { // Use codebooks from last strip

			for (uint16 j = 0; j < 256; j++) {
				_curFrame.strips[i].v1_codebook[j] = _curFrame.strips[i - 1].v1_codebook[j];
				_curFrame.strips[i].v4_codebook[j] = _curFrame.strips[i - 1].v4_codebook[j];
			}

			// Copy the QuickTime dither tables
			memcpy(_curFrame.strips[i].v1_dither, _curFrame.strips[i - 1].v1_dither, 256 * 4 * 4 * 4);
			memcpy(_curFrame.strips[i].v4_dither, _curFrame.strips[i - 1].v4_dither, 256 * 4 * 4 * 4);
		}

		_curFrame.strips[i].id = stream.readUint16BE();
		_curFrame.strips[i].length = stream.readUint16BE() - 12; // Subtract the 12 byte header
		_curFrame.strips[i].rect.top = _y; stream.readUint16BE(); // Ignore, substitute with our own.
		_curFrame.strips[i].rect.left = 0; stream.readUint16BE(); // Ignore, substitute with our own
		_curFrame.strips[i].rect.bottom = _y + stream.readUint16BE();
		_curFrame.strips[i].rect.right = _curFrame.width; stream.readUint16BE(); // Ignore, substitute with our own

		// Sanity check. Because Cinepak is based on 4x4 blocks, the width and height of each strip needs to be divisible by 4.
		assert(!(_curFrame.strips[i].rect.width() % 4) && !(_curFrame.strips[i].rect.height() % 4));

		uint32 pos = stream.pos();

		while ((uint32)stream.pos() < (pos + _curFrame.strips[i].length) && !stream.eos()) {
			byte chunkID = stream.readByte();

			if (stream.eos())
				break;

			// Chunk Size is 24-bit, ignore the first 4 bytes
			uint32 chunkSize = stream.readByte() << 16;
			chunkSize += stream.readUint16BE() - 4;

			int32 startPos = stream.pos();

			switch (chunkID) {
			case 0x20:
			case 0x21:
			case 0x24:
			case 0x25:
				loadCodebook(stream, i, 4, chunkID, chunkSize);
				break;
			case 0x22:
			case 0x23:
			case 0x26:
			case 0x27:
				loadCodebook(stream, i, 1, chunkID, chunkSize);
				break;
			case 0x30:
			case 0x31:
			case 0x32:
				if (_ditherPalette)
					ditherVectors(stream, i, chunkID, chunkSize);
				else
					decodeVectors(stream, i, chunkID, chunkSize);
				break;
			default:
				warning("Unknown Cinepak chunk ID %02x", chunkID);
				return _curFrame.surface;
			}

			if (stream.pos() != startPos + (int32)chunkSize)
				stream.seek(startPos + chunkSize);
		}

		_y = _curFrame.strips[i].rect.bottom;
	}

	return _curFrame.surface;
}

void CinepakDecoder::loadCodebook(Common::SeekableReadStream &stream, uint16 strip, byte codebookType, byte chunkID, uint32 chunkSize) {
	CinepakCodebook *codebook = (codebookType == 1) ? _curFrame.strips[strip].v1_codebook : _curFrame.strips[strip].v4_codebook;

	int32 startPos = stream.pos();
	uint32 flag = 0, mask = 0;

	for (uint16 i = 0; i < 256; i++) {
		if ((chunkID & 0x01) && !(mask >>= 1)) {
			if ((stream.pos() - startPos + 4) > (int32)chunkSize)
				break;

			flag  = stream.readUint32BE();
			mask  = 0x80000000;
		}

		if (!(chunkID & 0x01) || (flag & mask)) {
			byte n = (chunkID & 0x04) ? 4 : 6;
			if ((stream.pos() - startPos + n) > (int32)chunkSize)
				break;

			stream.read(codebook[i].y, 4);

			if (n == 6) {
				codebook[i].u = stream.readSByte();
				codebook[i].v = stream.readSByte();
			} else {
				// This codebook type indicates either greyscale or
				// palettized video. For greyscale, default us to
				// 0 for both u and v.
				codebook[i].u = 0;
				codebook[i].v = 0;
			}

			// Dither the codebook if we're dithering for QuickTime
			if (_ditherType == kDitherTypeQT)
				ditherCodebookQT(strip, codebookType, i);
		}
	}
}

void CinepakDecoder::ditherCodebookQT(uint16 strip, byte codebookType, uint16 codebookIndex) {
	if (codebookType == 1) {
		const CinepakCodebook &codebook = _curFrame.strips[strip].v1_codebook[codebookIndex];
		byte *output = _curFrame.strips[strip].v1_dither + (codebookIndex << 2);

		byte *ditherEntry = _colorMap + createDitherTableIndex(_clipTable, codebook.y[0], codebook.u, codebook.v);
		output[0x000] = ditherEntry[0x0000];
		output[0x001] = ditherEntry[0x4000];
		output[0x400] = ditherEntry[0xC000];
		output[0x401] = ditherEntry[0x0000];

		ditherEntry = _colorMap + createDitherTableIndex(_clipTable, codebook.y[1], codebook.u, codebook.v);
		output[0x002] = ditherEntry[0x8000];
		output[0x003] = ditherEntry[0xC000];
		output[0x402] = ditherEntry[0x4000];
		output[0x403] = ditherEntry[0x8000];

		ditherEntry = _colorMap + createDitherTableIndex(_clipTable, codebook.y[2], codebook.u, codebook.v);
		output[0x800] = ditherEntry[0x4000];
		output[0x801] = ditherEntry[0x8000];
		output[0xC00] = ditherEntry[0x8000];
		output[0xC01] = ditherEntry[0xC000];

		ditherEntry = _colorMap + createDitherTableIndex(_clipTable, codebook.y[3], codebook.u, codebook.v);
		output[0x802] = ditherEntry[0xC000];
		output[0x803] = ditherEntry[0x0000];
		output[0xC02] = ditherEntry[0x0000];
		output[0xC03] = ditherEntry[0x4000];
	} else {
		const CinepakCodebook &codebook = _curFrame.strips[strip].v4_codebook[codebookIndex];
		byte *output = _curFrame.strips[strip].v4_dither + (codebookIndex << 2);

		byte *ditherEntry = _colorMap + createDitherTableIndex(_clipTable, codebook.y[0], codebook.u, codebook.v);
		output[0x000] = ditherEntry[0x0000];
		output[0x400] = ditherEntry[0x8000];
		output[0x800] = ditherEntry[0x4000];
		output[0xC00] = ditherEntry[0xC000];

		ditherEntry = _colorMap + createDitherTableIndex(_clipTable, codebook.y[1], codebook.u, codebook.v);
		output[0x001] = ditherEntry[0x4000];
		output[0x401] = ditherEntry[0xC000];
		output[0x801] = ditherEntry[0x8000];
		output[0xC01] = ditherEntry[0x0000];

		ditherEntry = _colorMap + createDitherTableIndex(_clipTable, codebook.y[2], codebook.u, codebook.v);
		output[0x002] = ditherEntry[0xC000];
		output[0x402] = ditherEntry[0x4000];
		output[0x802] = ditherEntry[0x8000];
		output[0xC02] = ditherEntry[0x0000];

		ditherEntry = _colorMap + createDitherTableIndex(_clipTable, codebook.y[3], codebook.u, codebook.v);
		output[0x003] = ditherEntry[0x0000];
		output[0x403] = ditherEntry[0x8000];
		output[0x803] = ditherEntry[0xC000];
		output[0xC03] = ditherEntry[0x4000];
	}
}

void CinepakDecoder::decodeVectors(Common::SeekableReadStream &stream, uint16 strip, byte chunkID, uint32 chunkSize) {
	if (_curFrame.surface->format.bytesPerPixel == 1) {
		decodeVectorsTmpl<byte, CodebookConverterRaw>(_curFrame, _clipTable, _colorMap, stream, strip, chunkID, chunkSize);
	} else if (_curFrame.surface->format.bytesPerPixel == 2) {
		decodeVectorsTmpl<uint16, CodebookConverterRaw>(_curFrame, _clipTable, _colorMap, stream, strip, chunkID, chunkSize);
	} else if (_curFrame.surface->format.bytesPerPixel == 4) {
		decodeVectorsTmpl<uint32, CodebookConverterRaw>(_curFrame, _clipTable, _colorMap, stream, strip, chunkID, chunkSize);
	}
}

bool CinepakDecoder::canDither(DitherType type) const {
	return (type == kDitherTypeVFW || type == kDitherTypeQT) && _bitsPerPixel == 24;
}

void CinepakDecoder::setDither(DitherType type, const byte *palette) {
	assert(canDither(type));

	delete[] _colorMap;
	delete[] _ditherPalette;

	_ditherPalette = new byte[256 * 3];
	memcpy(_ditherPalette, palette, 256 * 3);

	_dirtyPalette = true;
	_pixelFormat = Graphics::PixelFormat::createFormatCLUT8();
	_ditherType = type;

	if (type == kDitherTypeVFW) {
		_colorMap = new byte[221];

		for (int i = 0; i < 221; i++)
			_colorMap[i] = findNearestRGB(i);
	} else {
		// Generate QuickTime dither table
		// 4 blocks of 0x4000 bytes (RGB554 lookup)
		_colorMap = createQuickTimeDitherTable(palette, 256);
	}
}

byte CinepakDecoder::findNearestRGB(int index) const {
	int r = s_defaultPalette[index * 3];
	int g = s_defaultPalette[index * 3 + 1];
	int b = s_defaultPalette[index * 3 + 2];

	byte result = 0;
	int diff = 0x7FFFFFFF;

	for (int i = 0; i < 256; i++) {
		int bDiff = b - (int)_ditherPalette[i * 3 + 2];
		int curDiffB = diff - (bDiff * bDiff);

		if (curDiffB > 0) {
			int gDiff = g - (int)_ditherPalette[i * 3 + 1];
			int curDiffG = curDiffB - (gDiff * gDiff);

			if (curDiffG > 0) {
				int rDiff = r - (int)_ditherPalette[i * 3];
				int curDiffR = curDiffG - (rDiff * rDiff);

				if (curDiffR > 0) {
					diff -= curDiffR;
					result = i;

					if (diff == 0)
						break;
				}
			}
		}
	}

	return result;
}

void CinepakDecoder::ditherVectors(Common::SeekableReadStream &stream, uint16 strip, byte chunkID, uint32 chunkSize) {
	if (_ditherType == kDitherTypeVFW)
		decodeVectorsTmpl<byte, CodebookConverterDitherVFW>(_curFrame, _clipTable, _colorMap, stream, strip, chunkID, chunkSize);
	else
		decodeVectorsTmpl<byte, CodebookConverterDitherQT>(_curFrame, _clipTable, _colorMap, stream, strip, chunkID, chunkSize);
}

} // End of namespace Image