/*****************************************************************************
 * pce                                                                       *
 *****************************************************************************/

/*****************************************************************************
 * File name:   src/drivers/video/terminal.c                                 *
 * Created:     2003-04-18 by Hampa Hug <hampa@hampa.ch>                     *
 * Copyright:   (C) 2003-2020 Hampa Hug <hampa@hampa.ch>                     *
 *****************************************************************************/

/*****************************************************************************
 * This program is free software. You can redistribute it and / or modify it *
 * under the terms of the GNU General Public License version 2 as  published *
 * by the Free Software Foundation.                                          *
 *                                                                           *
 * 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.                                          *
 *****************************************************************************/


#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <drivers/video/terminal.h>


#define TRM_ESC_ESC  1
#define TRM_ESC_OK   2
#define TRM_ESC_ALT  4
#define TRM_ESC_CTRL 8
#define TRM_ESC_KEYS (TRM_ESC_ALT | TRM_ESC_CTRL)


void trm_init (terminal_t *trm, void *ext)
{
	trm->ext = ext;

	trm->set_msg_emu_ext = NULL;
	trm->set_msg_emu = NULL;

	trm->set_key_ext = NULL;
	trm->set_key = NULL;

	trm->set_mouse_ext = NULL;
	trm->set_mouse = NULL;

	trm->del = NULL;
	trm->set_msg_trm = NULL;
	trm->update = NULL;
	trm->check = NULL;

	trm->is_open = 0;

	trm->escape_key = PCE_KEY_ESC;
	trm->escape = 0;

	trm->w = 0;
	trm->h = 0;

	trm->buf_cnt = 0;
	trm->buf = NULL;

	trm->scale = 1;

	trm->aspect_x = 4;
	trm->aspect_y = 3;

	trm->min_w = 320;
	trm->min_h = 240;

	trm->mouse_scale_x[0] = 1;
	trm->mouse_scale_x[1] = 1;
	trm->mouse_scale_x[2] = 0;
	trm->mouse_scale_y[0] = 1;
	trm->mouse_scale_y[1] = 1;
	trm->mouse_scale_y[2] = 0;

	trm->scale_buf_cnt = 0;
	trm->scale_buf = NULL;

	trm->update_x = 0;
	trm->update_y = 0;
	trm->update_w = 0;
	trm->update_h = 0;

	trm->pict_index = 0;
}

void trm_free (terminal_t *trm)
{
	trm_close (trm);

	free (trm->buf);
	free (trm->scale_buf);
}

void trm_del (terminal_t *trm)
{
	if (trm == NULL) {
		return;
	}

	trm_free (trm);

	if (trm->del != NULL) {
		trm->del (trm->ext);
	}
}

int trm_open (terminal_t *trm, unsigned w, unsigned h)
{
	if (trm->is_open) {
		return (0);
	}

	trm_set_size (trm, w, h);

	if (trm->open != NULL) {
		if (trm->open (trm->ext, w, h)) {
			return (1);
		}
	}

	trm->is_open = 1;

	return (0);
}

int trm_close (terminal_t *trm)
{
	if (trm->is_open == 0) {
		return (0);
	}

	if (trm->close != NULL) {
		if (trm->close (trm->ext)) {
			return (1);
		}
	}

	trm->is_open = 0;

	return (0);
}

void trm_set_msg_fct (terminal_t *trm, void *ext, void *fct)
{
	trm->set_msg_emu_ext = ext;
	trm->set_msg_emu = fct;
}

void trm_set_key_fct (terminal_t *trm, void *ext, void *fct)
{
	trm->set_key_ext = ext;
	trm->set_key = fct;
}

void trm_set_mouse_fct (terminal_t *trm, void *ext, void *fct)
{
	trm->set_mouse_ext = ext;
	trm->set_mouse = fct;
}

