/* 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 "common/stream.h"
#include "common/memstream.h"

#include "startrek/iwfile.h"
#include "startrek/room.h"
#include "startrek/startrek.h"

namespace StarTrek {

void StarTrekEngine::initActors() {
	for (int i = 0; i < NUM_ACTORS; i++)
		_actorList[i] = Actor();
	for (int i = 0; i < MAX_BAN_FILES; i++)
		_banFiles[i].reset();

	_kirkActor->animationString = "kstnd";
	_spockActor->animationString = "sstnd";
	_mccoyActor->animationString = "mstnd";
	_redshirtActor->animationString = "rstnd";
}

int StarTrekEngine::loadActorAnim(int actorIndex, const Common::String &animName, int16 x, int16 y, Fixed8 scale) {
	debugC(6, kDebugGraphics, "Load animation '%s' on actor %d", animName.c_str(), actorIndex);

	if (actorIndex == -1) {
		// TODO
		warning("loadActorAnim: actor == -1");
	} else {
		Actor *actor = &_actorList[actorIndex];

		if (actor->spriteDrawn) {
			releaseAnim(actor);
			drawActorToScreen(actor, animName, x, y, scale, false);
		} else {
			drawActorToScreen(actor, animName, x, y, scale, true);
		}

		actor->triggerActionWhenAnimFinished = false;
		actor->finishedAnimActionParam = 0;
	}

	return actorIndex;
}

void StarTrekEngine::loadBanFile(const Common::String &name) {
	debugC(kDebugGeneral, 7, "Load BAN file: %s.ban", name.c_str());
	for (int i = 0; i < MAX_BAN_FILES; i++) {
		if (!_banFiles[i]) {
			_banFiles[i] = SharedPtr<Common::MemoryReadStreamEndian>(loadFile(name + ".ban"));
			_banFileOffsets[i] = 0;
			return;
		}
	}

	warning("Couldn't load .BAN file \"%s.ban\"", name.c_str());
}

bool StarTrekEngine::actorWalkToPosition(int actorIndex, const Common::String &animFile, int16 srcX, int16 srcY, int16 destX, int16 destY) {
	debugC(6, "Obj %d: walk from (%d,%d) to (%d,%d)", actorIndex, srcX, srcY, destX, destY);

	Actor *actor = &_actorList[actorIndex];

	actor->triggerActionWhenAnimFinished = false;
	if (isPositionSolid(destX, destY))
		return false;

	if (actor->spriteDrawn)
		releaseAnim(actor);
	else
		_gfx->addSprite(&actor->sprite);

	actor->spriteDrawn = true;
	actor->animType = 1;
	actor->frameToStartNextAnim = _frameIndex + 1;
	actor->animationString2 = animFile;

	actor->dest.x = destX;
	actor->dest.y = destY;
	actor->field92 = 0;
	actor->triggerActionWhenAnimFinished = false;

	actor->iwDestPosition = -1;
	actor->iwSrcPosition = -1;

	if (directPathExists(srcX, srcY, destX, destY)) {
		chooseActorDirectionForWalking(actor, srcX, srcY, destX, destY);
		updateActorPositionWhileWalking(actor, (actor->granularPosX + 0.5).toInt(), (actor->granularPosY + 0.5).toInt());
		return true;
	} else {
		actor->iwSrcPosition = _iwFile->getClosestKeyPosition(srcX, srcY);
		actor->iwDestPosition = _iwFile->getClosestKeyPosition(destX, destY);

		if (actor->iwSrcPosition == -1 || actor->iwDestPosition == -1) {
			// No path exists; face south by default.
			actor->animationString2 += "S";
			actor->direction = 'S';

			updateActorPositionWhileWalking(actor, srcX, srcY);
			initStandAnim(actorIndex);

			return false;
		} else {
			Common::Point iwSrc = _iwFile->_keyPositions[actor->iwSrcPosition];
			chooseActorDirectionForWalking(actor, srcX, srcY, iwSrc.x, iwSrc.y);
			updateActorPositionWhileWalking(actor, (actor->granularPosX + 0.5).toInt(), (actor->granularPosY + 0.5).toInt());
			return true;
		}
	}
}

void StarTrekEngine::updateActorAnimations() {
	for (int i = 0; i < NUM_ACTORS; i++) {
		Actor *actor = &_actorList[i];
		if (!actor->spriteDrawn)
			continue;

		switch (actor->animType) {
		case 0: // Not walking?
		case 2:
			if (_frameIndex >= actor->frameToStartNextAnim) {
				int nextAnimIndex = getRandomWord() & 3;
				actor->animFile->seek(18 + nextAnimIndex + actor->animFrame * 22, SEEK_SET);
				byte nextAnimFrame = actor->animFile->readByte();

				if (actor->animFrame != nextAnimFrame) {
					if (nextAnimFrame == actor->numAnimFrames - 1) {
						actor->field62++;
						if (actor->triggerActionWhenAnimFinished) {
							addAction(ACTION_FINISHED_ANIMATION, actor->finishedAnimActionParam, 0, 0);
						}
					}
				}

				actor->animFrame = nextAnimFrame;
				if (actor->animFrame >= actor->numAnimFrames) {
					if (actor->animationString.empty())
						removeActorFromScreen(i);
					else
						initStandAnim(i);
				} else {
					Sprite *sprite = &actor->sprite;

					actor->animFile->seek(actor->animFrame * 22, SEEK_SET);
					char animFrameFilename[16];
					actor->animFile->read(animFrameFilename, 16);
					sprite->setBitmap(loadAnimationFrame(animFrameFilename, actor->scale));

					actor->bitmapFilename = animFrameFilename;

					actor->animFile->seek(10 + actor->animFrame * 22, SEEK_SET);
					uint16 xOffset = actor->animFile->readUint16();
					uint16 yOffset = actor->animFile->readUint16();
					uint16 basePriority = actor->animFile->readUint16();
					uint16 frames = actor->animFile->readUint16();

					sprite->pos.x = xOffset + actor->pos.x;
					sprite->pos.y = yOffset + actor->pos.y;
					sprite->drawPriority = _gfx->getPriValue(0, yOffset + actor->pos.y) + basePriority;
					sprite->bitmapChanged = true;

					actor->frameToStartNextAnim = frames + _frameIndex;
				}
			}
			break;
		case 1: // Walking
			if (_frameIndex < actor->frameToStartNextAnim)
				break;
			if (i == 0) // Kirk only
				checkTouchedLoadingZone(actor->pos.x, actor->pos.y);
			if (actor->field90 != 0) {
				Sprite *sprite = &actor->sprite;
				int loops;
				if (getActorScaleAtPosition((actor->granularPosY + 0.5).toInt()) < 0.625)
					loops = 1;
				else
					loops = 2;
				for (int k = 0; k < loops; k++) {
					if (actor->field90 == 0)
						break;
					actor->field90--;
					Fixed16 newX = actor->granularPosX + actor->speedX;
					Fixed16 newY = actor->granularPosY + actor->speedY;
					if ((actor->field90 & 3) == 0) {
						sprite->bitmap.reset();
						updateActorPositionWhileWalking(actor, (newX + 0.5).toInt(), (newY + 0.5).toInt());
						actor->field92++;
					}

					actor->granularPosX = newX;
					actor->granularPosY = newY;
					actor->frameToStartNextAnim = _frameIndex;
				}
			} else { // actor->field90 == 0
				if (actor->iwSrcPosition == -1) {
					if (actor->triggerActionWhenAnimFinished) {
						actor->triggerActionWhenAnimFinished = false;
						addAction(ACTION_FINISHED_WALKING, actor->finishedAnimActionParam & 0xff, 0, 0);
					}

					actor->sprite.bitmap.reset();
					updateActorPositionWhileWalking(actor, (actor->granularPosX + 0.5).toInt(), (actor->granularPosY + 0.5).toInt());
					initStandAnim(i);
				} else { // actor->iwSrcPosition != -1
					if (actor->iwSrcPosition == actor->iwDestPosition) {
						actor->animationString2.deleteLastChar();
						actor->iwDestPosition = -1;
						actor->iwSrcPosition = -1;
						chooseActorDirectionForWalking(actor, actor->pos.x, actor->pos.y, actor->dest.x, actor->dest.y);
					} else {
						int index = _iwFile->_iwEntries[actor->iwSrcPosition][actor->iwDestPosition];
						actor->iwSrcPosition = index;
						Common::Point dest = _iwFile->_keyPositions[actor->iwSrcPosition];
						actor->animationString2.deleteLastChar();
						chooseActorDirectionForWalking(actor, actor->pos.x, actor->pos.y, dest.x, dest.y);
					}
				}
			}
			break;
		default:
			error("Invalid anim type.");
			break;
		}
	}
}

void StarTrekEngine::renderBanBelowSprites() {
	if ((_frameIndex & 3) != 0)
		return;

	byte *screenPixels = _gfx->lockScreenPixels();
	byte *bgPixels = _gfx->getBackgroundPixels();

	for (int i = 0; i < MAX_BAN_FILES; i++) {
		if (!_banFiles[i])
			continue;

		// TODO: video modes other than VGA

		_banFiles[i]->seek(_banFileOffsets[i], SEEK_SET);
		uint16 offset = _banFiles[i]->readUint16();

		if (offset == 0xffff) {
			_banFileOffsets[i] = 0;
			_banFiles[i]->seek(0, SEEK_SET);
			offset = _banFiles[i]->readSint16();
		}

		int16 size = _banFiles[i]->readSint16();
		if (size != 0) {
			_banFiles[i]->seek(_banFileOffsets[i], SEEK_SET);
			renderBan(screenPixels, _banFiles[i]);

			_banFiles[i]->seek(_banFileOffsets[i], SEEK_SET);
			renderBan(bgPixels, _banFiles[i]);
		}

	}

	_gfx->unlockScreenPixels();
}

void StarTrekEngine::renderBan(byte *destPixels, FileStream banFile) {
	uint16 offset = banFile->readUint16();
	int32 size = banFile->readUint16();

	byte *dest = destPixels + offset;

	// Skip 8 bytes (rectangle encompassing the area being drawn to)
	banFile->readSint32();
	banFile->readSint32();

	while (--size >= 0) {
		assert(dest >= destPixels && dest < destPixels + SCREEN_WIDTH * SCREEN_HEIGHT);
		int8 b = banFile->readByte();

		if (b == -128) // Add value to destination (usually jumping to next row)
			dest += banFile->readUint16();
		else if (b < 0) { // Repeated byte
			byte c = banFile->readByte();
			if (c == 0)
				dest += (-b) + 1;
			else {
				for (int j = 0; j < (-b) + 1; j++)
					(*dest++) = c;
			}
		} else { // List of bytes
			b++;
			while (b-- != 0) {
				byte c = banFile->readByte();
				if (c == 0)
					dest++;
				else
					*(dest++) = c;
			}
		}
	}
}

void StarTrekEngine::renderBanAboveSprites() {
	if ((_frameIndex & 3) != 0)
		return;

	for (int i = 0; i < MAX_BAN_FILES; i++) {
		if (!_banFiles[i])
			continue;

		_banFiles[i]->seek(_banFileOffsets[i], SEEK_SET);
		uint16 offset = _banFiles[i]->readUint16();

		if (offset == 0xffff) {
			_banFileOffsets[i] = 0;
			_banFiles[i]->seek(0, SEEK_SET);
			offset = _banFiles[i]->readSint16();
		}

		int16 size = _banFiles[i]->readSint16();
		if (size != 0) {
			Common::Rect rect;
			rect.left   = _banFiles[i]->readSint16();
			rect.top    = _banFiles[i]->readSint16();
			rect.right  = _banFiles[i]->readSint16() + 1;
			rect.bottom = _banFiles[i]->readSint16() + 1;

			// Draw all sprites in this rectangle to a custom surface, and only update the
			// specific pixels that were updated by the BAN file this frame.
			// Rationale behind this is that, since the background may not have been
			// redrawn, the transparent sprites (ie. textboxes) would further darken any
			// pixels behind them that haven't been updated this frame. So, we can't just
			// update everything in this rectangle.
			// FIXME: This copies the entire screen surface for temporary drawing, which
			// is somewhat wasteful. Original game had one more graphics layer it drew to
			// before the screen was updated...
			::Graphics::Surface surface;
			_gfx->drawAllSpritesInRectToSurface(rect, &surface);

			byte *destPixels = _gfx->lockScreenPixels();
			byte *src = (byte *)surface.getPixels() + offset;
			byte *dest = destPixels + offset;

			// This is similar to renderBan(), except it copies pixels from the surface
			// above instead of drawing directly to it. (Important since sprites may be
			// drawn on top.)
			while (--size >= 0) {
				assert(dest >= destPixels && dest < destPixels + SCREEN_WIDTH * SCREEN_HEIGHT);
				int8 b = _banFiles[i]->readByte();
				if (b == -128) {
					uint16 skip = _banFiles[i]->readUint16();
					dest += skip;
					src  += skip;
				} else if (b < 0) {
					byte c = _banFiles[i]->readByte();
					if (c == 0) {
						dest += (-b) + 1;
						src  += (-b) + 1;
					}
					else {
						for (int j = 0; j < (-b) + 1; j++)
							*(dest++) = *(src++);
					}
				} else {
					b++;
					while (b-- != 0) {
						byte c = _banFiles[i]->readByte();
						if (c == 0) {
							dest++;
							src++;
						} else
							*(dest++) = *(src++);
					}
				}
			}

			_gfx->unlockScreenPixels();
			surface.free();

			_banFileOffsets[i] = _banFiles[i]->pos();
		}
	}
}

void StarTrekEngine::removeActorFromScreen(int actorIndex) {
	Actor *actor = &_actorList[actorIndex];

	if (actor->spriteDrawn != 1)
		return;

	debugC(6, kDebugGraphics, "Stop drawing actor %d", actorIndex);

	Sprite *sprite = &actor->sprite;
	sprite->field16 = true;
	sprite->bitmapChanged = true;
	_gfx->drawAllSprites();
	_gfx->delSprite(sprite);
	releaseAnim(actor);
}

void StarTrekEngine::actorFunc1() {
	for (int i = 0; i < NUM_ACTORS; i++) {
		if (_actorList[i].spriteDrawn == 1) {
			removeActorFromScreen(i);
		}
	}

	for (int i = 0; i < MAX_BAN_FILES; i++) {
		_banFiles[i].reset();
	}
}

void StarTrekEngine::drawActorToScreen(Actor *actor, const Common::String &_animName, int16 x, int16 y, Fixed8 scale, bool addSprite) {
	Common::String animFilename = _animName;
	if (_animName.hasPrefixIgnoreCase("stnd") /* && word_45d20 == -1 */) // TODO
		animFilename += 'j';

	actor->animFilename = _animName;
	actor->animType = 2;
	actor->animFile = SharedPtr<Common::MemoryReadStreamEndian>(loadFile(animFilename + ".anm"));
	actor->numAnimFrames = actor->animFile->size() / 22;
	actor->animFrame = 0;
	actor->pos.x = x;
	actor->pos.y = y;
	actor->field62 = 0;
	actor->scale = scale;

	actor->animFile->seek(16, SEEK_SET);
	actor->frameToStartNextAnim = actor->animFile->readUint16() + _frameIndex;

	char firstFrameFilename[11];
	actor->animFile->seek(0, SEEK_SET);
	actor->animFile->read(firstFrameFilename, 10);
	firstFrameFilename[10] = '\0';

	Sprite *sprite = &actor->sprite;
	if (addSprite)
		_gfx->addSprite(sprite);

	sprite->setBitmap(loadAnimationFrame(firstFrameFilename, scale));
	actor->bitmapFilename = firstFrameFilename;
	actor->scale = scale;
	actor->animFile->seek(10, SEEK_SET);

	uint16 xOffset = actor->animFile->readUint16();
	uint16 yOffset = actor->animFile->readUint16();
	uint16 basePriority = actor->animFile->readUint16();

	sprite->pos.x = xOffset + actor->pos.x;
	sprite->pos.y = yOffset + actor->pos.y;
	sprite->drawPriority = _gfx->getPriValue(0, yOffset + actor->pos.y) + basePriority;
	sprite->bitmapChanged = true;

	actor->spriteDrawn = 1;
}

