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

// Based off ffmpeg's SMC decoder

#include "image/codecs/smc.h"
#include "common/stream.h"
#include "common/textconsole.h"

namespace Image {

#define GET_BLOCK_COUNT() \
  (opcode & 0x10) ? (1 + stream.readByte()) : 1 + (opcode & 0x0F);

#define ADVANCE_BLOCK() \
{ \
	pixelPtr += 4; \
	if (pixelPtr >= _surface->w) { \
		pixelPtr = 0; \
		rowPtr += _surface->w * 4; \
	} \
	totalBlocks--; \
	if (totalBlocks < 0) { \
		warning("block counter just went negative (this should not happen)"); \
		return _surface; \
	} \
}

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

SMCDecoder::~SMCDecoder() {
	_surface->free();
	delete _surface;
}

const Graphics::Surface *SMCDecoder::decodeFrame(Common::SeekableReadStream &stream) {
	byte *pixels = (byte *)_surface->getPixels();

	uint32 numBlocks = 0;
	uint32 colorFlags = 0;
	uint32 colorFlagsA = 0;
	uint32 colorFlagsB = 0;

	const uint16 rowInc = _surface->w - 4;
	int32 rowPtr = 0;
	int32 pixelPtr = 0;
	uint32 blockPtr = 0;
	uint32 prevBlockPtr = 0;
	uint32 prevBlockPtr1 = 0, prevBlockPtr2 = 0;
	byte prevBlockFlag = false;
	uint32 pixel = 0;

	uint32 colorPairIndex = 0;
	uint32 colorQuadIndex = 0;
	uint32 colorOctetIndex = 0;
	uint32 colorTableIndex = 0;  // indices to color pair, quad, or octet tables

	int32 chunkSize = stream.readUint32BE() & 0x00FFFFFF;
	if (chunkSize != stream.size())
		warning("MOV chunk size != SMC chunk size (%d != %d); ignoring SMC chunk size", chunkSize, stream.size());

	int32 totalBlocks = ((_surface->w + 3) / 4) * ((_surface->h + 3) / 4);

	// traverse through the blocks
	while (totalBlocks != 0) {
		// sanity checks

		// make sure stream ptr hasn't gone out of bounds
		if (stream.pos() > stream.size()) {
			warning("SMC decoder just went out of bounds (stream ptr = %d, chunk size = %d)", stream.pos(), stream.size());
			return _surface;
		}

		// make sure the row pointer hasn't gone wild
		if (rowPtr >= _surface->w * _surface->h) {
			warning("SMC decoder just went out of bounds (row ptr = %d, size = %d)", rowPtr, _surface->w * _surface->h);
			return _surface;
		}

		byte opcode = stream.readByte();

		switch (opcode & 0xF0) {
		// skip n blocks
		case 0x00:
		case 0x10:
			numBlocks = GET_BLOCK_COUNT();
			while (numBlocks--) {
				ADVANCE_BLOCK();
			}
			break;

		// repeat last block n times
		case 0x20:
		case 0x30:
			numBlocks = GET_BLOCK_COUNT();

			// sanity check
			if (rowPtr == 0 && pixelPtr == 0) {
				warning("encountered repeat block opcode (%02X) but no blocks rendered yet", opcode & 0xF0);
				break;
			}

			// figure out where the previous block started
			if (pixelPtr == 0)
				prevBlockPtr1 = (rowPtr - _surface->w * 4) + _surface->w - 4;
			else
				prevBlockPtr1 = rowPtr + pixelPtr - 4;

			while (numBlocks--) {
				blockPtr = rowPtr + pixelPtr;
				prevBlockPtr = prevBlockPtr1;
				for (byte y = 0; y < 4; y++) {
					for (byte x = 0; x < 4; x++)
						pixels[blockPtr++] = pixels[prevBlockPtr++];
					blockPtr += rowInc;
					prevBlockPtr += rowInc;
				}
				ADVANCE_BLOCK();
			}
			break;

		// repeat previous pair of blocks n times
		case 0x40:
		case 0x50:
			numBlocks = GET_BLOCK_COUNT();
			numBlocks *= 2;

			// sanity check
			if (rowPtr == 0 && pixelPtr < 2 * 4) {
				warning("encountered repeat block opcode (%02X) but not enough blocks rendered yet", opcode & 0xF0);
				break;
			}

			// figure out where the previous 2 blocks started
			if (pixelPtr == 0)
				prevBlockPtr1 = (rowPtr - _surface->w * 4) + _surface->w - 4 * 2;
			else if (pixelPtr == 4)
				prevBlockPtr1 = (rowPtr - _surface->w * 4) + rowInc;
			else
				prevBlockPtr1 = rowPtr + pixelPtr - 4 * 2;

			if (pixelPtr == 0)
				prevBlockPtr2 = (rowPtr - _surface->w * 4) + rowInc;
			else
				prevBlockPtr2 = rowPtr + pixelPtr - 4;

			prevBlockFlag = 0;
			while (numBlocks--) {
				blockPtr = rowPtr + pixelPtr;

				if (prevBlockFlag)
					prevBlockPtr = prevBlockPtr2;
				else
					prevBlockPtr = prevBlockPtr1;

				prevBlockFlag = !prevBlockFlag;

				for (byte y = 0; y < 4; y++) {
					for (byte x = 0; x < 4; x++)
						pixels[blockPtr++] = pixels[prevBlockPtr++];

					blockPtr += rowInc;
					prevBlockPtr += rowInc;
				}
				ADVANCE_BLOCK();
			}
			break;

		// 1-color block encoding
		case 0x60:
		case 0x70:
			numBlocks = GET_BLOCK_COUNT();
			pixel = stream.readByte();

			while (numBlocks--) {
				blockPtr = rowPtr + pixelPtr;
				for (byte y = 0; y < 4; y++) {
					for (byte x = 0; x < 4; x++)
						pixels[blockPtr++] = pixel;

					blockPtr += rowInc;
				}
				ADVANCE_BLOCK();
			}
			break;

		// 2-color block encoding
		case 0x80:
		case 0x90:
			numBlocks = (opcode & 0x0F) + 1;

			// figure out which color pair to use to paint the 2-color block
			if ((opcode & 0xF0) == 0x80) {
				// fetch the next 2 colors from bytestream and store in next
				// available entry in the color pair table
				for (byte i = 0; i < CPAIR; i++) {
					pixel = stream.readByte();
					colorTableIndex = CPAIR * colorPairIndex + i;
					_colorPairs[colorTableIndex] = pixel;
				}

				// this is the base index to use for this block
				colorTableIndex = CPAIR * colorPairIndex;
				colorPairIndex++;

				// wraparound
				if (colorPairIndex == COLORS_PER_TABLE)
					colorPairIndex = 0;
			} else
				colorTableIndex = CPAIR * stream.readByte();

			while (numBlocks--) {
				colorFlags = stream.readUint16BE();
				uint16 flagMask = 0x8000;
				blockPtr = rowPtr + pixelPtr;
				for (byte y = 0; y < 4; y++) {
					for (byte x = 0; x < 4; x++) {
						if (colorFlags & flagMask)
							pixel = colorTableIndex + 1;
						else
							pixel = colorTableIndex;

						flagMask >>= 1;
						pixels[blockPtr++] = _colorPairs[pixel];
					}

					blockPtr += rowInc;
				}
				ADVANCE_BLOCK();
			}
			break;

		// 4-color block encoding
		case 0xA0:
		case 0xB0:
			numBlocks = (opcode & 0x0F) + 1;

			// figure out which color quad to use to paint the 4-color block
			if ((opcode & 0xF0) == 0xA0) {
				// fetch the next 4 colors from bytestream and store in next
				// available entry in the color quad table
				for (byte i = 0; i < CQUAD; i++) {
					pixel = stream.readByte();
					colorTableIndex = CQUAD * colorQuadIndex + i;
					_colorQuads[colorTableIndex] = pixel;
				}

				// this is the base index to use for this block
				colorTableIndex = CQUAD * colorQuadIndex;
				colorQuadIndex++;

				// wraparound
				if (colorQuadIndex == COLORS_PER_TABLE)
					colorQuadIndex = 0;
			} else
				colorTableIndex = CQUAD * stream.readByte();

			while (numBlocks--) {
				colorFlags = stream.readUint32BE();

				// flag mask actually acts as a bit shift count here
				byte flagMask = 30;
				blockPtr = rowPtr + pixelPtr;

				for (byte y = 0; y < 4; y++) {
					for (byte x = 0; x < 4; x++) {
						pixel = colorTableIndex + ((colorFlags >> flagMask) & 0x03);
						flagMask -= 2;
						pixels[blockPtr++] = _colorQuads[pixel];
					}
					blockPtr += rowInc;
				}
				ADVANCE_BLOCK();
			}
			break;

		// 8-color block encoding
		case 0xC0:
		case 0xD0:
			numBlocks = (opcode & 0x0F) + 1;

			// figure out which color octet to use to paint the 8-color block
			if ((opcode & 0xF0) == 0xC0) {
				// fetch the next 8 colors from bytestream and store in next
				// available entry in the color octet table
				for (byte i = 0; i < COCTET; i++) {
					pixel = stream.readByte();
					colorTableIndex = COCTET * colorOctetIndex + i;
					_colorOctets[colorTableIndex] = pixel;
				}

				// this is the base index to use for this block
				colorTableIndex = COCTET * colorOctetIndex;
				colorOctetIndex++;

				// wraparound
				if (colorOctetIndex == COLORS_PER_TABLE)
					colorOctetIndex = 0;
			} else
				colorTableIndex = COCTET * stream.readByte();

			while (numBlocks--) {
				/*
				  For this input of 6 hex bytes:
				    01 23 45 67 89 AB
				  Mangle it to this output:
				    flags_a = xx012456, flags_b = xx89A37B
				*/

				// build the color flags
				byte flagData[6];
				stream.read(flagData, 6);

				colorFlagsA = ((READ_BE_UINT16(flagData) & 0xFFF0) << 8) | (READ_BE_UINT16(flagData + 2) >> 4);
				colorFlagsB = ((READ_BE_UINT16(flagData + 4) & 0xFFF0) << 8) | ((flagData[1] & 0xF) << 8) |
								((flagData[3] & 0xF) << 4) | (flagData[5] & 0xf);

				colorFlags = colorFlagsA;

				// flag mask actually acts as a bit shift count here
				byte flagMask = 21;
				blockPtr = rowPtr + pixelPtr;
				for (byte y = 0; y < 4; y++) {
					// reload flags at third row (iteration y == 2)
					if (y == 2) {
						colorFlags = colorFlagsB;
						flagMask = 21;
					}

					for (byte x = 0; x < 4; x++) {
						pixel = colorTableIndex + ((colorFlags >> flagMask) & 0x07);
						flagMask -= 3;
						pixels[blockPtr++] = _colorOctets[pixel];
					}

					blockPtr += rowInc;
				}
				ADVANCE_BLOCK();
			}
			break;

		// 16-color block encoding (every pixel is a different color)
		case 0xE0:
			numBlocks = (opcode & 0x0F) + 1;

			while (numBlocks--) {
				blockPtr = rowPtr + pixelPtr;
				for (byte y = 0; y < 4; y++) {
					for (byte x = 0; x < 4; x++)
						pixels[blockPtr++] = stream.readByte();

					blockPtr += rowInc;
				}
				ADVANCE_BLOCK();
			}
			break;

		case 0xF0:
			warning("0xF0 opcode seen in SMC chunk (contact the developers)");
			break;
		}
	}

	return _surface;
}

} // End of namespace Image
