/* 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include "agi/agi.h"
#include "agi/graphics.h"
#include "agi/sprite.h"

namespace Agi {

void AgiEngine::updateView(ScreenObjEntry *screenObj) {
	int16 celNr, lastCelNr;

	if (screenObj->flags & fDontupdate) {
		screenObj->flags &= ~fDontupdate;
		return;
	}

	celNr = screenObj->currentCelNr;
	lastCelNr = screenObj->celCount - 1;

	switch (screenObj->cycle) {
	case kCycleNormal:
		celNr++;
		if (celNr > lastCelNr)
			celNr = 0;
		break;
	case kCycleEndOfLoop:
		if (celNr < lastCelNr) {
			debugC(5, kDebugLevelResources, "cel %d (last = %d)", celNr + 1, lastCelNr);
			if (++celNr != lastCelNr)
				break;
		}
		setFlag(screenObj->loop_flag, true);
		screenObj->flags &= ~fCycling;
		screenObj->direction = 0;
		screenObj->cycle = kCycleNormal;
		break;
	case kCycleRevLoop:
		if (celNr) {
			celNr--;
			if (celNr)
				break;
		}
		setFlag(screenObj->loop_flag, true);
		screenObj->flags &= ~fCycling;
		screenObj->direction = 0;
		screenObj->cycle = kCycleNormal;
		break;
	case kCycleReverse:
		if (celNr == 0) {
			celNr = lastCelNr;
		} else {
			celNr--;
		}
		break;
	default:
		break;
	}

	setCel(screenObj, celNr);
}

/*
 * Public functions
 */

/**
 * Decode an AGI view resource.
 * This function decodes the raw data of the specified AGI view resource
 * and fills the corresponding views array element.
 * @param n number of view resource to decode
 */
