/* 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 "sci/resource.h"
#include "sci/engine/seg_manager.h"
#include "sci/engine/state.h"
#include "sci/graphics/celobj32.h"
#include "sci/graphics/frameout.h"
#include "sci/graphics/palette32.h"
#include "sci/graphics/picture.h"
#include "sci/graphics/view.h"

namespace Sci {
#pragma mark CelScaler
CelScaler *CelObj::_scaler = nullptr;

void CelScaler::activateScaleTables(const Ratio &scaleX, const Ratio &scaleY) {
	const int16 screenWidth = g_sci->_gfxFrameout->getCurrentBuffer().screenWidth;
	const int16 screenHeight = g_sci->_gfxFrameout->getCurrentBuffer().screenHeight;

	for (int i = 0; i < ARRAYSIZE(_scaleTables); ++i) {
		if (_scaleTables[i].scaleX == scaleX && _scaleTables[i].scaleY == scaleY) {
			_activeIndex = i;
			return;
		}
	}

	int i = 1 - _activeIndex;
	_activeIndex = i;
	CelScalerTable &table = _scaleTables[i];

	if (table.scaleX != scaleX) {
		assert(screenWidth <= ARRAYSIZE(table.valuesX));
		buildLookupTable(table.valuesX, scaleX, screenWidth);
		table.scaleX = scaleX;
	}

	if (table.scaleY != scaleY) {
		assert(screenHeight <= ARRAYSIZE(table.valuesY));
		buildLookupTable(table.valuesY, scaleY, screenHeight);
		table.scaleY = scaleY;
	}
}

void CelScaler::buildLookupTable(int *table, const Ratio &ratio, const int size) {
	int value = 0;
	int remainder = 0;
	int num = ratio.getNumerator();
	for (int i = 0; i < size; ++i) {
		*table++ = value;
		remainder += ratio.getDenominator();
		if (remainder >= num) {
			value += remainder / num;
			remainder %= num;
		}
	}
}

const CelScalerTable *CelScaler::getScalerTable(const Ratio &scaleX, const Ratio &scaleY) {
	activateScaleTables(scaleX, scaleY);
	return &_scaleTables[_activeIndex];
}

#pragma mark -
#pragma mark CelObj

void CelObj::init() {
	_nextCacheId = 1;
	delete _scaler;
	_scaler = new CelScaler();
	delete _cache;
	_cache = new CelCache;
	_cache->resize(100);
}

void CelObj::deinit() {
	delete _scaler;
	_scaler = nullptr;
	delete _cache;
	_cache = nullptr;
}

#pragma mark -
#pragma mark CelObj - Scalers

template <bool FLIP, typename READER>
struct SCALER_NoScale {
	const byte *_row;
	READER _reader;
	const int16 _lastIndex;

	SCALER_NoScale(const CelObj &celObj, const int16 maxWidth) :
	_reader(celObj, FLIP ? celObj._width : maxWidth),
	_lastIndex(celObj._width - 1) {}

	inline void setSource(const int16 x, const int16 y) {
		_row = _reader.getRow(y);

		if (FLIP) {
			_row += _lastIndex - x;
		} else {
			_row += x;
		}
	}

	inline byte read() {
		if (FLIP) {
			return *_row--;
		} else {
			return *_row++;
		}
	}
};

template<bool FLIP, typename READER>
struct SCALER_Scale {
	const byte *_row;
	READER _reader;
	const CelScalerTable *_table;
	int16 _x;
	const uint16 _lastIndex;

	SCALER_Scale(const CelObj &celObj, const int16 maxWidth, const Ratio scaleX, const Ratio scaleY) :
	// The maximum width of the scaled object may not be as
	// wide as the source data it requires if downscaling,
	// so just always make the reader decompress an entire
	// line of source data when scaling
	_reader(celObj, celObj._width),
	_table(CelObj::_scaler->getScalerTable(scaleX, scaleY)),
	_lastIndex(maxWidth - 1) {}

	inline void setSource(const int16 x, const int16 y) {
		_row = _reader.getRow(_table->valuesY[y]);
		if (FLIP) {
			_x = _lastIndex - x;
		} else {
			_x = x;
		}
	}

	inline byte read() {
		if (FLIP) {
			return _row[_table->valuesX[_x--]];
		} else {
			return _row[_table->valuesX[_x++]];
		}
	}
};

#pragma mark -
#pragma mark CelObj - Resource readers

struct READER_Uncompressed {
private:
	byte *_pixels;
	const int16 _sourceWidth;

public:
	READER_Uncompressed(const CelObj &celObj, const int16) :
	_sourceWidth(celObj._width) {
		byte *resource = celObj.getResPointer();
		_pixels = resource + READ_SCI11ENDIAN_UINT32(resource + celObj._celHeaderOffset + 24);
	}

	inline const byte *getRow(const int16 y) const {
		return _pixels + y * _sourceWidth;
	}
};

struct READER_Compressed {
private:
	byte *_resource;
	byte _buffer[1024];
	uint32 _controlOffset;
	uint32 _dataOffset;
	uint32 _uncompressedDataOffset;
	int16 _y;
	const int16 _sourceHeight;
	const uint8 _transparentColor;
	const int16 _maxWidth;

public:
	READER_Compressed(const CelObj &celObj, const int16 maxWidth) :
	_resource(celObj.getResPointer()),
	_y(-1),
	_sourceHeight(celObj._height),
	_transparentColor(celObj._transparentColor),
	_maxWidth(maxWidth) {
		assert(_maxWidth <= celObj._width);

		byte *celHeader = _resource + celObj._celHeaderOffset;
		_dataOffset = READ_SCI11ENDIAN_UINT32(celHeader + 24);
		_uncompressedDataOffset = READ_SCI11ENDIAN_UINT32(celHeader + 28);
		_controlOffset = READ_SCI11ENDIAN_UINT32(celHeader + 32);
	}

	inline const byte *getRow(const int16 y) {
		if (y != _y) {
			// compressed data segment for row
			byte *row = _resource + _dataOffset + READ_SCI11ENDIAN_UINT32(_resource + _controlOffset + y * 4);

			// uncompressed data segment for row
			byte *literal = _resource + _uncompressedDataOffset + READ_SCI11ENDIAN_UINT32(_resource + _controlOffset + _sourceHeight * 4 + y * 4);

			uint8 length;
			for (int i = 0; i < _maxWidth; i += length) {
				byte controlByte = *row++;
				length = controlByte;

				// Run-length encoded
				if (controlByte & 0x80) {
					length &= 0x3F;
					assert(i + length < (int)sizeof(_buffer));

					// Fill with skip color
					if (controlByte & 0x40) {
						memset(_buffer + i, _transparentColor, length);
					// Next value is fill color
					} else {
						memset(_buffer + i, *literal, length);
						++literal;
					}
				// Uncompressed
				} else {
					assert(i + length < (int)sizeof(_buffer));
					memcpy(_buffer + i, literal, length);
					literal += length;
				}
			}
			_y = y;
		}

		return _buffer;
	}
};

#pragma mark -
#pragma mark CelObj - Remappers

struct MAPPER_NoMD {
	inline void draw(byte *target, const byte pixel, const uint8 skipColor) const {
		if (pixel != skipColor) {
			*target = pixel;
		}
	}
};
struct MAPPER_NoMDNoSkip {
	inline void draw(byte *target, const byte pixel, const uint8) const {
		*target = pixel;
	}
};

void CelObj::draw(Buffer &target, const ScreenItem &screenItem, const Common::Rect &targetRect) const {
	const Buffer &priorityMap = g_sci->_gfxFrameout->getPriorityMap();
	const Common::Point &scaledPosition = screenItem._scaledPosition;
	const Ratio &scaleX = screenItem._ratioX;
	const Ratio &scaleY = screenItem._ratioY;

	if (_remap) {
		if (g_sci->_gfxFrameout->_hasRemappedScreenItem) {
			const uint8 priority = MAX((int16)0, MIN((int16)255, screenItem._priority));

			// NOTE: In the original engine code, there was a second branch for
			// _remap here that would then call the following functions if _remap was false:
			//
			// drawHzFlip(Buffer &, Buffer &, Common::Rect &, Common::Point &, uint8)
			// drawNoFlip(Buffer &, Buffer &, Common::Rect &, Common::Point &, uint8)
			// drawUncompHzFlip(Buffer &, Buffer &, Common::Rect &, Common::Point &, uint8)
			// drawUncompNoFlip(Buffer &, Buffer &, Common::Rect &, Common::Point &, uint8)
			// scaleDraw(Buffer &, Buffer &, Ratio &, Ratio &, Common::Rect &, Common::Point &, uint8)
			// scaleDrawUncomp(Buffer &, Buffer &, Ratio &, Ratio &, Common::Rect &, Common::Point &, uint8)
			//
			// However, obviously, _remap cannot be false here. This dead code branch existed in
			// at least SCI2/GK1 and SCI2.1/SQ6.

			if (scaleX.isOne() && scaleY.isOne()) {
				if (_compressionType == kCelCompressionNone) {
					if (_drawMirrored) {
						drawUncompHzFlipMap(target, priorityMap, targetRect, scaledPosition, priority);
					} else {
						drawUncompNoFlipMap(target, priorityMap, targetRect, scaledPosition, priority);
					}
				} else {
					if (_drawMirrored) {
						drawHzFlipMap(target, priorityMap, targetRect, scaledPosition, priority);
					} else {
						drawNoFlipMap(target, priorityMap, targetRect, scaledPosition, priority);
					}
				}
			} else {
				if (_compressionType == kCelCompressionNone) {
					scaleDrawUncompMap(target, priorityMap, scaleX, scaleY, targetRect, scaledPosition, priority);
				} else {
					scaleDrawMap(target, priorityMap, scaleX, scaleY, targetRect, scaledPosition, priority);
				}
			}
		} else {
			// NOTE: In the original code this check was `g_Remap_numActiveRemaps && _remap`,
			// but since we are already in a `_remap` branch, there is no reason to check it
			// again
			if (/* TODO: g_Remap_numActiveRemaps */ false) {
				if (scaleX.isOne() && scaleY.isOne()) {
					if (_compressionType == kCelCompressionNone) {
						if (_drawMirrored) {
							drawUncompHzFlipMap(target, targetRect, scaledPosition);
						} else {
							drawUncompNoFlipMap(target, targetRect, scaledPosition);
						}
					} else {
						if (_drawMirrored) {
							drawHzFlipMap(target, targetRect, scaledPosition);
						} else {
							drawNoFlipMap(target, targetRect, scaledPosition);
						}
					}
				} else {
					if (_compressionType == kCelCompressionNone) {
						scaleDrawUncompMap(target, scaleX, scaleY, targetRect, scaledPosition);
					} else {
						scaleDrawMap(target, scaleX, scaleY, targetRect, scaledPosition);
					}
				}
			} else {
				if (scaleX.isOne() && scaleY.isOne()) {
					if (_compressionType == kCelCompressionNone) {
						if (_drawMirrored) {
							drawUncompHzFlip(target, targetRect, scaledPosition);
						} else {
							drawUncompNoFlip(target, targetRect, scaledPosition);
						}
					} else {
						if (_drawMirrored) {
							drawHzFlip(target, targetRect, scaledPosition);
						} else {
							drawNoFlip(target, targetRect, scaledPosition);
						}
					}
				} else {
					if (_compressionType == kCelCompressionNone) {
						scaleDrawUncomp(target, scaleX, scaleY, targetRect, scaledPosition);
					} else {
						scaleDraw(target, scaleX, scaleY, targetRect, scaledPosition);
					}
				}
			}
		}
	} else {
		if (g_sci->_gfxFrameout->_hasRemappedScreenItem) {
			const uint8 priority = MAX((int16)0, MIN((int16)255, screenItem._priority));
			if (scaleX.isOne() && scaleY.isOne()) {
				if (_compressionType == kCelCompressionNone) {
					if (_drawMirrored) {
						drawUncompHzFlipNoMD(target, priorityMap, targetRect, scaledPosition, priority);
					} else {
						drawUncompNoFlipNoMD(target, priorityMap, targetRect, scaledPosition, priority);
					}
				} else {
					if (_drawMirrored) {
						drawHzFlipNoMD(target, priorityMap, targetRect, scaledPosition, priority);
					} else {
						drawNoFlipNoMD(target, priorityMap, targetRect, scaledPosition, priority);
					}
				}
			} else {
				if (_compressionType == kCelCompressionNone) {
					scaleDrawUncompNoMD(target, priorityMap, scaleX, scaleY, targetRect, scaledPosition, priority);
				} else {
					scaleDrawNoMD(target, priorityMap, scaleX, scaleY, targetRect, scaledPosition, priority);
				}
			}
		} else {
			if (scaleX.isOne() && scaleY.isOne()) {
				if (_compressionType == kCelCompressionNone) {
					if (_transparent) {
						if (_drawMirrored) {
							drawUncompHzFlipNoMD(target, targetRect, scaledPosition);
						} else {
							drawUncompNoFlipNoMD(target, targetRect, scaledPosition);
						}
					} else {
						if (_drawMirrored) {
							drawUncompHzFlipNoMDNoSkip(target, targetRect, scaledPosition);
						} else {
							drawUncompNoFlipNoMDNoSkip(target, targetRect, scaledPosition);
						}
					}
				} else {
					if (_drawMirrored) {
						drawHzFlipNoMD(target, targetRect, scaledPosition);
					} else {
						drawNoFlipNoMD(target, targetRect, scaledPosition);
					}
				}
			} else {
				if (_compressionType == kCelCompressionNone) {
					scaleDrawUncompNoMD(target, scaleX, scaleY, targetRect, scaledPosition);
				} else {
					scaleDrawNoMD(target, scaleX, scaleY, targetRect, scaledPosition);
				}
			}
		}
	}
}

