/* 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 "common/endian.h"
#include "common/system.h"
#include "common/rect.h"
#include "engines/util.h"
#include "dragons/dragons.h"
#include "dragons/scene.h"
#include "dragons/screen.h"

namespace Dragons {

Screen::Screen() {
	//TODO add support for more video modes like RGB565 and RGBA555
	_pixelFormat = Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0);
	initGraphics(320, 200, &_pixelFormat);
	_backSurface = new Graphics::Surface();
	_backSurface->create(320, 200, _pixelFormat);
	_screenShakeOffset = Common::Point();
}

void Screen::updateScreen() {
	if (_screenShakeOffset.x != 0 || _screenShakeOffset.y != 0) {
		g_system->fillScreen(0); //TODO this is meant for 8bit screens. we should use system shake here.
	}
	Common::Rect clipRect = clipRectToScreen(_screenShakeOffset.x,  _screenShakeOffset.y, Common::Rect(_backSurface->w, _backSurface->h));
	g_system->copyRectToScreen((byte*)_backSurface->getBasePtr(clipRect.left, clipRect.top),
			_backSurface->pitch,
			_screenShakeOffset.x < 0 ? 0 : _screenShakeOffset.x, _screenShakeOffset.y < 0 ? 0 : _screenShakeOffset.y,
			clipRect.width(), clipRect.height());
//	if (_screenShakeOffset < 0) {
//		_backSurface->fillRect(Common::Rect(0, _backSurface->h + _screenShakeOffset - 1, _backSurface->w - 1, _backSurface->h - 1), 0);
//	}
//	if (_screenShakeOffset > 0) {
//		_backSurface->fillRect(Common::Rect(0, 0, _backSurface->w - 1, _screenShakeOffset - 1), 0);
//	}
	g_system->updateScreen();
}

Screen::~Screen() {
	_backSurface->free();
	delete _backSurface;
}

void Screen::copyRectToSurface(const Graphics::Surface &srcSurface, int destX, int destY) {
	copyRectToSurface(srcSurface.getBasePtr(0, 0), srcSurface.pitch, srcSurface.w, 0, destX, destY, srcSurface.w, srcSurface.h, false, NONE);
}

void Screen::copyRectToSurface(const Graphics::Surface &srcSurface, int destX, int destY, const Common::Rect srcRect, bool flipX, AlphaBlendMode alpha) {
	Common::Rect clipRect = clipRectToScreen(destX,  destY, srcRect);
	if (clipRect.width() == 0 || clipRect.height() == 0) {
		return;
	}

	if (destX < 0) {
		destX = 0;
	}
	if (destY < 0) {
		destY = 0;
	}

	copyRectToSurface(srcSurface.getBasePtr(clipRect.left, clipRect.top), srcSurface.pitch, srcSurface.w, clipRect.left, destX, destY, clipRect.width(), clipRect.height(), flipX, alpha);
}

void Screen::copyRectToSurface8bpp(const Graphics::Surface &srcSurface, const byte *palette, int destX, int destY, const Common::Rect srcRect, bool flipX, AlphaBlendMode alpha, uint16 scale) {
	if (scale != DRAGONS_ENGINE_SPRITE_100_PERCENT_SCALE) {
		drawScaledSprite(_backSurface, (const byte *)srcSurface.getBasePtr(0, 0),
				srcRect.width(), srcRect.height(),
				destX, destY,
				srcRect.width() * scale / DRAGONS_ENGINE_SPRITE_100_PERCENT_SCALE, srcRect.height() * scale / DRAGONS_ENGINE_SPRITE_100_PERCENT_SCALE,
				palette, flipX, alpha);
		return;
	}
	Common::Rect clipRect = clipRectToScreen(destX,  destY, srcRect);
	if (clipRect.width() == 0 || clipRect.height() == 0) {
		return;
	}

	if (destX < 0) {
		destX = 0;
	}
	if (destY < 0) {
		destY = 0;
	}
	//TODO should we have different methods for alpha modes?
	copyRectToSurface8bpp(srcSurface.getBasePtr(clipRect.left, clipRect.top), palette, srcSurface.pitch, srcSurface.w, clipRect.left, destX, destY, clipRect.width(), clipRect.height(), flipX, alpha);
}

/**
 * Fast RGB555 pixel blending
 * @param fg      The foreground color in uint16 RGB565 format
 * @param bg      The background color in uint16 RGB565 format
 * @param alpha   The alpha in range 0-255
 **/