int trm_screenshot (terminal_t *trm, const char *fname)
{
	FILE          *fp;
	char          str[256];
	unsigned long cnt;

	if ((fname == NULL) || (fname[0] == 0)) {
		sprintf (str, "pce%04u.ppm", trm->pict_index);
		trm->pict_index += 1;
		fname = str;
	}

	fp = fopen (fname, "wb");

	if (fp == NULL) {
		return (1);
	}

	cnt = 3 * (unsigned long) trm->w * (unsigned long) trm->h;

	fprintf (fp, "P6\n%u %u\n%u\x0a", trm->w, trm->h, 255);

	if (fwrite (trm->buf, 1, cnt, fp) != cnt) {
		fclose (fp);
		return (1);
	}

	fclose (fp);

	return (0);
}

int trm_set_msg_trm (terminal_t *trm, const char *msg, const char *val)
{
	if (strcmp (msg, "term.escape") == 0) {
		trm_set_escape_str (trm, val);
		return (0);
	}

	if (strcmp (msg, "term.screenshot") == 0) {
		trm_screenshot (trm, val);
		return (0);
	}

	if (trm->set_msg_trm != NULL) {
		return (trm->set_msg_trm (trm->ext, msg, val));
	}

	return (-1);
}

void trm_set_mouse_scale (terminal_t *trm, int mul_x, int div_x, int mul_y, int div_y)
{
	trm->mouse_scale_x[0] = mul_x;
	trm->mouse_scale_x[1] = div_x;
	trm->mouse_scale_x[2] = 0;

	trm->mouse_scale_y[0] = mul_y;
	trm->mouse_scale_y[1] = div_y;
	trm->mouse_scale_y[2] = 0;
}

void trm_set_size (terminal_t *trm, unsigned w, unsigned h)
{
	unsigned long cnt;

	if ((w == 0) || (h == 0)) {
		free (trm->buf);

		trm->buf_cnt = 0;
		trm->buf = NULL;

		trm->w = 0;
		trm->h = 0;

		return;
	}

	if ((trm->w == w) && (trm->h == h)) {
		return;
	}

	cnt = 3 * (unsigned long) w * (unsigned long) h;

	if (trm->buf_cnt != cnt) {
		unsigned char *tmp;

		tmp = realloc (trm->buf, cnt);
		if (tmp == NULL) {
			return;
		}

		trm->buf = tmp;
		trm->buf_cnt = cnt;
	}

	trm->w = w;
	trm->h = h;

	trm->update_x = 0;
	trm->update_y = 0;
	trm->update_w = w;
	trm->update_h = h;
}

void trm_set_min_size (terminal_t *trm, unsigned w, unsigned h)
{
	trm->min_w = w;
	trm->min_h = h;
}

void trm_set_scale (terminal_t *trm, unsigned v)
{
	trm->scale = (v < 1) ? 1 : v;

	trm->update_x = 0;
	trm->update_y = 0;
	trm->update_w = trm->w;
	trm->update_h = trm->h;
}

void trm_set_aspect_ratio (terminal_t *trm, unsigned x, unsigned y)
{
	trm->aspect_x = x;
	trm->aspect_y = y;
}

void trm_set_pixel (terminal_t *trm, unsigned x, unsigned y, const unsigned char *col)
{
	unsigned char *buf;

	buf = trm->buf + 3 * (trm->w * y + x);

	buf[0] = col[0];
	buf[1] = col[1];
	buf[2] = col[2];

	if (trm->update_w == 0) {
		trm->update_x = x;
		trm->update_w = 1;
	}
	else {
		if (x < trm->update_x) {
			trm->update_w += trm->update_x - x;
			trm->update_x = x;
		}

		if (x >= (trm->update_x + trm->update_w)) {
			trm->update_w = x - trm->update_x + 1;
		}
	}

	if (trm->update_h == 0) {
		trm->update_y = y;
		trm->update_h = 1;
	}
	else {
		if (y < trm->update_y) {
			trm->update_h += trm->update_y - y;
			trm->update_y = y;
		}

		if (y >= (trm->update_y + trm->update_h)) {
			trm->update_h = y - trm->update_y + 1;
		}
	}
}