void CelObj::draw(Buffer &target, const ScreenItem &screenItem, const Common::Rect &targetRect, bool mirrorX) {
	_drawMirrored = mirrorX;
	draw(target, screenItem, targetRect);
}

void CelObj::draw(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition, const bool mirrorX) {
	_drawMirrored = mirrorX;
	Ratio square;
	drawTo(target, targetRect, scaledPosition, square, square);
}

void CelObj::drawTo(Buffer &target, Common::Rect const &targetRect, Common::Point const &scaledPosition, Ratio const &scaleX, Ratio const &scaleY) const {
	if (_remap) {
		if (scaleX.isOne() && scaleY.isOne()) {
			if (_compressionType == kCelCompressionNone) {
				if (_drawMirrored) {
					drawUncompHzFlipMap(target, targetRect, scaledPosition);
				} else {
					drawUncompNoFlipMap(target, targetRect, scaledPosition);
				}
			} else {
				if (_drawMirrored) {
					drawHzFlipMap(target, targetRect, scaledPosition);
				} else {
					drawNoFlipMap(target, targetRect, scaledPosition);
				}
			}
		} else {
			if (_compressionType == kCelCompressionNone) {
				scaleDrawUncompMap(target, scaleX, scaleY, targetRect, scaledPosition);
			} else {
				scaleDrawMap(target, scaleX, scaleY, targetRect, scaledPosition);
			}
		}
	} else {
		if (scaleX.isOne() && scaleY.isOne()) {
			if (_compressionType == kCelCompressionNone) {
				if (_drawMirrored) {
					drawUncompHzFlipNoMD(target, targetRect, scaledPosition);
				} else {
					drawUncompNoFlipNoMD(target, targetRect, scaledPosition);
				}
			} else {
				if (_drawMirrored) {
					drawHzFlipNoMD(target, targetRect, scaledPosition);
				} else {
					drawNoFlipNoMD(target, targetRect, scaledPosition);
				}
			}
		} else {
			if (_compressionType == kCelCompressionNone) {
				scaleDrawUncompNoMD(target, scaleX, scaleY, targetRect, scaledPosition);
			} else {
				scaleDrawNoMD(target, scaleX, scaleY, targetRect, scaledPosition);
			}
		}
	}
}