uint16 alphaBlendRGB555(uint32 fg, uint32 bg, uint8 alpha){
	alpha = (alpha + 4) >> 3;
	bg = (bg | (bg << 16)) & 0x3e07c1f;
	fg = (fg | (fg << 16)) & 0x3e07c1f;
	uint32 result = ((((fg - bg) * alpha) >> 5) + bg) & 0x3e07c1f;
	return (uint16)((result >> 16) | result);
}

uint16 alphaBlendAdditiveRGB555(uint32 fg, uint32 bg){
	bg = (bg | (bg << 16)) & 0x3e07c1f;
	fg = (fg | (fg << 16)) & 0x3e07c1f;

	uint32 result = bg + fg;
	//clip r g b values to 565.
	if (result & (0x3f << 26)) {
		result &= 0x1fffff;
		result |= 0x3E00000;
	}

	if (result & 0x1F8000) {
		result &= 0x3e07fff;
		result |= 0x7C00;
	}

	if (result & 0x3E0) {
		result &= 0x3e07c1f;
		result |= 0x1f;
	}
	return (uint16)((result >> 16) | result);
}

void Screen::copyRectToSurface(const void *buffer, int srcPitch, int srcWidth, int srcXOffset, int destX, int destY, int width, int height, bool flipX, AlphaBlendMode alpha) {
	assert(buffer);

	assert(destX >= 0 && destX < _backSurface->w);
	assert(destY >= 0 && destY < _backSurface->h);
	assert(height > 0 && destY + height <= _backSurface->h);
	assert(width > 0 && destX + width <= _backSurface->w);

	// Copy buffer data to internal buffer
	const byte *src = (const byte *)buffer;
	byte *dst = (byte *)_backSurface->getBasePtr(destX, destY);
	for (int i = 0; i < height; i++) {
		for (int j = 0; j < width; j++) {
			int32 srcIdx = flipX ? srcWidth - (srcXOffset * 2) - j - 1 : j;
			if (src[srcIdx * 2] != 0 || src[srcIdx * 2 + 1] != 0) {
				if ((src[srcIdx * 2 + 1] & 0x80) == 0 || alpha == NONE) {
					// only copy opaque pixels
					dst[j * 2] = src[srcIdx * 2];
					dst[j * 2 + 1] = src[srcIdx * 2 + 1];
				} else {
					WRITE_SCREEN(&dst[j * 2], alphaBlendRGB555(READ_SCREEN(&src[srcIdx * 2]), READ_SCREEN(&dst[j * 2]), 128));
					// semi-transparent pixels.
				}
			}
		}
		src += srcPitch;
		dst += _backSurface->pitch;
	}
}

void Screen::copyRectToSurface8bpp(const void *buffer, const byte* palette, int srcPitch, int srcWidth, int srcXOffset, int destX, int destY, int width, int height, bool flipX, AlphaBlendMode alpha) {
	assert(buffer);

	assert(destX >= 0 && destX < _backSurface->w);
	assert(destY >= 0 && destY < _backSurface->h);
	assert(height > 0 && destY + height <= _backSurface->h);
	assert(width > 0 && destX + width <= _backSurface->w);

	// Copy buffer data to internal buffer
	const byte *src = (const byte *)buffer;
	byte *dst = (byte *)_backSurface->getBasePtr(destX, destY);
	for (int i = 0; i < height; i++) {
		for (int j = 0; j < width; j++) {
			int32 srcIdx = flipX ? srcWidth - (srcXOffset * 2) - j - 1 : j;
			uint16 c = READ_LE_UINT16(&palette[src[srcIdx] * 2]);
			if (c != 0) {
				if (!(c & 0x8000) || alpha == NONE) {
					// only copy opaque pixels
					WRITE_SCREEN(&dst[j * 2], c & ~0x8000);
				} else {
					// semi-transparent pixels.
					WRITE_SCREEN(&dst[j * 2], alpha == NORMAL
						? alphaBlendRGB555(c & 0x7fff, READ_SCREEN(&dst[j * 2]) & 0x7fff, 128)
						: alphaBlendAdditiveRGB555(c & 0x7fff, READ_SCREEN(&dst[j * 2]) & 0x7fff));
				}
			}
		}
		src += srcPitch;
		dst += _backSurface->pitch;
	}
}