void StarTrekEngine::releaseAnim(Actor *actor) {
	switch (actor->animType) {
	case 0:
	case 2:
		actor->sprite.bitmap.reset();
		actor->animFile.reset();
		break;
	case 1:
		actor->sprite.bitmap.reset();
		break;
	default:
		error("Invalid anim type");
		break;
	}

	actor->spriteDrawn = 0;
}

void StarTrekEngine::initStandAnim(int actorIndex) {
	Actor *actor = &_actorList[actorIndex];

	if (!actor->spriteDrawn)
		error("initStandAnim: dead anim");

	////////////////////
	// sub_239d2
	const char *directions = "nsew";

	if (actorIndex >= 0 && actorIndex <= 3) {
		int8 dir = _awayMission.crewDirectionsAfterWalk[actorIndex];
		if (dir != -1) {
			actor->direction = directions[dir];
			_awayMission.crewDirectionsAfterWalk[actorIndex] = -1;
		}
	}
	// end of sub_239d2
	////////////////////

	Common::String animName;
	if (actor->direction != 0)
		animName = actor->animationString + (char)actor->direction;
	else // Default to facing south
		animName = actor->animationString + 's';

	Fixed8 scale = getActorScaleAtPosition(actor->pos.y);
	loadActorAnim(actorIndex, animName, actor->pos.x, actor->pos.y, scale);
	actor->animType = 0;
}