uint8 CelObj::readPixel(uint16 x, const uint16 y, bool mirrorX) const {
	if (mirrorX) {
		x = _width - x - 1;
	}

	if (_compressionType == kCelCompressionNone) {
		READER_Uncompressed reader(*this, x + 1);
		return reader.getRow(y)[x];
	} else {
		READER_Compressed reader(*this, x + 1);
		return reader.getRow(y)[x];
	}
}

void CelObj::submitPalette() const {
	if (_hunkPaletteOffset) {
		Palette palette;

		byte *res = getResPointer();
		// NOTE: In SCI engine this uses HunkPalette::Init.
		// TODO: Use a better size value
		g_sci->_gfxPalette32->createFromData(res + _hunkPaletteOffset, 999999, &palette);
		g_sci->_gfxPalette32->submit(palette);
	}
}

#pragma mark -
#pragma mark CelObj - Caching
int CelObj::_nextCacheId = 1;
CelCache *CelObj::_cache = nullptr;

int CelObj::searchCache(const CelInfo32 &celInfo, int *nextInsertIndex) const {
	int oldestId = _nextCacheId + 1;
	int oldestIndex = -1;

	for (int i = 0, len = _cache->size(); i < len; ++i) {
		CelCacheEntry &entry = (*_cache)[i];

		if (entry.celObj != nullptr) {
			if (entry.celObj->_info == celInfo) {
				entry.id = ++_nextCacheId;
				return i;
			}

			if (oldestId > entry.id) {
				oldestId = entry.id;
				oldestIndex = i;
			}
		} else if (oldestIndex == -1) {
			oldestIndex = i;
		}
	}

	// NOTE: Unlike the original SCI engine code, the out-param
	// here is only updated if there was not a cache hit.
	*nextInsertIndex = oldestIndex;
	return -1;
}