void Screen::drawScaledSprite(Graphics::Surface *destSurface, const byte *source, int sourceWidth, int sourceHeight,
		int destX, int destY, int destWidth, int destHeight, const byte *palette, bool flipX, AlphaBlendMode alpha) {
	//TODO this logic is pretty messy. It should probably be re-written. It is trying to scale, clip, flip and blend at once.

	// Based on the GNAP engine scaling code
	if (destWidth == 0 || destHeight == 0) {
		return;
	}
	const int xs = ((sourceWidth - 1) << 16) / destWidth;
	const int ys = ((sourceHeight -1) << 16) / destHeight;
	int clipX = 0, clipY = 0;
	const int destPitch = destSurface->pitch;
	if (destX < 0) {
		clipX = -destX;
		destX = 0;
		destWidth -= clipX;
	}

	if (destY < 0) {
		clipY = -destY;
		destY = 0;
		destHeight -= clipY;
	}
	if (destY + destHeight >= destSurface->h) {
		destHeight = destSurface->h - destY;
	}
	if (destWidth < 0 || destHeight < 0)
		return;
	byte *dst = (byte *)destSurface->getBasePtr(destX, destY);
	int yi = ys * clipY;
	const byte *hsrc = source + sourceWidth * ((yi + 0x8000) >> 16);
	for (int yc = 0; yc < destHeight; ++yc) {
		byte *wdst = flipX ? dst + (destWidth - 1) * 2 : dst;
		int16 currX = flipX ? destX + (destWidth - 1) : destX;
		int xi = flipX ? xs : xs * clipX;
		const byte *wsrc = hsrc + ((xi + 0x8000) >> 16);
		for (int xc = 0; xc < destWidth; ++xc) {
			if (currX >= 0 && currX < destSurface->w) {
				byte colorIndex = *wsrc;
				uint16 c = READ_LE_UINT16(&palette[colorIndex * 2]);
				if (c != 0) {
					if (!(c & 0x8000u) || alpha == NONE) {
						// only copy opaque pixels
						WRITE_SCREEN(wdst, c & ~0x8000);
					} else {
						WRITE_SCREEN(wdst, alphaBlendRGB555(c & 0x7fffu, READ_SCREEN(wdst) & 0x7fffu, 128));
						// semi-transparent pixels.
					}
				}
			}
			currX += (flipX ? -1 : 1);
			wdst += (flipX ? -2 : 2);
			xi += xs;
			wsrc = hsrc + ((xi + 0x8000) >> 16);
		}
		dst += destPitch;
		yi += ys;
		hsrc = source + sourceWidth * ((yi + 0x8000) >> 16);
	}
}

Common::Rect Screen::clipRectToScreen(int destX, int destY, const Common::Rect rect) {
	return clipRectToRect(destX, destY, rect, Common::Rect(320, 200));
}

Common::Rect Screen::clipRectToRect(int destX, int destY, const Common::Rect rect, const Common::Rect containerRect) {
	int16 x, y, w, h;
	x = rect.left;
	y = rect.top;
	w = rect.width();
	h = rect.height();

	if (destX >= containerRect.width()) {
		w = 0;
	}

	if (destY >= containerRect.height()) {
		h = 0;
	}

	if (destX < 0) {
		w += destX;
		x += -destX;
	}

	if (destY < 0) {
		h += destY;
		y += -destY;
	}

	if (w > 0 && destX + w >= containerRect.width()) {
		w -= (destX + w) - containerRect.width();
	}

	if (h > 0 && destY + h >= containerRect.height()) {
		h -= (destY + h) - containerRect.height();
	}

	if (w < 0) {
		w = 0;
	}

	if (h < 0) {
		h = 0;
	}

	return Common::Rect(x, y, x + w, y + h);
}