void StarTrekEngine::updateActorPositionWhileWalking(Actor *actor, int16 x, int16 y) {
	actor->scale = getActorScaleAtPosition(y);
	Common::String animName = Common::String::format("%s%02d", actor->animationString2.c_str(), actor->field92 & 7);
	actor->sprite.setBitmap(loadAnimationFrame(animName, actor->scale));
	actor->bitmapFilename = animName;

	Sprite *sprite = &actor->sprite;
	sprite->drawPriority = _gfx->getPriValue(0, y);
	sprite->pos.x = x;
	sprite->pos.y = y;
	sprite->bitmapChanged = true;

	actor->frameToStartNextAnim = _frameIndex;
	actor->pos.x = x;
	actor->pos.y = y;
}

void StarTrekEngine::chooseActorDirectionForWalking(Actor *actor, int16 srcX, int16 srcY, int16 destX, int16 destY) {
	actor->granularPosX = srcX;
	actor->granularPosY = srcY;

	int16 distX = destX - srcX;
	int16 distY = destY - srcY;
	int16 absDistX = abs(distX);
	int16 absDistY = abs(distY);

	if (absDistX > absDistY) {
		char d;
		if (distX > 0)
			d = 'E';
		else
			d = 'W';

		// Append direction to animation string
		actor->animationString2 += d;

		actor->direction = d;
		actor->field90 = absDistX;

		if (distX != 0) {
			if (distX > 0)
				actor->speedX = 1.0;
			else
				actor->speedX = -1.0;

			actor->speedY = Fixed16(distY) / absDistX;
		}
	} else {
		char d;
		if (distY > 0)
			d = 'S';
		else
			d = 'N';

		// Append direction to animation string
		actor->animationString2 += d;

		actor->direction = d;
		actor->field90 = absDistY;

		if (distY != 0) {
			if (distY > 0)
				actor->speedY = 1.0;
			else
				actor->speedY = -1.0;

			actor->speedX = Fixed16(distX) / absDistY;
		}
	}
}

