/* 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.
 *
 * $URL$
 * $Id$
 *
 */

#include "common/scummsys.h"
#include "common/system.h"
#include "common/events.h"

#include "sci/include/gfx_driver.h"
#include "sci/include/gfx_tools.h"

namespace Sci {

struct _scummvm_driver_state {
	gfx_pixmap_t *priority[2];
	byte *visual[3];
	byte *pointer_data[2];
	int xsize, ysize;
	//int buckystate;
	bool update_screen;
	bool update_mouse;
};

#define S ((struct _scummvm_driver_state *)(drv->state))

static int
scummvm_init_specific(struct _gfx_driver *drv, int xfact, int yfact, int bytespp) {
	int i;

	if (!drv->state) // = S
		drv->state = new _scummvm_driver_state;
	if (!drv->state)
		return GFX_FATAL;

	S->xsize = xfact * 320;
	S->ysize = yfact * 200;

	S->pointer_data[0] = NULL;
	S->pointer_data[1] = NULL;
	//S->buckystate = 0;

	for (i = 0; i < 2; i++) {
		S->priority[i] = gfx_pixmap_alloc_index_data(gfx_new_pixmap(S->xsize, S->ysize, GFX_RESID_NONE, -i, -777));
		if (!S->priority[i]) {
			printf("Out of memory: Could not allocate priority maps! (%dx%d)\n", S->xsize, S->ysize);
			return GFX_FATAL;
		}
	}
	// create the visual buffers
	for (i = 0; i < 3; i++) {
		S->visual[i] = NULL;
		S->visual[i] = new byte[S->xsize * S->ysize];
		if (!S->visual[i]) {
			printf("Out of memory: Could not allocate visual buffers! (%dx%d)\n", S->xsize, S->ysize);
			return GFX_FATAL;
		}
		memset(S->visual[i], 0, S->xsize * S->ysize);
	}

	drv->mode = gfx_new_mode(xfact, yfact, bytespp, 0, 0, 0, 0, 0, 0, 0, 0, 256, 0);

	return GFX_OK;
}

static int scummvm_init(struct _gfx_driver *drv) {
	return scummvm_init_specific(drv, 1, 1, GFX_COLOR_MODE_INDEX);
}

static void scummvm_exit(struct _gfx_driver *drv) {
	int i;
	if (S) {
		for (i = 0; i < 2; i++) {
			gfx_free_pixmap(drv, S->priority[i]);
			S->priority[i] = NULL;
		}

		for (i = 0; i < 3; i++) {
			delete[] S->visual[i];
			S->visual[i] = NULL;
		}

		for (i = 0; i < 2; i++)
			if (S->pointer_data[i]) {
				delete[] S->pointer_data[i];
				S->pointer_data[i] = NULL;
			}

		delete S;
	}
}


// Drawing operations

// This code shamelessly lifted from the SDL_gfxPrimitives package
static void lineColor2(byte *dst, int16 x1, int16 y1, int16 x2, int16 y2, uint32 color) {
	int pixx, pixy;
	int x, y;
	int dx, dy;
	int sx, sy;
	int swaptmp;
	uint8 *pixel;

	dx = x2 - x1;
	dy = y2 - y1;
	sx = (dx >= 0) ? 1 : -1;
	sy = (dy >= 0) ? 1 : -1;

	dx = sx * dx + 1;
	dy = sy * dy + 1;
	pixx = 1;
	pixy = 320;
	pixel = ((uint8*)dst) + pixx * (int)x1 + pixy * (int)y1;
	pixx *= sx;
	pixy *= sy;
	if (dx < dy) {
		swaptmp = dx;
		dx = dy;
		dy = swaptmp;
		swaptmp = pixx;
		pixx = pixy;
		pixy = swaptmp;
	}

	// Draw
	x = 0;
	y = 0;
	for (; x < dx; x++, pixel += pixx) {
		*pixel = color;
		y += dy;
		if (y >= dx) {
			y -= dx;
			pixel += pixy;
		}
	}
}

static int scummvm_draw_line(struct _gfx_driver *drv, Common::Point start, Common::Point end,
                  gfx_color_t color, gfx_line_mode_t line_mode, gfx_line_style_t line_style) {
	uint32 scolor = color.visual.global_index;
	int xsize = S->xsize;
	int ysize = S->ysize;

	if (color.mask & GFX_MASK_VISUAL) {
		Common::Point nstart, nend;

		nstart.x = CLIP<int16>(start.x, 0, xsize);
		nstart.y = CLIP<int16>(start.y, 0, ysize);
		nend.x = CLIP<int16>(end.x, 0, xsize - 1);
		nend.y = CLIP<int16>(end.y, 0, ysize - 1);

		lineColor2(S->visual[1], (int16)nstart.x, (int16)nstart.y, (int16)nend.x, (int16)nend.y, scolor);

		if (color.mask & GFX_MASK_PRIORITY) {
			gfx_draw_line_pixmap_i(S->priority[0], nstart, nend, color.priority);
		}
	}

	return GFX_OK;
}

static int scummvm_draw_filled_rect(struct _gfx_driver *drv, rect_t rect, gfx_color_t color1, gfx_color_t color2,
                         gfx_rectangle_fill_t shade_mode) {
	if (color1.mask & GFX_MASK_VISUAL) {
		for (int i = rect.y; i < rect.y + rect.yl; i++) {
			memset(S->visual[1] + i * S->xsize + rect.x, color1.visual.global_index, rect.xl);
		}
	}

	if (color1.mask & GFX_MASK_PRIORITY)
		gfx_draw_box_pixmap_i(S->priority[0], rect, color1.priority);

	return GFX_OK;
}

// Pixmap operations

static int scummvm_draw_pixmap(struct _gfx_driver *drv, gfx_pixmap_t *pxm, int priority, 
							   rect_t src, rect_t dest, gfx_buffer_t buffer) {
	int bufnr = (buffer == GFX_BUFFER_STATIC) ? 2 : 1;
	int pribufnr = bufnr - 1;

	if (dest.xl != src.xl || dest.yl != src.yl) {
		printf("Attempt to scale pixmap (%dx%d)->(%dx%d): Not supported\n", src.xl, src.yl, dest.xl, dest.yl);
		return GFX_ERROR;
	}

	gfx_crossblit_pixmap(drv->mode, pxm, priority, src, dest, S->visual[bufnr], S->xsize,
	                     S->priority[pribufnr]->index_data, S->priority[pribufnr]->index_xl, 1, 0);

	return GFX_OK;
}

static int scummvm_grab_pixmap(struct _gfx_driver *drv, rect_t src, gfx_pixmap_t *pxm, gfx_map_mask_t map) {
	if (src.x < 0 || src.y < 0) {
		printf("Attempt to grab pixmap from invalid coordinates (%d,%d)\n", src.x, src.y);
		return GFX_ERROR;
	}

	if (!pxm->data) {
		printf("Attempt to grab pixmap to unallocated memory\n");
		return GFX_ERROR;
	}

	switch (map) {

	case GFX_MASK_VISUAL:
		pxm->xl = src.xl;
		pxm->yl = src.yl;
		for (int i = 0; i < src.yl; i++) {
			memcpy(pxm->data + i * src.xl, S->visual[1] + (i + src.y) * S->xsize + src.x, src.xl);
		}
		break;

	case GFX_MASK_PRIORITY:
		printf("FIXME: priority map grab not implemented yet");
		break;

	default:
		printf("Attempt to grab pixmap from invalid map 0x%02x\n", map);
		return GFX_ERROR;
	}

	return GFX_OK;
}

// Buffer operations

static int scummvm_update(struct _gfx_driver *drv, rect_t src, Common::Point dest, gfx_buffer_t buffer) {
	//TODO
	int data_source = (buffer == GFX_BUFFER_BACK) ? 2 : 1;
	int data_dest = data_source - 1;

	/*
	if (src.x != dest.x || src.y != dest.y) {
		printf("Updating %d (%d,%d)(%dx%d) to (%d,%d) on %d\n", buffer, src.x, src.y, src.xl, src.yl, dest.x, dest.y, data_dest);
	} else {
		printf("Updating %d (%d,%d)(%dx%d) to %d\n", buffer, src.x, src.y, src.xl, src.yl, data_dest);
	}
	*/

	switch (buffer) {
	case GFX_BUFFER_BACK:
		//memcpy(S->visual[data_dest], S->visual[data_source],
		//	S->xsize * S->ysize);
		for (int i = 0; i < src.yl; i++) {
			memcpy(S->visual[data_dest] + (dest.y + i) * S->xsize + dest.x,
			       S->visual[data_source] + (src.y + i) * S->xsize + src.x, src.xl);
		}

		if ((src.x == dest.x) && (src.y == dest.y))
			gfx_copy_pixmap_box_i(S->priority[0], S->priority[1], src);
		break;
	case GFX_BUFFER_FRONT:
		memcpy(S->visual[data_dest], S->visual[data_source], S->xsize * S->ysize);

		g_system->copyRectToScreen(S->visual[data_dest] + src.x + src.y * S->xsize, S->xsize, dest.x, dest.y, src.xl, src.yl);
		/*
		g_system->copyRectToScreen(S->visual[data_dest], S->xsize, 0, 0, S->xsize, S->ysize);
		*/
		S->update_screen = true;
		break;
	default:
		GFXERROR("Invalid buffer %d in update", buffer);
		return GFX_ERROR;
	}

	return GFX_OK;
}

static int scummvm_set_static_buffer(struct _gfx_driver *drv, gfx_pixmap_t *pic, gfx_pixmap_t *priority) {
	memcpy(S->visual[2], pic->data, S->xsize * S->ysize);
	/*gfx_crossblit_pixmap(drv->mode, pic, 0, rect, rect, S->visual[2], S->xsize, S->priority[1]->index_data,
							S->priority[1]->index_xl, 1, 0);*/

	gfx_copy_pixmap_box_i(S->priority[1], priority, gfx_rect(0, 0, S->xsize, S->ysize));

	return GFX_OK;
}

// Mouse pointer operations

static int scummvm_set_pointer(struct _gfx_driver *drv, gfx_pixmap_t *pointer) {
	if (pointer == NULL) {
		g_system->showMouse(false);
	} else {
		g_system->setMouseCursor(pointer->index_data, pointer->xl, pointer->yl, pointer->xoffset, pointer->yoffset);
		g_system->showMouse(true);
	}

	// Pointer pixmap or mouse position has changed
	S->update_mouse = true;
	return GFX_OK;
}

// Palette operations

static int scummvm_set_palette(struct _gfx_driver *drv, int index, byte red, byte green, byte blue) {
	if (index < 0 || index > 255) {
		GFXERROR("Attempt to set invalid palette entry %d\n", index);
		return GFX_ERROR;
	}

	byte color[] = {red, green, blue, 255};
	g_system->setPalette(color, index, 1);

	return GFX_OK;
}

// Event management

static sci_event_t scummvm_get_event(struct _gfx_driver *drv) {
	sci_event_t input = { SCI_EVT_NONE, 0, 0, 0 };

	Common::EventManager *em = g_system->getEventManager();
	Common::Event ev;

	bool found = em->pollEvent(ev);
	Common::Point p = ev.mouse;

	// Don't generate events for mouse movement
	while (found && ev.type == Common::EVENT_MOUSEMOVE) {
		found = em->pollEvent(ev);
		p = ev.mouse;
		drv->pointer_x = p.x;
		drv->pointer_y = p.y;
		S->update_mouse = true;
	}

	// Update the screen here, since it's called very often
	if (S->update_mouse)
		g_system->warpMouse(drv->pointer_x, drv->pointer_y);
	if (S->update_screen || S->update_mouse) {
		g_system->updateScreen();
		S->update_screen = false;
		S->update_mouse = false;
	}

	if (found && !ev.synthetic && ev.type != Common::EVENT_MOUSEMOVE) {
		int modifiers;
		if (ev.type == Common::EVENT_KEYDOWN)
			modifiers = ev.kbd.flags;
		else
			modifiers = em->getModifierState();

		input.buckybits =
		    ((modifiers & Common::KBD_ALT) ? SCI_EVM_ALT : 0) |
		    ((modifiers & Common::KBD_CTRL) ? SCI_EVM_CTRL : 0) |
		    ((modifiers & Common::KBD_SHIFT) ? SCI_EVM_LSHIFT | SCI_EVM_RSHIFT : 0);
		//TODO: SCI_EVM_SCRLOCK SCI_EVM_NUMLOCK SCI_EVM_CAPSLOCK SCI_EVM_INSERT

		switch (ev.type) {
			// Keyboard events
		case Common::EVENT_KEYDOWN:
			input.data = ev.kbd.keycode;
			input.character = ev.kbd.ascii;

			if (!(input.data & 0xFF00)) {
				// Directly accept most common keys without conversion
				input.type = SCI_EVT_KEYBOARD;
				if (input.data == Common::KEYCODE_TAB) {
					// Tab
					input.type = SCI_EVT_KEYBOARD;
					input.data = SCI_K_TAB;
					if (input.buckybits & (SCI_EVM_LSHIFT | SCI_EVM_RSHIFT))
						input.character = SCI_K_SHIFT_TAB;
					else
						input.character = SCI_K_TAB;
				}
			} else if ((input.data >= Common::KEYCODE_F1) && input.data <= Common::KEYCODE_F10) {
				// F1-F10
				input.type = SCI_EVT_KEYBOARD;
				// SCI_K_F1 == 59 << 8
				// SCI_K_SHIFT_F1 == 84 << 8
				input.data = (input.data - Common::KEYCODE_F1 + SCI_K_F1) << 8;
				if (input.buckybits & (SCI_EVM_LSHIFT | SCI_EVM_RSHIFT))
					input.character = input.data + ((SCI_K_SHIFT_F1 - SCI_K_F1) << 8);
				else
					input.character = input.data;
			} else {
				// Special keys that need conversion
				input.type = SCI_EVT_KEYBOARD;
				switch (ev.kbd.keycode) {
				case Common::KEYCODE_UP:
					input.data = SCI_K_UP;
					break;
				case Common::KEYCODE_DOWN:
					input.data = SCI_K_DOWN;
					break;
				case Common::KEYCODE_RIGHT:
					input.data = SCI_K_RIGHT;
					break;
				case Common::KEYCODE_LEFT:
					input.data = SCI_K_LEFT;
					break;
				case Common::KEYCODE_INSERT:
					input.data = SCI_K_INSERT;
					break;
				case Common::KEYCODE_HOME:
					input.data = SCI_K_HOME;
					break;
				case Common::KEYCODE_END:
					input.data = SCI_K_END;
					break;
				case Common::KEYCODE_PAGEUP:
					input.data = SCI_K_PGUP;
					break;
				case Common::KEYCODE_PAGEDOWN:
					input.data = SCI_K_PGDOWN;
					break;
				case Common::KEYCODE_DELETE:
					input.data = SCI_K_DELETE;
					break;
					//TODO: SCI_K_CENTER
				default:
					input.type = SCI_EVT_NONE;
					break;
				}
				input.character = input.data;
			}
			break;

			// Mouse events
		case Common::EVENT_LBUTTONDOWN:
			input.type = SCI_EVT_MOUSE_PRESS;
			input.data = 1;
			drv->pointer_x = p.x;
			drv->pointer_y = p.y;
			break;
		case Common::EVENT_RBUTTONDOWN:
			input.type = SCI_EVT_MOUSE_PRESS;
			input.data = 2;
			drv->pointer_x = p.x;
			drv->pointer_y = p.y;
			break;
		case Common::EVENT_LBUTTONUP:
			input.type = SCI_EVT_MOUSE_RELEASE;
			input.data = 1;
			drv->pointer_x = p.x;
			drv->pointer_y = p.y;
			break;
		case Common::EVENT_RBUTTONUP:
			input.type = SCI_EVT_MOUSE_RELEASE;
			input.data = 2;
			drv->pointer_x = p.x;
			drv->pointer_y = p.y;
			break;

			// Misc events
		case Common::EVENT_QUIT:
			input.type = SCI_EVT_QUIT;
			break;

		default:
			break;
		}
	}

	return input;
}

static int scummvm_usec_sleep(struct _gfx_driver *drv, long usecs) {
	g_system->delayMillis(usecs / 1000);
	return GFX_OK;
}

gfx_driver_t gfx_driver_scummvm = {
	NULL,
	0, 0,
	0,		// flags here
	0,
	NULL,
	scummvm_init_specific,
	scummvm_init,
	scummvm_exit,
	scummvm_draw_line,
	scummvm_draw_filled_rect,
	scummvm_draw_pixmap,
	scummvm_grab_pixmap,
	scummvm_update,
	scummvm_set_static_buffer,
	scummvm_set_pointer,
	scummvm_set_palette,
	scummvm_get_event,
	scummvm_usec_sleep,
	NULL
};

} // End of namespace Sci