int AgiEngine::decodeView(byte *resourceData, uint16 resourceSize, int16 viewNr) {
	AgiView *viewData = &_game.views[viewNr];
	uint16 headerId = 0;
	byte   headerStepSize = 0;
	byte   headerCycleTime = 0;
	byte   headerLoopCount = 0;
	uint16 headerDescriptionOffset = 0;
	bool   isAGI256Data = false;

	AgiViewLoop *loopData = nullptr;
	uint16 loopOffset = 0;
	byte   loopHeaderCelCount = 0;

	AgiViewCel *celData = nullptr;
	uint16 celOffset = 0;
	byte   celHeaderWidth = 0;
	byte   celHeaderHeight = 0;
	byte   celHeaderTransparencyMirror = 0;
	byte   celHeaderClearKey = 0;
	bool   celHeaderMirrored = false;
	byte   celHeaderMirrorLoop = 0;

	byte  *celCompressedData = nullptr;
	uint16 celCompressedSize = 0;

	debugC(5, kDebugLevelResources, "decode_view(%d)", viewNr);

	if (resourceSize < 5)
		error("unexpected end of view data for view %d", viewNr);

	headerId = READ_LE_UINT16(resourceData);
	if (getVersion() < 0x2000) {
		headerStepSize = resourceData[0];
		headerCycleTime = resourceData[1];
	}
	headerLoopCount = resourceData[2];
	headerDescriptionOffset = READ_LE_UINT16(resourceData + 3);

	if (headerId == 0xF00F)
		isAGI256Data = true; // AGI 256-2 view detected, 256 color view

	viewData->headerStepSize = headerStepSize;
	viewData->headerCycleTime = headerCycleTime;
	viewData->loopCount = headerLoopCount;
	viewData->description = nullptr;
	viewData->loop = nullptr;

	if (headerDescriptionOffset) {
		// Figure out length of description
		uint16 descriptionPos = headerDescriptionOffset;
		uint16 descriptionLen = 0;
		while (descriptionPos < resourceSize) {
			if (resourceData[descriptionPos] == 0)
				break;
			descriptionPos++;
			descriptionLen++;
		}
		// Allocate memory for description
		viewData->description = new byte[descriptionLen + 1];
		// Copy description over
		memcpy(viewData->description, resourceData + headerDescriptionOffset, descriptionLen);
		viewData->description[descriptionLen] = 0; // set terminator
	}

	if (!viewData->loopCount) // no loops, exit now
		return errOK;

	// Check, if at least the loop-offsets are available
	if (resourceSize < 5 + (headerLoopCount * 2))
		error("unexpected end of view data for view %d", viewNr);

	// Allocate space for loop-information
	loopData = new AgiViewLoop[headerLoopCount];
	viewData->loop = loopData;

	for (int16 loopNr = 0; loopNr < headerLoopCount; loopNr++) {
		loopOffset = READ_LE_UINT16(resourceData + 5 + (loopNr * 2));

		// Check, if at least the loop-header is available
		if (resourceSize < (loopOffset + 1))
			error("unexpected end of view data for view %d", viewNr);

		// loop-header:
		//  celCount:BYTE
		//  relativeCelOffset[0]:WORD
		//  relativeCelOffset[1]:WORD
		//  etc.
		loopHeaderCelCount = resourceData[loopOffset];

		loopData->celCount = loopHeaderCelCount;
		loopData->cel = nullptr;

		// Check, if at least the cel-offsets for current loop are available
		if (resourceSize < (loopOffset + 1 + (loopHeaderCelCount * 2)))
			error("unexpected end of view data for view %d", viewNr);

		if (loopHeaderCelCount) {
			// Allocate space for cel-information of current loop
			celData = new AgiViewCel[loopHeaderCelCount];
			loopData->cel = celData;

			for (int16 celNr = 0; celNr < loopHeaderCelCount; celNr++) {
				celOffset = READ_LE_UINT16(resourceData + loopOffset + 1 + (celNr * 2));
				celOffset += loopOffset; // cel offset is relative to loop offset, so adjust accordingly

				// Check, if at least the cel-header is available
				if (resourceSize < (celOffset + 3))
					error("unexpected end of view data for view %d", viewNr);

				// cel-header:
				//  width:BYTE
				//  height:BYTE
				//  Transparency + Mirroring:BYTE
				//  celData follows
				celHeaderWidth = resourceData[celOffset + 0];
				celHeaderHeight = resourceData[celOffset + 1];
				celHeaderTransparencyMirror = resourceData[celOffset + 2];

				if (!isAGI256Data) {
					// regular AGI view data
					// Transparency + Mirroring byte is as follows:
					//  Bit 0-3 - clear key
					//  Bit 4-6 - original loop, that is not supposed to be mirrored in any case
					//  Bit 7   - apply mirroring
					celHeaderClearKey = celHeaderTransparencyMirror & 0x0F; // bit 0-3 is the clear key
					celHeaderMirrored = false;
					if (celHeaderTransparencyMirror & 0x80) {
						// mirror bit is set
						celHeaderMirrorLoop = (celHeaderTransparencyMirror >> 4) & 0x07;
						if (celHeaderMirrorLoop != loopNr) {
							// only set to mirror'd in case we are not the original loop
							celHeaderMirrored = true;
						}
					}
				} else {
					// AGI256-2 view data
					celHeaderClearKey = celHeaderTransparencyMirror; // full 8 bits for clear key
					celHeaderMirrored = false;
				}

				celData->width = celHeaderWidth;
				celData->height = celHeaderHeight;
				celData->clearKey = celHeaderClearKey;
				celData->mirrored = celHeaderMirrored;

				// Now decompress cel-data
				if ((celHeaderWidth == 0) && (celHeaderHeight == 0))
					error("view cel is 0x0");

				celCompressedData = resourceData + celOffset + 3;
				celCompressedSize = resourceSize - (celOffset + 3);

				if (celCompressedSize == 0)
					error("compressed size of loop within view %d is 0 bytes", viewNr);

				if (!isAGI256Data) {
					unpackViewCelData(celData, celCompressedData, celCompressedSize);
				} else {
					unpackViewCelDataAGI256(celData, celCompressedData, celCompressedSize);
				}
				celData++;
			}
		}

		loopData++;
	}

	return errOK;
}