bool StarTrekEngine::directPathExists(int16 srcX, int16 srcY, int16 destX, int16 destY) {
	int32 distX = destX - srcX;
	int32 distY = destY - srcY;

	int32 absDistX = abs(distX);
	int32 absDistY = abs(distY);

	int32 distCounter;
	Fixed16 speedX, speedY;

	if (absDistX > absDistY) {
		distCounter = absDistX;

		if (distCounter == 0)
			return true;

		speedY = Fixed16(distY) / absDistX;

		if (distX > 0)
			speedX = 1.0;
		else
			speedX = -1.0;
	} else { // absDistX <= absDistY
		distCounter = absDistY;

		if (distCounter == 0)
			return true;

		speedX = Fixed16(distX) / absDistY;

		if (distY > 0)
			speedY = 1.0;
		else
			speedY = -1.0;
	}

	Fixed16 fixedX = srcX;
	Fixed16 fixedY = srcY;

	if (isPositionSolid((fixedX + 0.5).toInt(), (fixedY + 0.5).toInt()))
		return false;

	while (distCounter-- > 0) {
		fixedX += speedX;
		fixedY += speedY;

		if (isPositionSolid((fixedX + 0.5).toInt(), (fixedY + 0.5).toInt()))
			return false;
	}

	return true;
}

int StarTrekEngine::findObjectAt(int x, int y) {
	Sprite *sprite = _gfx->getSpriteAt(x, y);

	if (sprite != nullptr) {
		if (sprite == &_inventoryIconSprite)
			return OBJECT_INVENTORY_ICON;
		else if (sprite == &_itemIconSprite)
			return _awayMission.activeObject;

		for (int i = 0; i < NUM_ACTORS; i++) {
			Actor *actor = &_actorList[i];
			if (sprite == &actor->sprite)
				return i;
		}

		error("findObject: Clicked on an unknown sprite");
	}

	_objectHasWalkPosition = false;
	int actionBit = 1 << (_awayMission.activeAction - 1);
	int offset = _room->getFirstHotspot();

	while (offset != _room->getHotspotEnd()) {
		uint16 word = _room->readRdfWord(offset);
		if (word & 0x8000) {
			if ((word & actionBit) && isPointInPolygon((int16 *)(_room->_rdfData + offset + 6), x, y)) {
				int actorIndex = _room->readRdfWord(offset + 6);
				_objectHasWalkPosition = true;
				_objectWalkPosition.x = _room->readRdfWord(offset + 2);
				_objectWalkPosition.y = _room->readRdfWord(offset + 4);
				return actorIndex;
			}

			int numVertices = _room->readRdfWord(offset + 8);
			offset = offset + 10 + numVertices * 4;
		} else {
			if (isPointInPolygon((int16 *)(_room->_rdfData + offset), x, y)) {
				int actorIndex = _room->readRdfWord(offset);
				return actorIndex;
			}

			int numVertices = _room->readRdfWord(offset + 2);
			offset = offset + 4 + numVertices * 4;
		}
	}

	return -1;
}