void CelObj::putCopyInCache(const int cacheIndex) const {
	if (cacheIndex == -1) {
		error("Invalid cache index");
	}

	CelCacheEntry &entry = (*_cache)[cacheIndex];

	if (entry.celObj != nullptr) {
		delete entry.celObj;
	}

	entry.celObj = duplicate();
	entry.id = ++_nextCacheId;
}

#pragma mark -
#pragma mark CelObj - Drawing

template <typename MAPPER, typename SCALER>
struct RENDERER {
	MAPPER &_mapper;
	SCALER &_scaler;
	const uint8 _skipColor;

	RENDERER(MAPPER &mapper, SCALER &scaler, const uint8 skipColor) :
	_mapper(mapper),
	_scaler(scaler),
	_skipColor(skipColor) {}

	inline void draw(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
		const int16 sourceX = targetRect.left - scaledPosition.x;
		const int16 sourceY = targetRect.top - scaledPosition.y;

		byte *targetPixel = (byte *)target.getPixels() + target.screenWidth * targetRect.top + targetRect.left;

		const int16 skipStride = target.screenWidth - targetRect.width();
		const int16 targetWidth = targetRect.width();
		const int16 targetHeight = targetRect.height();
		for (int y = 0; y < targetHeight; ++y) {
			_scaler.setSource(sourceX, sourceY + y);

			for (int x = 0; x < targetWidth; ++x) {
				_mapper.draw(targetPixel++, _scaler.read(), _skipColor);
			}

			targetPixel += skipStride;
		}
	}
};