void AgiEngine::unpackViewCelData(AgiViewCel *celData, byte *compressedData, uint16 compressedSize) {
	byte *rawBitmap = new byte[celData->width * celData->height];
	int16 remainingHeight = celData->height;
	int16 remainingWidth = celData->width;
	bool  isMirrored = celData->mirrored;
	byte curByte;
	byte curColor;
	byte curChunkLen;
	int16 adjustPreChangeSingle = 0;
	int16 adjustAfterChangeSingle = +1;

	celData->rawBitmap = rawBitmap;

	if (isMirrored) {
		adjustPreChangeSingle = -1;
		adjustAfterChangeSingle = 0;
		rawBitmap += celData->width;
	}

	while (remainingHeight) {
		if (!compressedSize)
			error("unexpected end of data, while unpacking AGI256 data");

		curByte = *compressedData++;
		compressedSize--;

		if (curByte == 0) {
			curColor = celData->clearKey;
			curChunkLen = remainingWidth;
		} else {
			curColor = curByte >> 4;
			curChunkLen = curByte & 0x0F;
			if (curChunkLen > remainingWidth)
				error("invalid chunk in view data");
		}

		switch (curChunkLen) {
		case 0:
			break;
		case 1:
			rawBitmap += adjustPreChangeSingle;
			*rawBitmap = curColor;
			rawBitmap += adjustAfterChangeSingle;
			break;
		default:
			if (isMirrored)
				rawBitmap -= curChunkLen;
			memset(rawBitmap, curColor, curChunkLen);
			if (!isMirrored)
				rawBitmap += curChunkLen;
			break;
		}

		remainingWidth -= curChunkLen;

		if (curByte == 0) {
			remainingWidth = celData->width;
			remainingHeight--;

			if (isMirrored)
				rawBitmap += celData->width * 2;
		}
	}

	// for CGA rendering, apply dithering
	switch (_renderMode) {
	case Common::kRenderCGA: {
		uint16 totalPixels = celData->width * celData->height;

		// dither clear key
		celData->clearKey = _gfx->getCGAMixtureColor(celData->clearKey);

		rawBitmap = celData->rawBitmap;
		for (uint16 pixelNr = 0; pixelNr < totalPixels; pixelNr++) {
			curColor = *rawBitmap;
			*rawBitmap = _gfx->getCGAMixtureColor(curColor);
			rawBitmap++;
		}
		break;
	}
	default:
		break;
	}
}

void AgiEngine::unpackViewCelDataAGI256(AgiViewCel *celData, byte *compressedData, uint16 compressedSize) {
	byte *rawBitmap = new byte[celData->width * celData->height];
	int16 remainingHeight = celData->height;
	int16 remainingWidth = celData->width;
	byte curByte;

	celData->rawBitmap = rawBitmap;

	while (remainingHeight) {
		if (!compressedSize)
			error("unexpected end of data, while unpacking AGI256 view");

		curByte = *compressedData++;
		compressedSize--;

		if (curByte == 0) {
			// Go to next vertical position
			if (remainingWidth) {
				// fill remaining bytes with clear key
				memset(rawBitmap, celData->clearKey, remainingWidth);
				rawBitmap += remainingWidth;
				remainingWidth = 0;
			}
		} else {
			if (!remainingWidth) {
				error("broken view data, while unpacking AGI256 view");
				break;
			}
			*rawBitmap = curByte;
			rawBitmap++;
			remainingWidth--;
		}

		if (curByte == 0) {
			remainingWidth = celData->width;
			remainingHeight--;
		}
	}
}

/**
 * Unloads all data in a view resource
 * @param n number of view resource
 */
void AgiEngine::unloadView(int16 viewNr) {
	AgiView *viewData = &_game.views[viewNr];

	debugC(5, kDebugLevelResources, "discard view %d", viewNr);
	if (!(_game.dirView[viewNr].flags & RES_LOADED))
		return;

	// Rebuild sprite list, see Sarien bug #779302
	_sprites->eraseSprites();

	// free data
	for (int16 loopNr = 0; loopNr < viewData->loopCount; loopNr++) {
		AgiViewLoop *loopData = &viewData->loop[loopNr];
		for (int16 celNr = 0; celNr < loopData->celCount; celNr++) {
			AgiViewCel *celData = &loopData->cel[celNr];

			delete[] celData->rawBitmap;
		}
		delete[] loopData->cel;
	}
	delete[] viewData->loop;

	if (viewData->description)
		delete[] viewData->description;

	viewData->headerCycleTime = 0;
	viewData->headerStepSize = 0;
	viewData->description = nullptr;
	viewData->loop = nullptr;
	viewData->loopCount = 0;

	// Mark this view as not loaded anymore
	_game.dirView[viewNr].flags &= ~RES_LOADED;

	_sprites->buildAllSpriteLists();
	_sprites->drawAllSpriteLists();
}

/**
 * Set a view table entry to use the specified view resource.
 * @param screenObj pointer to screen object
 * @param viewNr number of AGI view resource
 */