Bitmap *StarTrekEngine::loadAnimationFrame(const Common::String &filename, Fixed8 scale) {
	Bitmap *bitmapToReturn = nullptr;

	char basename[5];
	strncpy(basename, filename.c_str() + 1, 4);
	basename[4] = '\0';

	char c = filename[0];
	if ((strcmp(basename, "stnd") == 0 || strcmp(basename, "tele") == 0)
	        && (c == 'm' || c == 's' || c == 'k' || c == 'r')) {
		if (c == 'm') {
			// Mccoy has the "base" animations for all crewmen
			bitmapToReturn = _gfx->loadBitmap(filename);
		} else {
			// All crewman other than mccoy copy the animation frames from mccoy, change
			// the colors of the uniforms, and load an "xor" file to redraw the face.

			// TODO: The ".$bm" extension is a "virtual file"? Caches the changes to the
			// file made here?
			// bitmapToReturn = _gfx->loadBitmap(filename + ".$bm");

			if (bitmapToReturn == nullptr) {
				Common::String mccoyFilename = filename;
				mccoyFilename.setChar('m', 0);
				Bitmap *bitmap = _gfx->loadBitmap(mccoyFilename);

				uint16 width = bitmap->width;
				uint16 height = bitmap->height;

				bitmapToReturn = new Bitmap(width, height);
				bitmapToReturn->xoffset = bitmap->xoffset;
				bitmapToReturn->yoffset = bitmap->yoffset;

				// Change uniform color
				int16 colorShift;
				switch (c) {
				case 'k': // Kirk
					colorShift = 8;
					break;
				case 'r': // Redshirt
					colorShift = -8;
					break;
				case 's': // Spock
					colorShift = 0;
					break;
				case 'm': // McCoy
					colorShift = 0;
					break;
				default:
					colorShift = 0;
					break;
				}

				if (colorShift == 0) {
					memcpy(bitmapToReturn->pixels, bitmap->pixels, width * height);
				} else {
					byte *src = bitmap->pixels;
					byte *dest = bitmapToReturn->pixels;
					byte baseUniformColor = 0xa8;

					for (int i = 0; i < width * height; i++) {
						byte b = *src++;
						if (b >= baseUniformColor && b < baseUniformColor + 8)
							*dest++ = b + colorShift;
						else
							*dest++ = b;
					}
				}

				// Redraw face with xor file
				Common::MemoryReadStreamEndian *xorFile = loadFile(filename + ".xor");
				xorFile->seek(0, SEEK_SET);
				uint16 xoffset = bitmap->xoffset - xorFile->readUint16();
				uint16 yoffset = bitmap->yoffset - xorFile->readUint16();
				uint16 xorWidth = xorFile->readUint16();
				uint16 xorHeight = xorFile->readUint16();

				byte *dest = bitmapToReturn->pixels + yoffset * bitmap->width + xoffset;

				for (int i = 0; i < xorHeight; i++) {
					for (int j = 0; j < xorWidth; j++)
						*dest++ ^= xorFile->readByte();
					dest += (bitmap->width - xorWidth);
				}

				delete xorFile;
			}
		}
	} else {
		// TODO: when loading a bitmap, it passes a different argument than is standard to
		// the "file loading with cache" function...
		bitmapToReturn = _gfx->loadBitmap(filename);
	}

	if (scale != 1.0) {
		bitmapToReturn = scaleBitmap(bitmapToReturn, scale);
	}

	return bitmapToReturn;
}


int StarTrekEngine::selectObjectForUseAction() {
	while (true) {
		if (!(_awayMission.crewDownBitset & (1 << OBJECT_KIRK)))
			showInventoryIcons(false);

		TrekEvent event;

		while (true) {
			if (!getNextEvent(&event))
				continue;

			if (event.type == TREKEVENT_TICK) {
				updateMouseBitmap();
				_gfx->drawAllSprites();
				_sound->checkLoopMusic();
			} else if (event.type == TREKEVENT_LBUTTONDOWN) {
				removeNextEvent();
				break;
			} else if (event.type == TREKEVENT_MOUSEMOVE) {
			} else if (event.type == TREKEVENT_RBUTTONDOWN) {
				// Allow this to be processed by main away mission loop
				break;
			} else if (event.type == TREKEVENT_KEYDOWN) {
				if (event.kbd.keycode == Common::KEYCODE_ESCAPE
				        || event.kbd.keycode == Common::KEYCODE_w
				        || event.kbd.keycode == Common::KEYCODE_t
				        || event.kbd.keycode == Common::KEYCODE_u
				        || event.kbd.keycode == Common::KEYCODE_g
				        || event.kbd.keycode == Common::KEYCODE_l
				        || event.kbd.keycode == Common::KEYCODE_SPACE
				        || event.kbd.keycode == Common::KEYCODE_F2) {
					// Allow these buttons to be processed by main away mission loop
					break;
				} else if (event.kbd.keycode == Common::KEYCODE_i) {
					removeNextEvent();
					break;
				} else if (event.kbd.keycode == Common::KEYCODE_RETURN || event.kbd.keycode == Common::KEYCODE_KP_ENTER || event.kbd.keycode == Common::KEYCODE_F1) {
					// Simulate left-click
					removeNextEvent();
					event.type = TREKEVENT_LBUTTONDOWN;
					break;
				}
			}

			removeNextEvent();
		}

		if (event.type == TREKEVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_i) {
			hideInventoryIcons();
			int clickedObject = showInventoryMenu(50, 50, true);
			if (clickedObject == -1)
				continue;
			return clickedObject;
		} else if (event.type == TREKEVENT_LBUTTONDOWN) {
			int clickedObject = findObjectAt(_gfx->getMousePos());
			hideInventoryIcons();

			if (clickedObject == -1)
				continue;
			else if (isObjectUnusable(clickedObject, ACTION_USE))
				continue;
			else if (clickedObject == OBJECT_INVENTORY_ICON) {
				clickedObject = showInventoryMenu(50, 50, false);
				if (clickedObject == -1)
					continue;
				else
					return clickedObject;
			} else if (clickedObject <= OBJECT_REDSHIRT)
				return clickedObject;
			else if (isObjectUnusable(OBJECT_KIRK, ACTION_USE))
				continue;
			else if (_room->actionHasCode(ACTION_USE, OBJECT_KIRK, clickedObject, 0)
			         || _room->actionHasCode(ACTION_GET, clickedObject, 0, 0)
			         || _room->actionHasCode(ACTION_WALK, clickedObject, 0, 0)) {
				_awayMission.activeObject = OBJECT_KIRK;
				_awayMission.passiveObject = clickedObject;
				_awayMission.activeAction = ACTION_USE;
				clickedObject = OBJECT_KIRK;
				if (!walkActiveObjectToHotspot())
					addAction(_awayMission.activeAction, _awayMission.activeObject, _awayMission.passiveObject, 0);
				return clickedObject;
			} else
				continue;
		} else {
			hideInventoryIcons();
			return -1;
		}
	}
}

Common::String StarTrekEngine::getCrewmanAnimFilename(int actorIndex, const Common::String &basename) {
	const char *crewmanChars = "ksmr";
	assert(actorIndex >= 0 && actorIndex < 4);
	return crewmanChars[actorIndex] + basename;
}