void trm_set_lines (terminal_t *trm, const void *buf, unsigned y, unsigned cnt)
{
	unsigned long       w3, tmp;
	const unsigned char *src;
	unsigned char       *dst;

	w3 = 3UL * trm->w;

	src = buf;
	dst = trm->buf + w3 * y;

	while (cnt > 0) {
		if (memcmp (dst, src, w3) != 0) {
			break;
		}

		src += w3;
		dst += w3;
		y += 1;
		cnt -= 1;
	}

	tmp = cnt * w3;

	while (cnt > 0) {
		tmp -= w3;

		if (memcmp (dst + tmp, src + tmp, w3) != 0) {
			break;
		}

		cnt -= 1;
	}

	if (cnt == 0) {
		return;
	}

	memcpy (dst, src, w3 * cnt);

	trm->update_x = 0;
	trm->update_w = trm->w;

	if (trm->update_h == 0) {
		trm->update_y = y;
		trm->update_h = cnt;
	}
	else {
		if (y < trm->update_y) {
			trm->update_h += trm->update_y - y;
			trm->update_y = y;
		}

		if ((y + cnt) > (trm->update_y + trm->update_h)) {
			trm->update_h = (y + cnt) - trm->update_y;
		}
	}
}

void trm_update (terminal_t *trm)
{
	if ((trm->update_w == 0) || (trm->update_h == 0)) {
		return;
	}

	if (trm->update != NULL) {
		trm->update (trm->ext);
	}

	trm->update_x = 0;
	trm->update_y = 0;
	trm->update_w = 0;
	trm->update_h = 0;
}

void trm_check (terminal_t *trm)
{
	if (trm->check != NULL) {
		trm->check (trm->ext);
	}
}


int trm_set_msg_emu (terminal_t *trm, const char *msg, const char *val)
{
	if (trm->set_msg_emu != NULL) {
		return (trm->set_msg_emu (trm->set_msg_emu_ext, msg, val));
	}

	return (1);
}

int trm_set_escape_str (terminal_t *trm, const char *str)
{
	pce_key_t key;

	key = pce_key_from_string (str);

	if (key == PCE_KEY_NONE) {
		return (1);
	}

	trm->escape_key = key;

	return (0);
}

void trm_set_escape_key (terminal_t *trm, pce_key_t key)
{
	trm->escape_key = key;
}

static
int trm_set_key_magic (terminal_t *trm, pce_key_t key)
{
	switch (key) {
	case PCE_KEY_0:
		trm_set_msg_emu (trm, "emu.cpu.speed", "0");
		return (0);

	case PCE_KEY_1:
		trm_set_msg_emu (trm, "emu.cpu.speed", "1");
		return (0);

	case PCE_KEY_2:
		trm_set_msg_emu (trm, "emu.cpu.speed", "2");
		return (0);

	case PCE_KEY_3:
		trm_set_msg_emu (trm, "emu.cpu.speed", "3");
		return (0);

	case PCE_KEY_4:
		trm_set_msg_emu (trm, "emu.cpu.speed", "4");
		return (0);

	case PCE_KEY_5:
		trm_set_msg_emu (trm, "emu.cpu.speed", "5");
		return (0);

	case PCE_KEY_6:
		trm_set_msg_emu (trm, "emu.cpu.speed", "6");
		return (0);

	case PCE_KEY_7:
		trm_set_msg_emu (trm, "emu.cpu.speed", "7");
		return (0);

	case PCE_KEY_8:
		trm_set_msg_emu (trm, "emu.cpu.speed", "8");
		return (0);


	case PCE_KEY_A:
		trm_set_msg_trm (trm, "term.autosize", "");
		return (0);

	case PCE_KEY_F:
		trm_set_msg_trm (trm, "term.fullscreen.toggle", "");
		return (0);

	case PCE_KEY_G:
		trm_set_msg_trm (trm, "term.grab", "");
		return (0);

	case PCE_KEY_M:
		trm_set_msg_trm (trm, "term.release", "");
		trm_set_msg_trm (trm, "term.fullscreen", "0");
		trm_set_msg_emu (trm, "emu.stop", "1");
		return (0);

	case PCE_KEY_P:
		trm_set_msg_emu (trm, "emu.pause.toggle", "");
		return (0);

	case PCE_KEY_Q:
		trm_set_msg_trm (trm, "term.release", "");
		trm_set_msg_trm (trm, "term.fullscreen", "0");
		trm_set_msg_emu (trm, "emu.exit", "1");
		return (0);

	case PCE_KEY_R:
		trm_set_msg_emu (trm, "emu.reset", "1");
		return (0);

	case PCE_KEY_S:
		trm_screenshot (trm, NULL);
		return (0);

	case PCE_KEY_UP:
	case PCE_KEY_KP_8:
		trm_set_scale (trm, trm->scale + 1);
		return (0);

	case PCE_KEY_DOWN:
	case PCE_KEY_KP_2:
		if (trm->scale > 1) {
			trm_set_scale (trm, trm->scale - 1);
		}
		return (0);

	case PCE_KEY_LEFT:
	case PCE_KEY_KP_4:
		trm_set_msg_emu (trm, "emu.cpu.speed.step", "-1");
		return (0);

	case PCE_KEY_RIGHT:
	case PCE_KEY_KP_6:
		trm_set_msg_emu (trm, "emu.cpu.speed.step", "+1");
		return (0);

	default:
		return (1);
	}

	return (1);
}