void Screen::updatePaletteTransparency(uint16 paletteNum, uint16 startOffset, uint16 endOffset, bool isTransparent) {
	assert(paletteNum < DRAGONS_NUM_PALETTES);
	assert(startOffset < 256);
	assert(endOffset < 256);

	if (paletteNum == 0) {
		// set all layers to pixel addition blending (100% back + 100% sprite)
		DragonsEngine *vm = getEngine();
		vm->_scene->setLayerAlphaMode(0, ADDITIVE);
		vm->_scene->setLayerAlphaMode(1, ADDITIVE);
		vm->_scene->setLayerAlphaMode(2, ADDITIVE);
	}

	for (int i = startOffset; i <= endOffset; i++) {
		if (isTransparent) {
			_palettes[paletteNum][i * 2 + 1] |= 0x80;
		} else {
			_palettes[paletteNum][i * 2 + 1] &= ~0x80;
		}
	}
}

void Screen::loadPalette(uint16 paletteNum, const byte *palette) {
	bool isTransPalette = (paletteNum & 0x8000);
	paletteNum &= ~0x8000;
	assert(paletteNum < DRAGONS_NUM_PALETTES);
	if (paletteNum == 0) {
		memcpy(&_palettes[paletteNum][0], palette, 512);
	} else {
		memcpy(&_palettes[paletteNum][0], palette, 512);
		if (paletteNum == 2 || paletteNum == 4 || paletteNum == 5) {
			_palettes[paletteNum][2] = 0;
			_palettes[paletteNum][3] = 0;
		}
		if (paletteNum == 1) {
			_palettes[paletteNum][2] = 1;
			_palettes[paletteNum][3] = 0;
		}
	}

	for (int i =1 ; i < 0x100; i++) {
		uint16 c = READ_LE_INT16(&_palettes[paletteNum][i * 2]);
		if ((c & ~0x8000) == 0) {
			if (!isTransPalette) {
				WRITE_SCREEN(&_palettes[paletteNum][i * 2], 0x8000);
			}
		} else {
			//TODO is this needed? see load_palette_into_frame_buffer()
//			c = (uint16)(((uint)c & 0x1f) << 10) | (uint16)(((uint)c & 0x7c00) >> 10) |
//					(c & 0x3e0) | (c & 0x8000);
		}
	}
	WRITE_SCREEN(&_palettes[paletteNum][0], 0);
}

void Screen::setPaletteRecord(uint16 paletteNum, uint16 offset, uint16 newValue) {
	assert(paletteNum < DRAGONS_NUM_PALETTES);
	assert(offset < 256);
	WRITE_SCREEN(&_palettes[paletteNum][offset * 2], newValue);
}

byte *Screen::getPalette(uint16 paletteNum) {
	assert(paletteNum < DRAGONS_NUM_PALETTES);
	return _palettes[paletteNum];
}

void Screen::clearScreen() {
	_backSurface->fillRect(Common::Rect(0, 0, _backSurface->w, _backSurface->h), 0);
}

void Screen::drawRect(uint16 colour, Common::Rect rect, int id) {
	Common::Rect clippedRect = clipRectToScreen(0, 0, rect);
	//top
	_backSurface->drawLine(clippedRect.left, clippedRect.top, clippedRect.right, clippedRect.top, colour);
	//right
	_backSurface->drawLine(clippedRect.right, clippedRect.top, clippedRect.right, clippedRect.bottom, colour);
	//bottom
	_backSurface->drawLine(clippedRect.left, clippedRect.bottom, clippedRect.right, clippedRect.bottom, colour);
	//left
	_backSurface->drawLine(clippedRect.left, clippedRect.top, clippedRect.left, clippedRect.bottom, colour);

}

void Screen::setScreenShakeOffset(int16 x, int16 y) {
	_screenShakeOffset.x = x;
	_screenShakeOffset.y = y;
}