void StarTrekEngine::updateMouseBitmap() {
	const bool worksOnCrewmen[] = { // True if the action reacts with crewmen
		false, // ACTION_WALK
		true,  // ACTION_USE
		false, // ACTION_GET
		true,  // ACTION_LOOK
		true   // ACTION_TALK
	};
	const bool worksOnActors[] = { // True if the action reacts with other objects
		false, // ACTION_WALK
		true,  // ACTION_USE
		true,  // ACTION_GET
		true,  // ACTION_LOOK
		true   // ACTION_TALK
	};
	const bool worksOnHotspots[] = { // True if the action reacts with hotspots
		false, // ACTION_WALK
		true,  // ACTION_USE
		true,  // ACTION_GET
		true,  // ACTION_LOOK
		false  // ACTION_TALK
	};

	Common::Point mousePos = _gfx->getMousePos();
	int selected = findObjectAt(mousePos.x, mousePos.y);
	int action = _awayMission.activeAction;
	assert(action >= 1 && action <= 5);

	bool withRedOutline;

	if (selected >= 0 && selected <= 3 && worksOnCrewmen[action - 1])
		withRedOutline = true;
	else if (selected > 3 && selected < NUM_ACTORS && worksOnActors[action - 1])
		withRedOutline = true;
	else if (selected >= NUM_ACTORS && selected < HOTSPOTS_END && worksOnHotspots[action - 1])
		withRedOutline = true;
	else
		withRedOutline = false;

	chooseMouseBitmapForAction(action, withRedOutline);
}

bool StarTrekEngine::walkActiveObjectToHotspot() {
	if (!_objectHasWalkPosition)
		return false;

	int objectIndex;
	if (_awayMission.activeAction != ACTION_USE)
		objectIndex = OBJECT_KIRK;
	else if (_awayMission.activeObject <= OBJECT_REDSHIRT)
		objectIndex = _awayMission.activeObject;
	else if (_awayMission.activeObject >= ITEMS_START && _awayMission.activeObject <= ITEMS_END) { // FIXME: "<= ITEMS_END" doesn't make sense?
		if (_awayMission.activeObject == OBJECT_ISTRICOR)
			objectIndex = OBJECT_SPOCK;
		else if (_awayMission.activeObject == OBJECT_IMTRICOR)
			objectIndex = OBJECT_MCCOY;
		else
			objectIndex = OBJECT_KIRK;
	} else // This is the original error message...
		error("Jay didn't think about pmcheck");

	byte finishedAnimActionParam = false;
	bool walk = false;

	if (_awayMission.activeAction == ACTION_WALK)
		walk = true;
	else {
		// If this action has code defined for it in this room, buffer the action to be
		// done after the object finished walking there.
		Action action = {_awayMission.activeAction, _awayMission.activeObject, 0, 0};
		if (_awayMission.activeAction == ACTION_USE)
			action.b2 = _awayMission.passiveObject;

		if (_room->actionHasCode(action)) {
			for (int i = 0; i < MAX_BUFFERED_WALK_ACTIONS; i++) {
				if (!_actionOnWalkCompletionInUse[i]) {
					finishedAnimActionParam = i + 0xe0;
					_actionOnWalkCompletionInUse[i] = true;
					_actionOnWalkCompletion[i] = action;
					walk = true;
					break;
				}
			}
		}
	}

	if (walk) {
		Actor *actor = &_actorList[objectIndex];
		Common::String anim = getCrewmanAnimFilename(objectIndex, "walk");
		actorWalkToPosition(objectIndex, anim, actor->pos.x, actor->pos.y, _objectWalkPosition.x, _objectWalkPosition.y);
		if (finishedAnimActionParam != 0) {
			actor->triggerActionWhenAnimFinished = true;
			actor->finishedAnimActionParam = finishedAnimActionParam;
		}
		_objectHasWalkPosition = false;
		return true;
	} else {
		_objectHasWalkPosition = false;
		return false;
	}
}

void StarTrekEngine::showInventoryIcons(bool showItem) {
	const char *crewmanFilenames[] = {
		"ikirk",
		"ispock",
		"imccoy",
		"iredshir"
	};

	Common::String itemFilename;

	if (showItem) {
		int i = _awayMission.activeObject;
		if (i >= OBJECT_KIRK && i <= OBJECT_REDSHIRT)
			itemFilename = crewmanFilenames[i];
		else {
			assert(i >= ITEMS_START && i < ITEMS_END);
			Item *item = &_itemList[i - ITEMS_START];
			itemFilename = item->name;
		}
	}

	if (itemFilename.empty())
		_inventoryIconSprite.pos.x = 10;
	else {
		_gfx->addSprite(&_itemIconSprite);
		_itemIconSprite.drawMode = 2;
		_itemIconSprite.pos.x = 10;
		_itemIconSprite.pos.y = 10;
		_itemIconSprite.drawPriority = 15;
		_itemIconSprite.drawPriority2 = 8;
		_itemIconSprite.setBitmap(_gfx->loadBitmap(itemFilename));

		_inventoryIconSprite.pos.x = 46;
	}

	_gfx->addSprite(&_inventoryIconSprite);

	_inventoryIconSprite.pos.y = 10;
	_inventoryIconSprite.drawMode = 2;
	_inventoryIconSprite.drawPriority = 15;
	_inventoryIconSprite.drawPriority2 = 8;
	_inventoryIconSprite.setBitmap(_gfx->loadBitmap("inv00"));
}

bool StarTrekEngine::isObjectUnusable(int object, int action) {
	if (action == ACTION_LOOK)
		return false;
	if (object == OBJECT_REDSHIRT && _awayMission.redshirtDead)
		return true;
	if (object >= OBJECT_KIRK && object <= OBJECT_REDSHIRT && (_awayMission.crewDownBitset & (1 << object)))
		return true;
	if (object == OBJECT_IMTRICOR && (_awayMission.crewDownBitset & (1 << OBJECT_MCCOY)))
		return true;
	if (object == OBJECT_ISTRICOR && (_awayMission.crewDownBitset & (1 << OBJECT_SPOCK)))
		return true;
	return false;
}