static
void trm_set_key_emu (terminal_t *trm, unsigned event, pce_key_t key)
{
	if (event == PCE_KEY_EVENT_MAGIC) {
		if (trm_set_key_magic (trm, key) == 0) {
			return;
		}
	}

	if (trm->set_key != NULL) {
		trm->set_key (trm->set_key_ext, event, key);
	}
}

void trm_set_key (terminal_t *trm, unsigned event, pce_key_t key)
{
	if (event == PCE_KEY_EVENT_DOWN) {
		if (key == trm->escape_key) {
			trm->escape ^= TRM_ESC_ESC;
		}

		if (trm->escape & TRM_ESC_ESC) {
			return;
		}

		if (key == PCE_KEY_LALT) {
			trm->escape |= TRM_ESC_ALT;
		}
		else if (key == PCE_KEY_LCTRL) {
			trm->escape |= TRM_ESC_CTRL;
		}
		else if (key == PCE_KEY_PAUSE) {
			if ((trm->escape & (TRM_ESC_ALT | TRM_ESC_CTRL)) == 0) {
				trm_set_msg_trm (trm, "term.release", "");
				trm_set_msg_trm (trm, "term.fullscreen", "0");
				trm_set_msg_emu (trm, "emu.exit", "1");
				return;
			}
		}

		if ((trm->escape & TRM_ESC_KEYS) == TRM_ESC_KEYS) {
			trm_set_msg_trm (trm, "term.release", "");
			trm->escape &= ~TRM_ESC_KEYS;
		}
	}
	else if (event == PCE_KEY_EVENT_UP) {
		if (trm->escape & TRM_ESC_ESC) {
			if (key != trm->escape_key) {
				trm_set_key_emu (trm, PCE_KEY_EVENT_MAGIC, key);
				trm->escape &= ~TRM_ESC_ESC;
			}
			return;
		}

		if (key == PCE_KEY_LALT) {
			trm->escape &= ~TRM_ESC_ALT;
		}
		else if (key == PCE_KEY_LCTRL) {
			trm->escape &= ~TRM_ESC_CTRL;
		}
		else if (key == PCE_KEY_PAUSE) {
			return;
		}
	}

	trm_set_key_emu (trm, event, key);
}

void trm_set_mouse (terminal_t *trm, int dx, int dy, unsigned but)
{
	int tx, ty;

	dx = trm->mouse_scale_x[0] * dx + trm->mouse_scale_x[2];
	tx = dx;
	dx = dx / trm->mouse_scale_x[1];
	trm->mouse_scale_x[2] = tx - trm->mouse_scale_x[1] * dx;

	dy = trm->mouse_scale_y[0] * dy + trm->mouse_scale_y[2];
	ty = dy;
	dy = dy / trm->mouse_scale_y[1];
	trm->mouse_scale_y[2] = ty - trm->mouse_scale_y[1] * dy;

	if (trm->set_mouse != NULL) {
		trm->set_mouse (trm->set_mouse_ext, dx, dy, but);
	}
}