void AgiEngine::setView(ScreenObjEntry *screenObj, int16 viewNr) {
	if (!(_game.dirView[viewNr].flags & RES_LOADED)) {
		// View resource currently not loaded, this is probably a game bug
		// Load the resource now to fix the issue, and give out a warning
		// This happens in at least Larry 1 for Apple IIgs right after getting beaten up by taxi driver
		// Original interpreter bombs out in this situation saying "view not loaded, Press ESC to quit"
		warning("setView() called on screen object %d to use view %d, but view not loaded", screenObj->objectNr, viewNr);
		warning("probably game script bug, trying to load view into memory");
		if (agiLoadResource(RESOURCETYPE_VIEW, viewNr) != errOK) {
			// loading failed, we better error() out now
			error("setView() called to set view %d for screen object %d, which is not loaded atm and loading failed", viewNr, screenObj->objectNr);
			return;
		};
	}

	screenObj->viewResource = &_game.views[viewNr];
	screenObj->currentViewNr = viewNr;
	screenObj->loopCount = screenObj->viewResource->loopCount;
	screenObj->viewReplaced = true;

	if (getVersion() < 0x2000) {
		screenObj->stepSize = screenObj->viewResource->headerStepSize;
		screenObj->cycleTime = screenObj->viewResource->headerCycleTime;
		screenObj->cycleTimeCount = 0;
	}
	if (screenObj->currentLoopNr >= screenObj->loopCount) {
		setLoop(screenObj, 0);
	} else {
		setLoop(screenObj, screenObj->currentLoopNr);
	}
}

/**
 * Set a view table entry to use the specified loop of the current view.
 * @param screenObj pointer to screen object
 * @param loopNr number of loop
 */
void AgiEngine::setLoop(ScreenObjEntry *screenObj, int16 loopNr) {
	if (!(_game.dirView[screenObj->currentViewNr].flags & RES_LOADED)) {
		error("setLoop() called on screen object %d, which has no loaded view resource assigned to it", screenObj->objectNr);
		return;
	}
	assert(screenObj->viewResource);

	if (screenObj->loopCount == 0) {
		warning("setLoop() called on screen object %d, which has no loops (view %d)", screenObj->objectNr, screenObj->currentViewNr);
		return;
	}

	if (loopNr >= screenObj->loopCount) {
		// requested loop not existent
		// instead of error()ing out, we instead clip it
		// At least required for possibly Manhunter 1 according to previous comment when leaving the arcade machine
		// TODO: Check MH1
		// TODO: This causes an issue in KQ1, when bowing to the king in room 53
		//       Ego will face away from the king, because the scripts set the loop first and then the view
		//       Loop is corrected by us, because at that time it's invalid. Was already present in 1.7.0
		//       We should probably script-patch it out.
		int16 requestedLoopNr = loopNr;

		loopNr = screenObj->loopCount - 1;

		warning("Non-existent loop requested for screen object %d", screenObj->objectNr);
		warning("view %d, requested loop %d -> clipped to loop %d", screenObj->currentViewNr, requestedLoopNr, loopNr);
	}

	AgiViewLoop *curViewLoop = &_game.views[screenObj->currentViewNr].loop[loopNr];

	screenObj->currentLoopNr = loopNr;
	screenObj->loopData = curViewLoop;
	screenObj->celCount = curViewLoop->celCount;

	if (screenObj->currentCelNr >= screenObj->celCount) {
		setCel(screenObj, 0);
	} else {
		setCel(screenObj, screenObj->currentCelNr);
	}
}

/**
 * Set a view table entry to use the specified cel of the current loop.
 * @param screenObj pointer to screen object
 * @param celNr number of cel
 */
void AgiEngine::setCel(ScreenObjEntry *screenObj, int16 celNr) {
	if (!(_game.dirView[screenObj->currentViewNr].flags & RES_LOADED)) {
		error("setCel() called on screen object %d, which has no loaded view resource assigned to it", screenObj->objectNr);
		return;
	}
	assert(screenObj->viewResource);

	if (screenObj->loopCount == 0) {
		warning("setLoop() called on screen object %d, which has no loops (view %d)", screenObj->objectNr, screenObj->currentViewNr);
		return;
	}

	AgiViewLoop *curViewLoop = &_game.views[screenObj->currentViewNr].loop[screenObj->currentLoopNr];

	// Added by Amit Vainsencher <amitv@subdimension.com> to prevent
	// crash in KQ1 -- not in the Sierra interpreter
	if (curViewLoop->celCount == 0) {
		warning("setCel() called on screen object %d, which has no cels (view %d)", screenObj->objectNr, screenObj->currentViewNr);
		return;
	}

	if (celNr >= screenObj->celCount) {
		// requested cel not existent
		// instead of error()ing out, we instead clip it
		// At least required for King's Quest 3 on Apple IIgs - walking the planks death cutscene
		// see bug #5832, which is a game bug!
		int16 requestedCelNr = celNr;

		celNr = screenObj->celCount - 1;

		warning("Non-existent cel requested for screen object %d", screenObj->objectNr);
		warning("view %d, loop %d, requested cel %d -> clipped to cel %d", screenObj->currentViewNr, screenObj->currentLoopNr, requestedCelNr, celNr);
	}

	screenObj->currentCelNr = celNr;

	AgiViewCel *curViewCel;
	curViewCel         = &curViewLoop->cel[celNr];
	screenObj->celData = curViewCel;
	screenObj->xSize   = curViewCel->width;
	screenObj->ySize   = curViewCel->height;

	// If position isn't appropriate, update it accordingly
	clipViewCoordinates(screenObj);
}