void StarTrekEngine::hideInventoryIcons() {
	// Clear these sprites from the screen
	if (_itemIconSprite.drawMode == 2)
		_itemIconSprite.dontDrawNextFrame();
	if (_inventoryIconSprite.drawMode == 2)
		_inventoryIconSprite.dontDrawNextFrame();

	_gfx->drawAllSprites();

	if (_itemIconSprite.drawMode == 2) {
		_gfx->delSprite(&_itemIconSprite);
		_itemIconSprite.drawMode = 0;
		_itemIconSprite.bitmap.reset();
	}

	if (_inventoryIconSprite.drawMode == 2) {
		_gfx->delSprite(&_inventoryIconSprite);
		_inventoryIconSprite.drawMode = 0;
		_inventoryIconSprite.bitmap.reset();
	}
}

void StarTrekEngine::updateCrewmanGetupTimers() {
	if (_awayMission.crewDownBitset == 0)
		return;
	for (int i = OBJECT_KIRK; i <= OBJECT_REDSHIRT; i++) {
		Actor *actor = &_actorList[i];

		if (!(_awayMission.crewDownBitset & (1 << i)))
			continue;

		_awayMission.crewGetupTimers[i]--;
		if (_awayMission.crewGetupTimers[i] <= 0) {
			Common::String anim = getCrewmanAnimFilename(i, "getu");
			int8 dir = _awayMission.crewDirectionsAfterWalk[i];
			char d;
			if (dir == -1) {
				d = actor->direction;
			} else {
				const char *dirs = "nsew";
				Fixed8 scale = getActorScaleAtPosition(actor->sprite.pos.y);
				d = dirs[dir];

				int16 xOffset = 0, yOffset = 0;
				if (d == 'n') {
					xOffset = -24;
					yOffset = -8;
				} else if (d == 'w') {
					xOffset = -35;
					yOffset = -12;
				}
				actor->sprite.pos.x += scale.multToInt(xOffset);
				actor->sprite.pos.y += scale.multToInt(yOffset);
			}

			anim += (char)d;
			loadActorAnimWithRoomScaling(i, anim, actor->sprite.pos.x, actor->sprite.pos.y);
			_awayMission.crewDownBitset &= ~(1 << i);
		}
	}
}

int StarTrekEngine::showInventoryMenu(int x, int y, bool restoreMouse) {
	const int ITEMS_PER_ROW = 5;

	Common::Point oldMousePos = _gfx->getMousePos();
	bool keyboardControlledMouse = _keyboardControlsMouse;
	_keyboardControlsMouse = false;

	int itemIndex = 0;
	int numItems = 0;

	char itemNames[NUM_OBJECTS][10];
	Common::Point itemPositions[NUM_OBJECTS];
	int16 itemIndices[NUM_OBJECTS];

	while (itemIndex < NUM_OBJECTS) {
		if (_itemList[itemIndex].have) {
			strcpy(itemNames[numItems], _itemList[itemIndex].name);

			int16 itemX = (numItems % ITEMS_PER_ROW) * 32 + x;
			int16 itemY = (numItems / ITEMS_PER_ROW) * 32 + y;
			itemPositions[numItems] = Common::Point(itemX, itemY);
			itemIndices[numItems] = _itemList[itemIndex].field2;

			numItems++;
		}
		itemIndex++;
	}

	Sprite itemSprites[NUM_OBJECTS];

	for (int i = 0; i < numItems; i++) {
		_gfx->addSprite(&itemSprites[i]);

		itemSprites[i].drawMode = 2;
		itemSprites[i].pos.x = itemPositions[i].x;
		itemSprites[i].pos.y = itemPositions[i].y;
		itemSprites[i].drawPriority = 15;
		itemSprites[i].drawPriority2 = 8;
		itemSprites[i].setBitmap(_gfx->loadBitmap(itemNames[i]));
	}

	chooseMousePositionFromSprites(itemSprites, numItems, -1, 4);
	bool displayMenu = true;
	int lastItemIndex = -1;

	while (displayMenu) {
		_sound->checkLoopMusic();

		TrekEvent event;
		if (!getNextEvent(&event))
			continue;

		switch (event.type) {
		case TREKEVENT_TICK: {
			Common::Point mousePos = _gfx->getMousePos();
			itemIndex = getMenuButtonAt(itemSprites, numItems, mousePos.x, mousePos.y);
			if (itemIndex != lastItemIndex) {
				if (lastItemIndex != -1) {
					drawMenuButtonOutline(itemSprites[lastItemIndex].bitmap, 0);
					itemSprites[lastItemIndex].bitmapChanged = true;
				}
				if (itemIndex != -1) {
					drawMenuButtonOutline(itemSprites[itemIndex].bitmap, 15);
					itemSprites[itemIndex].bitmapChanged = true;
				}
				lastItemIndex = itemIndex;
			}
			_gfx->drawAllSprites();
			break;
		}

		case TREKEVENT_LBUTTONDOWN:
exitWithSelection:
			displayMenu = false;
			break;

		case TREKEVENT_RBUTTONDOWN:
exitWithoutSelection:
			displayMenu = false;
			lastItemIndex = -1;
			break;

		case TREKEVENT_KEYDOWN:
			switch (event.kbd.keycode) {
			case Common::KEYCODE_ESCAPE:
			case Common::KEYCODE_F2:
				goto exitWithoutSelection;

			case Common::KEYCODE_RETURN:
			case Common::KEYCODE_KP_ENTER:
			case Common::KEYCODE_F1:
				goto exitWithSelection;

			case Common::KEYCODE_HOME:
			case Common::KEYCODE_KP7:
				chooseMousePositionFromSprites(itemSprites, numItems, lastItemIndex, 4);
				break;

			case Common::KEYCODE_UP:
			case Common::KEYCODE_KP8:
			case Common::KEYCODE_PAGEUP:
			case Common::KEYCODE_KP9:
				chooseMousePositionFromSprites(itemSprites, numItems, lastItemIndex, 2);
				break;

			case Common::KEYCODE_LEFT:
			case Common::KEYCODE_KP4:
				chooseMousePositionFromSprites(itemSprites, numItems, lastItemIndex, 1);
				break;

			case Common::KEYCODE_RIGHT:
			case Common::KEYCODE_KP6:
				chooseMousePositionFromSprites(itemSprites, numItems, lastItemIndex, 0);
				break;

			case Common::KEYCODE_END:
			case Common::KEYCODE_KP1:
				chooseMousePositionFromSprites(itemSprites, numItems, lastItemIndex, 5);
				break;

			case Common::KEYCODE_DOWN:
			case Common::KEYCODE_KP2:
			case Common::KEYCODE_PAGEDOWN:
			case Common::KEYCODE_KP3:
				chooseMousePositionFromSprites(itemSprites, numItems, lastItemIndex, 3);
				break;

			default:
				break;
			}
			break;

		default:
			break;
		}

		removeNextEvent();
	}

	_sound->playSoundEffectIndex(0x10);
	if (lastItemIndex >= 0)
		drawMenuButtonOutline(itemSprites[lastItemIndex].bitmap, 0);

	for (int i = 0; i < numItems; i++)
		itemSprites[i].dontDrawNextFrame();

	_gfx->drawAllSprites();

	for (int i = 0; i < numItems; i++) {
		itemSprites[i].bitmap.reset();
		_gfx->delSprite(&itemSprites[i]);
	}

	if (lastItemIndex >= 0) {
		lastItemIndex = itemIndices[lastItemIndex];
	}

	if (restoreMouse)
		_gfx->warpMouse(oldMousePos.x, oldMousePos.y);

	_keyboardControlsMouse = keyboardControlledMouse;
	return lastItemIndex;
}