template <typename MAPPER, typename SCALER>
void CelObj::render(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {

	MAPPER mapper;
	SCALER scaler(*this, targetRect.left - scaledPosition.x + targetRect.width());
	RENDERER<MAPPER, SCALER> renderer(mapper, scaler, _transparentColor);
	renderer.draw(target, targetRect, scaledPosition);
}

template <typename MAPPER, typename SCALER>
void CelObj::render(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition, const Ratio &scaleX, const Ratio &scaleY) const {

	MAPPER mapper;
	SCALER scaler(*this, targetRect.left - scaledPosition.x + targetRect.width(), scaleX, scaleY);
	RENDERER<MAPPER, SCALER> renderer(mapper, scaler, _transparentColor);
	renderer.draw(target, targetRect, scaledPosition);
}

void dummyFill(Buffer &target, const Common::Rect &targetRect) {
	target.fillRect(targetRect, 250);
}

void CelObj::drawHzFlip(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	debug("drawHzFlip");
	dummyFill(target, targetRect);
}
void CelObj::drawNoFlip(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	debug("drawNoFlip");
	dummyFill(target, targetRect);
}
void CelObj::drawUncompNoFlip(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	debug("drawUncompNoFlip");
	dummyFill(target, targetRect);
}
void CelObj::drawUncompHzFlip(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	debug("drawUncompHzFlip");
	dummyFill(target, targetRect);
}
void CelObj::scaleDraw(Buffer &target, const Ratio &scaleX, const Ratio &scaleY, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	debug("scaleDraw");
	dummyFill(target, targetRect);
}
void CelObj::scaleDrawUncomp(Buffer &target, const Ratio &scaleX, const Ratio &scaleY, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	debug("scaleDrawUncomp");
	dummyFill(target, targetRect);
}
void CelObj::drawHzFlipMap(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	debug("drawHzFlipMap");
	dummyFill(target, targetRect);
}
void CelObj::drawNoFlipMap(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	debug("drawNoFlipMap");
	dummyFill(target, targetRect);
}
void CelObj::drawUncompNoFlipMap(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	debug("drawUncompNoFlipMap");
	dummyFill(target, targetRect);
}
void CelObj::drawUncompHzFlipMap(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	debug("drawUncompHzFlipMap");
	dummyFill(target, targetRect);
}
void CelObj::scaleDrawMap(Buffer &target, const Ratio &scaleX, const Ratio &scaleY, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	debug("scaleDrawMap");
	dummyFill(target, targetRect);
}
void CelObj::scaleDrawUncompMap(Buffer &target, const Ratio &scaleX, const Ratio &scaleY, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	debug("scaleDrawUncompMap");
	dummyFill(target, targetRect);
}

void CelObj::drawNoFlipNoMD(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	render<MAPPER_NoMD, SCALER_NoScale<false, READER_Compressed> >(target, targetRect, scaledPosition);
}

void CelObj::drawHzFlipNoMD(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	render<MAPPER_NoMD, SCALER_NoScale<true, READER_Compressed> >(target, targetRect, scaledPosition);
}

void CelObj::drawUncompNoFlipNoMD(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	render<MAPPER_NoMD, SCALER_NoScale<false, READER_Uncompressed> >(target, targetRect, scaledPosition);
}

void CelObj::drawUncompNoFlipNoMDNoSkip(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	render<MAPPER_NoMDNoSkip, SCALER_NoScale<false, READER_Uncompressed> >(target, targetRect, scaledPosition);
}
void CelObj::drawUncompHzFlipNoMD(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	render<MAPPER_NoMD, SCALER_NoScale<true, READER_Uncompressed> >(target, targetRect, scaledPosition);
}
void CelObj::drawUncompHzFlipNoMDNoSkip(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	render<MAPPER_NoMDNoSkip, SCALER_NoScale<true, READER_Uncompressed> >(target, targetRect, scaledPosition);
}

void CelObj::scaleDrawNoMD(Buffer &target, const Ratio &scaleX, const Ratio &scaleY, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	if (_drawMirrored) {
		render<MAPPER_NoMD, SCALER_Scale<true, READER_Compressed> >(target, targetRect, scaledPosition, scaleX, scaleY);
	} else {
		render<MAPPER_NoMD, SCALER_Scale<false, READER_Compressed> >(target, targetRect, scaledPosition, scaleX, scaleY);
	}
}

void CelObj::scaleDrawUncompNoMD(Buffer &target, const Ratio &scaleX, const Ratio &scaleY, const Common::Rect &targetRect, const Common::Point &scaledPosition) const {
	if (_drawMirrored) {
		render<MAPPER_NoMD, SCALER_Scale<true, READER_Uncompressed> >(target, targetRect, scaledPosition, scaleX, scaleY);
	} else {
		render<MAPPER_NoMD, SCALER_Scale<false, READER_Uncompressed> >(target, targetRect, scaledPosition, scaleX, scaleY);
	}
}

// TODO: These functions may all be vestigial.
void CelObj::drawHzFlipMap(Buffer &target, const Buffer &priorityMap, const Common::Rect &targetRect, const Common::Point &scaledPosition, const uint8 priority) const {}
void CelObj::drawNoFlipMap(Buffer &target, const Buffer &priorityMap, const Common::Rect &targetRect, const Common::Point &scaledPosition, const uint8 priority) const {}
void CelObj::drawUncompNoFlipMap(Buffer &target, const Buffer &priorityMap, const Common::Rect &targetRect, const Common::Point &scaledPosition, const uint8 priority) const {}
void CelObj::drawUncompHzFlipMap(Buffer &target, const Buffer &priorityMap, const Common::Rect &targetRect, const Common::Point &scaledPosition, const uint8 priority) const {}
void CelObj::scaleDrawMap(Buffer &target, const Buffer &priorityMap, const Ratio &scaleX, const Ratio &scaleY, const Common::Rect &targetRect, const Common::Point &scaledPosition, const uint8 priority) const {}
void CelObj::scaleDrawUncompMap(Buffer &target, const Buffer &priorityMap, const Ratio &scaleX, const Ratio &scaleY, const Common::Rect &targetRect, const Common::Point &scaledPosition, const uint8 priority) const {}
void CelObj::drawHzFlipNoMD(Buffer &target, const Buffer &priorityMap, const Common::Rect &targetRect, const Common::Point &scaledPosition, const uint8 priority) const {}
void CelObj::drawNoFlipNoMD(Buffer &target, const Buffer &priorityMap, const Common::Rect &targetRect, const Common::Point &scaledPosition, const uint8 priority) const {}
void CelObj::drawUncompNoFlipNoMD(Buffer &target, const Buffer &priorityMap, const Common::Rect &targetRect, const Common::Point &scaledPosition, const uint8 priority) const {}
void CelObj::drawUncompHzFlipNoMD(Buffer &target, const Buffer &priorityMap, const Common::Rect &targetRect, const Common::Point &scaledPosition, const uint8 priority) const {}
void CelObj::scaleDrawNoMD(Buffer &target, const Buffer &priorityMap, const Ratio &scaleX, const Ratio &scaleY, const Common::Rect &targetRect, const Common::Point &scaledPosition, const uint8 priority) const {}
void CelObj::scaleDrawUncompNoMD(Buffer &target, const Buffer &priorityMap, const Ratio &scaleX, const Ratio &scaleY, const Common::Rect &targetRect, const Common::Point &scaledPosition, const uint8 priority) const {}

#pragma mark -
#pragma mark CelObjView
CelObjView::CelObjView(const GuiResourceId viewId, const int16 loopNo, const int16 celNo) {
	_info.type = kCelTypeView;
	_info.resourceId = viewId;
	_info.loopNo = loopNo;
	_info.celNo = celNo;
	_mirrorX = false;
	_compressionType = kCelCompressionInvalid;
	_transparent = true;

	int cacheInsertIndex;
	int cacheIndex = searchCache(_info, &cacheInsertIndex);
	if (cacheIndex != -1) {
		CelCacheEntry &entry = (*_cache)[cacheIndex];
		*this = *dynamic_cast<CelObjView *>(entry.celObj);
		entry.id = ++_nextCacheId;
		return;
	}

	// TODO: The next code should be moved to a common file that
	// generates view resource metadata for both SCI16 and SCI32
	// implementations

	Resource *resource = g_sci->getResMan()->findResource(ResourceId(kResourceTypeView, viewId), false);

	// NOTE: SCI2.1/SQ6 just silently returns here.
	if (!resource) {
		warning("View resource %d not loaded", viewId);
		return;
	}

	byte *data = resource->data;

	_scaledWidth = READ_SCI11ENDIAN_UINT16(data + 14);
	_scaledHeight = READ_SCI11ENDIAN_UINT16(data + 16);

	if (_scaledWidth == 0 || _scaledHeight == 0) {
		byte sizeFlag = data[5];
		if (sizeFlag == 0) {
			_scaledWidth = 320;
			_scaledHeight = 200;
		} else if (sizeFlag == 1) {
			_scaledWidth = 640;
			_scaledHeight = 480;
		} else if (sizeFlag == 2) {
			_scaledWidth = 640;
			_scaledHeight = 400;
		}
	}

	uint16 loopCount = data[2];
	if (_info.loopNo >= loopCount) {
		_info.loopNo = loopCount - 1;
	}

	// NOTE: This is the actual check, in the actual location,
	// from SCI engine.
	if (loopNo < 0) {
		error("Loop is less than 0!");
	}

	const uint16 viewHeaderSize = READ_SCI11ENDIAN_UINT16(data);
	const uint8 loopHeaderSize = data[12];
	const uint8 viewHeaderFieldSize = 2;

	byte *loopHeader = data + viewHeaderFieldSize + viewHeaderSize + (loopHeaderSize * _info.loopNo);

	if ((int8)loopHeader[0] != -1) {
		if (loopHeader[1] == 1) {
			_mirrorX = true;
		}

		loopHeader = data + viewHeaderFieldSize + viewHeaderSize + (loopHeaderSize * (int8)loopHeader[0]);
	}

	uint8 celCount = loopHeader[2];
	if (_info.celNo >= celCount) {
		_info.celNo = celCount - 1;
	}

	_hunkPaletteOffset = READ_SCI11ENDIAN_UINT32(data + 8);
	_celHeaderOffset = READ_SCI11ENDIAN_UINT32(loopHeader + 12) + (data[13] * _info.celNo);

	byte *celHeader = data + _celHeaderOffset;

	_width = READ_SCI11ENDIAN_UINT16(celHeader);
	_height = READ_SCI11ENDIAN_UINT16(celHeader + 2);
	_displace.x = _width / 2 - (int16)READ_SCI11ENDIAN_UINT16(celHeader + 4);
	_displace.y = _height - (int16)READ_SCI11ENDIAN_UINT16(celHeader + 6) - 1;
	_transparentColor = celHeader[8];
	_compressionType = (CelCompressionType)celHeader[9];

	if (_compressionType != kCelCompressionNone && _compressionType != kCelCompressionRLE) {
		error("Compression type not supported - V: %d  L: %d  C: %d", _info.resourceId, _info.loopNo, _info.celNo);
	}

	if (celHeader[10] & 128) {
		// NOTE: This is correct according to SCI2.1/SQ6/DOS;
		// the engine re-reads the byte value as a word value
		uint16 flags = READ_SCI11ENDIAN_UINT16(celHeader + 10);
		_transparent = flags & 1 ? true : false;
		_remap = flags & 2 ? true : false;
	} else if (_compressionType == kCelCompressionNone) {
		_remap = analyzeUncompressedForRemap();
	} else {
		_remap = analyzeForRemap();
	}

	putCopyInCache(cacheInsertIndex);
}

bool CelObjView::analyzeUncompressedForRemap() const {
	byte *pixels = getResPointer() + READ_SCI11ENDIAN_UINT32(getResPointer() + _celHeaderOffset + 24);
	for (int i = 0; i < _width * _height; ++i) {
		uint8 pixel = pixels[i];
		if (/* TODO: pixel >= Remap::minRemapColor && pixel <= Remap::maxRemapColor */ false && pixel != _transparentColor) {
			return true;
		}
	}
	return false;
}

bool CelObjView::analyzeForRemap() const {
	// TODO: Implement decompression and analysis
	return false;
}

void CelObjView::draw(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition, bool mirrorX, const Ratio &scaleX, const Ratio &scaleY) {
	_drawMirrored = mirrorX;
	drawTo(target, targetRect, scaledPosition, scaleX, scaleY);
}

CelObjView *CelObjView::duplicate() const {
	return new CelObjView(*this);
}

byte *CelObjView::getResPointer() const {
	return g_sci->getResMan()->findResource(ResourceId(kResourceTypeView, _info.resourceId), false)->data;
}

#pragma mark -
#pragma mark CelObjPic
CelObjPic::CelObjPic(const GuiResourceId picId, const int16 celNo) {
	_info.type = kCelTypePic;
	_info.resourceId = picId;
	_info.loopNo = 0;
	_info.celNo = celNo;
	_mirrorX = false;
	_compressionType = kCelCompressionInvalid;
	_transparent = true;
	_remap = false;

	int cacheInsertIndex;
	int cacheIndex = searchCache(_info, &cacheInsertIndex);
	if (cacheIndex != -1) {
		CelCacheEntry &entry = (*_cache)[cacheIndex];
		*this = *dynamic_cast<CelObjPic *>(entry.celObj);
		entry.id = ++_nextCacheId;
		return;
	}

	Resource *resource = g_sci->getResMan()->findResource(ResourceId(kResourceTypePic, picId), false);

	// NOTE: SCI2.1/SQ6 just silently returns here.
	if (!resource) {
		warning("Pic resource %d not loaded", picId);
		return;
	}

	byte *data = resource->data;

	_celCount = data[2];

	if (_info.celNo >= _celCount) {
		error("Cel number %d greater than cel count %d", _info.celNo, _celCount);
	}

	_celHeaderOffset = READ_SCI11ENDIAN_UINT16(data) + (READ_SCI11ENDIAN_UINT16(data + 4) * _info.celNo);
	_hunkPaletteOffset = READ_SCI11ENDIAN_UINT32(data + 6);

	byte *celHeader = data + _celHeaderOffset;

	_width = READ_SCI11ENDIAN_UINT16(celHeader);
	_height = READ_SCI11ENDIAN_UINT16(celHeader + 2);
	_displace.x = (int16)READ_SCI11ENDIAN_UINT16(celHeader + 4);
	_displace.y = (int16)READ_SCI11ENDIAN_UINT16(celHeader + 6);
	_transparentColor = celHeader[8];
	_compressionType = (CelCompressionType)celHeader[9];
	_priority = READ_SCI11ENDIAN_UINT16(celHeader + 36);
	_relativePosition.x = (int16)READ_SCI11ENDIAN_UINT16(celHeader + 38);
	_relativePosition.y = (int16)READ_SCI11ENDIAN_UINT16(celHeader + 40);

	uint16 sizeFlag1 = READ_SCI11ENDIAN_UINT16(data + 10);
	uint16 sizeFlag2 = READ_SCI11ENDIAN_UINT16(data + 12);

	if (sizeFlag2) {
		_scaledWidth = sizeFlag1;
		_scaledHeight = sizeFlag2;
	} else if (sizeFlag1 == 0) {
		_scaledWidth = 320;
		_scaledHeight = 200;
	} else if (sizeFlag1 == 1) {
		_scaledWidth = 640;
		_scaledHeight = 480;
	} else if (sizeFlag1 == 2) {
		_scaledWidth = 640;
		_scaledHeight = 400;
	}

	if (celHeader[10] & 128) {
		// NOTE: This is correct according to SCI2.1/SQ6/DOS;
		// the engine re-reads the byte value as a word value
		uint16 flags = READ_SCI11ENDIAN_UINT16(celHeader + 10);
		_transparent = flags & 1 ? true : false;
		_remap = flags & 2 ? true : false;
	} else {
		_transparent = _compressionType != kCelCompressionNone ? true : analyzeUncompressedForSkip();

		if (_compressionType != kCelCompressionNone && _compressionType != kCelCompressionRLE) {
			error("Compression type not supported - P: %d  C: %d", picId, celNo);
		}
	}

	putCopyInCache(cacheInsertIndex);
}

bool CelObjPic::analyzeUncompressedForSkip() const {
	byte *resource = getResPointer();
	byte *pixels = resource + READ_SCI11ENDIAN_UINT32(resource + _celHeaderOffset + 24);
	for (int i = 0; i < _width * _height; ++i) {
		uint8 pixel = pixels[i];
		if (pixel == _transparentColor) {
			return true;
		}
	}

	return false;
}

void CelObjPic::draw(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition, const bool mirrorX) {
	Ratio square;
	_drawMirrored = mirrorX;
	drawTo(target, targetRect, scaledPosition, square, square);
}

CelObjPic *CelObjPic::duplicate() const {
	return new CelObjPic(*this);
}

byte *CelObjPic::getResPointer() const {
	return g_sci->getResMan()->findResource(ResourceId(kResourceTypePic, _info.resourceId), false)->data;
}

#pragma mark -
#pragma mark CelObjMem
void CelObjMem::buildBitmapHeader(byte *bitmap, const int16 width, const int16 height, const uint8 skipColor, const int16 displaceX, const int16 displaceY, const int16 scaledWidth, const int16 scaledHeight, const uint32 hunkPaletteOffset, const bool useRemap) {
	const uint16 bitmapHeaderSize = getBitmapHeaderSize();

	WRITE_SCI11ENDIAN_UINT16(bitmap + 0, width);
	WRITE_SCI11ENDIAN_UINT16(bitmap + 2, height);
	WRITE_SCI11ENDIAN_UINT16(bitmap + 4, (uint16)displaceX);
	WRITE_SCI11ENDIAN_UINT16(bitmap + 6, (uint16)displaceY);
	bitmap[8] = skipColor;
	bitmap[9] = 0;
	WRITE_SCI11ENDIAN_UINT16(bitmap + 10, 0);

	if (useRemap) {
		bitmap[10] |= 2;
	}

	WRITE_SCI11ENDIAN_UINT32(bitmap + 12, width * height);
	WRITE_SCI11ENDIAN_UINT32(bitmap + 16, 0);

	if (hunkPaletteOffset) {
		WRITE_SCI11ENDIAN_UINT32(bitmap + 20, hunkPaletteOffset + bitmapHeaderSize);
	} else {
		WRITE_SCI11ENDIAN_UINT32(bitmap + 20, 0);
	}

	WRITE_SCI11ENDIAN_UINT32(bitmap + 24, bitmapHeaderSize);
	WRITE_SCI11ENDIAN_UINT32(bitmap + 28, bitmapHeaderSize);
	WRITE_SCI11ENDIAN_UINT32(bitmap + 32, 0);

	if (bitmapHeaderSize >= 40) {
		WRITE_SCI11ENDIAN_UINT16(bitmap + 36, scaledWidth);
		WRITE_SCI11ENDIAN_UINT16(bitmap + 38, scaledHeight);
	}
}

CelObjMem::CelObjMem(const reg_t bitmap) {
	_info.type = kCelTypeMem;
	_info.bitmap = bitmap;
	_mirrorX = false;
	_compressionType = kCelCompressionNone;
	_celHeaderOffset = 0;
	_transparent = true;

	const uint32 bitmapHeaderSize = getBitmapHeaderSize();
	byte *bitmapData = g_sci->getEngineState()->_segMan->getHunkPointer(bitmap);
	if (bitmapData == nullptr || READ_SCI11ENDIAN_UINT32(bitmapData + 28) != bitmapHeaderSize) {
		error("Invalid Text bitmap %04x:%04x", PRINT_REG(bitmap));
	}

	_width = READ_SCI11ENDIAN_UINT16(bitmapData);
	_height = READ_SCI11ENDIAN_UINT16(bitmapData + 2);
	_displace.x = READ_SCI11ENDIAN_UINT16(bitmapData + 4);
	_displace.y = READ_SCI11ENDIAN_UINT16(bitmapData + 6);
	_transparentColor = bitmapData[8];
	if (bitmapHeaderSize >= 40) {
		_scaledWidth = READ_SCI11ENDIAN_UINT16(bitmapData + 36);
		_scaledHeight = READ_SCI11ENDIAN_UINT16(bitmapData + 38);
	} else {
		error("TODO: SCI2 bitmaps not implemented yet!");
	}
	_hunkPaletteOffset = READ_SCI11ENDIAN_UINT16(bitmapData + 20);
	_remap = (READ_SCI11ENDIAN_UINT16(bitmapData + 10) & 2) ? true : false;
}

CelObjMem *CelObjMem::duplicate() const {
	return new CelObjMem(*this);
}

byte *CelObjMem::getResPointer() const {
	return g_sci->getEngineState()->_segMan->getHunkPointer(_info.bitmap);
}

#pragma mark -
#pragma mark CelObjColor
CelObjColor::CelObjColor(const uint8 color, const int16 width, const int16 height) {
	_info.type = kCelTypeColor;
	_info.color = color;
	_displace.x = 0;
	_displace.y = 0;
	_scaledWidth = g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth;
	_scaledHeight = g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight;
	_hunkPaletteOffset = 0;
	_mirrorX = false;
	_remap = false;
	_width = width;
	_height = height;
}

void CelObjColor::draw(Buffer &target, const ScreenItem &screenItem, const Common::Rect &targetRect, const bool mirrorX) {
	// TODO: The original engine sets this flag but why? One cannot
	// draw a solid color mirrored.
	_drawMirrored = mirrorX;
	draw(target, targetRect);
}
void CelObjColor::draw(Buffer &target, const Common::Rect &targetRect, const Common::Point &scaledPosition, bool mirrorX) {
	error("Unsupported method");
}
void CelObjColor::draw(Buffer &target, const Common::Rect &targetRect) const {
	target.fillRect(targetRect, _info.color);
}

CelObjColor *CelObjColor::duplicate() const {
	return new CelObjColor(*this);
}

byte *CelObjColor::getResPointer() const {
	error("Unsupported method");
}
}