/**
 * Restrict view table entry's position so it stays wholly inside the screen.
 * Also take horizon into account when clipping if not set to ignore it.
 * @param v pointer to view table entry
 */
void AgiEngine::clipViewCoordinates(ScreenObjEntry *screenObj) {
	if (screenObj->xPos + screenObj->xSize > SCRIPT_WIDTH) {
		screenObj->flags |= fUpdatePos;
		screenObj->xPos = SCRIPT_WIDTH - screenObj->xSize;
	}
	if (screenObj->yPos - screenObj->ySize + 1 < 0) {
		screenObj->flags |= fUpdatePos;
		screenObj->yPos = screenObj->ySize - 1;
	}
	if (screenObj->yPos <= _game.horizon && (~screenObj->flags & fIgnoreHorizon)) {
		screenObj->flags |= fUpdatePos;
		screenObj->yPos = _game.horizon + 1;
	}

	if (getVersion() < 0x2000) {
		screenObj->flags |= fDontupdate;
	}

}

/**
 * Set the view table entry as updating.
 * @param v pointer to view table entry
 */
void AgiEngine::startUpdate(ScreenObjEntry *v) {
	if (~v->flags & fUpdate) {
		_sprites->eraseSprites();
		v->flags |= fUpdate;
		_sprites->buildAllSpriteLists();
		_sprites->drawAllSpriteLists();
	}
}

/**
 * Set the view table entry as non-updating.
 * @param v pointer to view table entry
 */
void AgiEngine::stopUpdate(ScreenObjEntry *viewPtr) {
	if (viewPtr->flags & fUpdate) {
		_sprites->eraseSprites();
		viewPtr->flags &= ~fUpdate;
		_sprites->buildAllSpriteLists();
		_sprites->drawAllSpriteLists();
	}
}

// loops to use according to direction and number of loops in
// the view resource
static int loopTable2[] = {
	0x04, 0x04, 0x00, 0x00, 0x00, 0x04, 0x01, 0x01, 0x01
};

static int loopTable4[] = {
	0x04, 0x03, 0x00, 0x00, 0x00, 0x02, 0x01, 0x01, 0x01
};

/**
 * Update view table entries.
 * This function is called at the end of each interpreter cycle
 * to update the view table entries and blit the sprites.
 */
void AgiEngine::updateScreenObjTable() {
	ScreenObjEntry *screenObj;
	int16 changeCount, loopNr;

	changeCount = 0;
	for (screenObj = _game.screenObjTable; screenObj < &_game.screenObjTable[SCREENOBJECTS_MAX]; screenObj++) {
		if ((screenObj->flags & (fAnimated | fUpdate | fDrawn)) != (fAnimated | fUpdate | fDrawn)) {
			continue;
		}

		changeCount++;

		loopNr = 4;
		if (!(screenObj->flags & fFixLoop)) {
			switch (screenObj->loopCount) {
			case 2:
			case 3:
				loopNr = loopTable2[screenObj->direction];
				break;
			case 4:
				loopNr = loopTable4[screenObj->direction];
				break;
			default:
				// for KQ4
				if (getVersion() == 0x3086 || getGameID() == GID_KQ4)
					loopNr = loopTable4[screenObj->direction];
				break;
			}
		}

		// AGI 2.272 (ddp, xmas) doesn't test step_time_count!
		if (loopNr != 4 && loopNr != screenObj->currentLoopNr) {
			if (getVersion() <= 0x2272 || screenObj->stepTimeCount == 1) {
				setLoop(screenObj, loopNr);
			}
		}

		if (screenObj->flags & fCycling) {
			if (screenObj->cycleTimeCount) {
				screenObj->cycleTimeCount--;
				if (screenObj->cycleTimeCount == 0) {
					updateView(screenObj);
					screenObj->cycleTimeCount = screenObj->cycleTime;
				}
			}
		}
	}

	if (changeCount) {
		_sprites->eraseRegularSprites();
		updatePosition();
		_sprites->buildRegularSpriteList();
		_sprites->drawRegularSpriteList();
		_sprites->showRegularSpriteList();

		_game.screenObjTable[SCREENOBJECTS_EGO_ENTRY].flags &= ~(fOnWater | fOnLand);
	}
}

bool AgiEngine::isEgoView(const ScreenObjEntry *screenObj) {
	return screenObj == _game.screenObjTable;
}

} // End of namespace Agi