void Screen::copyRectToSurface8bppWrappedY(const Graphics::Surface &srcSurface, const byte *palette, int yOffset) {
	byte *dst = (byte *)_backSurface->getBasePtr(0, 0);
	for (int i = 0; i < DRAGONS_SCREEN_HEIGHT; i++) {
		const byte *src = (const byte *)srcSurface.getPixels() + ((yOffset + i) % srcSurface.h) * srcSurface.pitch;
		for (int j = 0; j < DRAGONS_SCREEN_WIDTH; j++) {
			uint16 c = READ_LE_UINT16(&palette[src[j] * 2]);
			if (c != 0) {
				WRITE_SCREEN(&dst[j * 2], c & ~0x8000);
			}
		}
		dst += _backSurface->pitch;
	}
}

void Screen::copyRectToSurface8bppWrappedX(const Graphics::Surface &srcSurface, const byte *palette, Common::Rect srcRect,
										   AlphaBlendMode alpha) {
	// Copy buffer data to internal buffer
	const byte *src = (const byte *)srcSurface.getBasePtr(0, 0);
	int width = srcSurface.w > DRAGONS_SCREEN_WIDTH ? DRAGONS_SCREEN_WIDTH : srcSurface.w;
	int height = srcRect.height();

	byte *dst = (byte *)_backSurface->getBasePtr(0, 0);
	for (int i = 0; i < height; i++) {
		for (int j = 0; j < width; j++) {
			int32 srcIdx = (i + srcRect.top) * srcSurface.w + ((j + srcRect.left) % srcSurface.w);
			uint16 c = READ_LE_UINT16(&palette[src[srcIdx] * 2]);
			if (c != 0) {
				if (!(c & 0x8000) || alpha == NONE) {
					// only copy opaque pixels
					WRITE_SCREEN(&dst[j * 2], c & ~0x8000);
				} else {
					WRITE_SCREEN(&dst[j * 2], alpha == NORMAL ? alphaBlendRGB555(c, READ_SCREEN(&dst[j * 2]), 128) : alphaBlendAdditiveRGB555(c, READ_SCREEN(&dst[j * 2])));
					// semi-transparent pixels.
				}
			}
		}
		dst += _backSurface->pitch;
	}
}

int16 Screen::addFlatQuad(int16 x0, int16 y0, int16 x1, int16 y1, int16 x3, int16 y3, int16 x2, int16 y2, uint16 colour,
						 int16 priorityLayer, uint16 flags) {

	assert(x0 == x2 && x1 == x3 && y0 == y1 && y2 == y3); //make sure this is a rectangle

	for (int i = 0; i < DRAGONS_NUM_FLAT_QUADS; i++) {
		if (!(_flatQuads[i].flags & 1u)) {
			_flatQuads[i].flags = flags | 1u;
			_flatQuads[i].points[0].x = x0;
			_flatQuads[i].points[0].y = y0;
			_flatQuads[i].points[1].x = x1;
			_flatQuads[i].points[1].y = y1;
			_flatQuads[i].points[2].x = x2;
			_flatQuads[i].points[2].y = y2;
			_flatQuads[i].points[3].x = x3;
			_flatQuads[i].points[3].y = y3;
			_flatQuads[i].colour = colour;
			_flatQuads[i].priorityLayer = priorityLayer;
			return i;
		}
	}

	return -1;
}

void Screen::drawFlatQuads(uint16 priorityLayer) {
	for (int i = 0; i < DRAGONS_NUM_FLAT_QUADS; i++) {
		if (_flatQuads[i].flags & 1u && _flatQuads[i].priorityLayer == priorityLayer) {
			//TODO need to support semitrans mode.
			//TODO check if we need to support non-rectangular quads.
			fillRect(_flatQuads[i].colour, Common::Rect(_flatQuads[i].points[0].x, _flatQuads[i].points[0].y, _flatQuads[i].points[3].x + 1, _flatQuads[i].points[3].y + 1));
		}
	}
}

void Screen::fillRect(uint16 colour, Common::Rect rect) {
	_backSurface->fillRect(rect, colour);
}

void Screen::clearAllFlatQuads() {
	for (int i = 0; i < DRAGONS_NUM_FLAT_QUADS; i++) {
		_flatQuads[i].flags = 0;
	}
}

FlatQuad *Screen::getFlatQuad(uint16 quadId) {
	assert(quadId < DRAGONS_NUM_FLAT_QUADS);
	return &_flatQuads[quadId];
}

} // End of namespace Dragons