void StarTrekEngine::initStarfieldSprite(Sprite *sprite, Bitmap *bitmap, const Common::Rect &rect) {
	sprite->setXYAndPriority(rect.left, rect.top, 0);
	sprite->setBitmap(bitmap);
	bitmap->xoffset = 0;
	bitmap->yoffset = 0;
	bitmap->width = rect.width();
	bitmap->height = rect.height();
	_gfx->addSprite(sprite);
	sprite->drawMode = 1;
}

Bitmap *StarTrekEngine::scaleBitmap(Bitmap *bitmap, Fixed8 scale) {
	int scaledWidth  = scale.multToInt(bitmap->width);
	int scaledHeight = scale.multToInt(bitmap->height);
	int origWidth  = bitmap->width;
	int origHeight = bitmap->height;

	if (scaledWidth < 1)
		scaledWidth = 1;
	if (scaledHeight < 1)
		scaledHeight = 1;

	Bitmap *scaledBitmap = new Bitmap(scaledWidth, scaledHeight);
	scaledBitmap->xoffset = scale.multToInt(bitmap->xoffset);
	scaledBitmap->yoffset = scale.multToInt(bitmap->yoffset);

	// sub_344a5(scaledWidth, origWidth);

	origHeight--;
	scaledHeight--;

	byte *src = bitmap->pixels;
	byte *dest = scaledBitmap->pixels;

	if (scale <= 1.0) {
		int16 var2e = 0;
		uint16 var30 = scaledHeight << 1;
		uint16 var32 = (scaledHeight - origHeight) << 1;
		uint16 origRow = 0;

		while (origRow <= origHeight) {
			if (var2e < 0) {
				var2e += var30;
			} else {
				var2e += var32;
				scaleBitmapRow(src, dest, origWidth, scaledWidth);
				dest += scaledWidth;
			}

			src += bitmap->width;
			origRow++;
		}
	} else {
		int16 var2e = (origHeight << 1) - scaledHeight;
		uint16 var30 = origHeight << 1;
		uint16 var32 = (origHeight - scaledHeight) << 1;
		uint16 srcRowChanged = true;
		origWidth = bitmap->width;
		uint16 scaledRow = 0;
		byte *rowData = new byte[scaledWidth];

		while (scaledRow++ <= scaledHeight) {
			if (srcRowChanged) {
				scaleBitmapRow(src, rowData, origWidth, scaledWidth);
				srcRowChanged = false;
			}

			memcpy(dest, rowData, scaledWidth);
			dest += scaledWidth;

			if (var2e < 0) {
				var2e += var30;
			} else {
				var2e += var32;
				src += origWidth;
				srcRowChanged = true;
			}
		}

		delete[] rowData;
	}

	delete bitmap;

	return scaledBitmap;
}

void StarTrekEngine::scaleBitmapRow(byte *src, byte *dest, uint16 origWidth, uint16 scaledWidth) {
	if (origWidth >= scaledWidth) {
		int16 var2 = (scaledWidth << 1) - origWidth;
		uint16 var4 = scaledWidth << 1;
		uint16 var6 = (scaledWidth - origWidth) << 1;
		uint16 varE = 0;
		uint16 varA = 0;
		uint16 var8 = origWidth;
		uint16 di = 0;

		while (var8-- != 0) {
			if (var2 < 0) {
				var2 += var4;
			} else {
				var2 += var6;
				if (di != 0) {
					if (varE != 0) {
						*(dest - 1) = *src++;
						varE = 0;
						di--;
					}
					src += di;
					di = 0;
				}
				*dest++ = *src;
				varE = 1;
			}

			di++;
			varA++;
		}
	} else {
		int16 var2 = ((origWidth - 1) << 1) - (scaledWidth - 1);
		uint16 var4 = (origWidth - 1) << 1;
		uint16 var6 = ((origWidth - 1) - (scaledWidth - 1)) << 1;
		uint16 varA = 0;
		uint16 var8 = scaledWidth;
		uint16 di = 0;

		while (var8-- != 0) {
			if (di != 0) {
				src += di;
				di = 0;
			}
			*dest++ = *src;

			if (var2 < 0)
				var2 += var4;
			else {
				var2 += var6;
				di++;
			}

			varA++;
		}
	}
}

} // End of namespace StarTrek