void trm_get_scale (terminal_t *trm, unsigned w, unsigned h, unsigned *fx, unsigned *fy)
{
	unsigned long f;
	unsigned long w2, h2;
	unsigned long maxw, maxh;

	if ((w == 0) || (h == 0)) {
		*fx = 0;
		*fy = 0;
		return;
	}

	f = 1;

	while (((f * w) < trm->min_w) && ((f * h) < trm->min_h)) {
		f += 1;
	}

	*fx = trm->scale + f - 1;
	*fy = trm->scale + f - 1;

	if ((trm->aspect_x > 0) && (trm->aspect_y > 0)) {
		w2 = *fx * w;
		h2 = *fy * h;

		maxw = (trm->aspect_x * h2) / trm->aspect_y;
		maxh = (trm->aspect_y * w2) / trm->aspect_x;

		while ((h2 + h) <= maxh) {
			h2 += h;
			*fy += 1;
		}

		while ((w2 + w) <= maxw) {
			w2 += w;
			*fx += 1;
		}

		if (w2 < maxw) {
			if ((trm->aspect_y * (w2 + w - maxw)) < (trm->aspect_x * (maxw - w2))) {
				w2 += w;
				*fx += 1;
			}
		}
	}
}

static
unsigned char *trm_scale_get_buf (terminal_t *trm, unsigned w, unsigned h)
{
	unsigned long cnt;
	unsigned char *tmp;

	cnt = 3 * (unsigned long) w * (unsigned long) h;

	if (cnt > trm->scale_buf_cnt) {
		tmp = realloc (trm->scale_buf, cnt);
		if (tmp == NULL) {
			return (NULL);
		}

		trm->scale_buf = tmp;
		trm->scale_buf_cnt = cnt;
	}

	return (trm->scale_buf);
}

static
void trm_scale_w1 (unsigned char *dst, const unsigned char *src, unsigned w, unsigned h, unsigned fy)
{
	unsigned i, n, y;

	n = 3 * w;

	for (y = 0; y < h; y++) {
		memcpy (dst, src, n);

		dst += n;
		src += n;

		for (i = 1; i < fy; i++) {
			memcpy (dst, dst - n, n);
			dst += n;
		}
	}
}

static
void trm_scale_w2 (unsigned char *dst, const unsigned char *src, unsigned w, unsigned h, unsigned fy)
{
	unsigned i, n, x, y;

	n = 6 * w;

	for (y = 0; y < h; y++) {
		for (x = 0; x < w; x++) {
			dst[0] = src[0];
			dst[1] = src[1];
			dst[2] = src[2];
			dst[3] = src[0];
			dst[4] = src[1];
			dst[5] = src[2];

			dst += 6;
			src += 3;
		}

		for (i = 1; i < fy; i++) {
			memcpy (dst, dst - n, n);
			dst += n;
		}
	}
}

static
void trm_scale_wx (unsigned char *dst, const unsigned char *src, unsigned w, unsigned h, unsigned fx, unsigned fy)
{
	unsigned i, j, n, x, y;

	n = 3 * fx * w;

	for (y = 0; y < h; y++) {
		for (x = 0; x < w; x++) {
			for (i = 0; i < fx; i++) {
				dst[0] = src[0];
				dst[1] = src[1];
				dst[2] = src[2];

				dst += 3;
			}

			src += 3;
		}

		for (j = 1; j < fy; j++) {
			memcpy (dst, dst - n, n);
			dst += n;
		}
	}
}

const unsigned char *trm_scale (terminal_t *trm,
	const unsigned char *src, unsigned w, unsigned h,
	unsigned fx, unsigned fy)
{
	unsigned      dw, dh;
	unsigned char *dst;

	if ((fx == 1) && (fy == 1)) {
		return (src);
	}

	dw = fx * w;
	dh = fy * h;

	dst = trm_scale_get_buf (trm, dw, dh);

	if (dst == NULL) {
		return (NULL);
	}

	if (fx == 1) {
		trm_scale_w1 (dst, src, w, h, fy);
	}
	else if (fx == 2) {
		trm_scale_w2 (dst, src, w, h, fy);
	}
	else {
		trm_scale_wx (dst, src, w, h, fx, fy);
	}

	return (dst);
}
