/*
 * Copyright (c) 2025 Jiri Svoboda
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/** @addtogroup libui
 * @{
 */
/**
 * @file User interface
 */

#include <adt/list.h>
#include <ctype.h>
#include <display.h>
#include <errno.h>
#include <fibril.h>
#include <fibril_synch.h>
#include <gfx/color.h>
#include <gfx/cursor.h>
#include <gfx/render.h>
#include <io/console.h>
#include <stdbool.h>
#include <stdlib.h>
#include <str.h>
#include <task.h>
#include <types/common.h>
#include <ui/clickmatic.h>
#include <ui/ui.h>
#include <ui/wdecor.h>
#include <ui/window.h>
#include "../private/wdecor.h"
#include "../private/window.h"
#include "../private/ui.h"

/** Parse output specification.
 *
 * Output specification has the form <proto>@<service> where proto is
 * eiher 'disp' for display service, 'cons' for console, 'null'
 * for dummy output. Service is a location ID service name (e.g. hid/display).
 *
 * @param ospec Output specification
 * @param ws Place to store window system type (protocol)
 * @param osvc Place to store pointer to output service name
 * @param ridev_id Place to store input device ID
 * @return EOK on success, EINVAL if syntax is invalid, ENOMEM if out of
 *         memory
 */
static errno_t ui_ospec_parse(const char *ospec, ui_winsys_t *ws,
    char **osvc, sysarg_t *ridev_id)
{
	const char *cp;
	const char *qm;
	const char *endptr;
	uint64_t idev_id;
	errno_t rc;

	*ridev_id = 0;

	cp = ospec;
	while (isalpha(*cp))
		++cp;

	/* Window system / protocol */
	if (*cp == '@') {
		if (str_lcmp(ospec, "disp@", str_length("disp@")) == 0) {
			*ws = ui_ws_display;
		} else if (str_lcmp(ospec, "cons@", str_length("cons@")) == 0) {
			*ws = ui_ws_console;
		} else if (str_lcmp(ospec, "null@", str_length("null@")) == 0) {
			*ws = ui_ws_null;
		} else if (str_lcmp(ospec, "@", str_length("@")) == 0) {
			*ws = ui_ws_any;
		} else {
			*ws = ui_ws_unknown;
		}

		++cp;
	} else {
		*ws = ui_ws_display;
	}

	/* Output service is the part before question mark */
	qm = str_chr(cp, '?');
	if (qm != NULL) {
		*osvc = str_ndup(cp, qm - cp);
	} else {
		/* No question mark */
		*osvc = str_dup(cp);
	}

	if (*osvc == NULL)
		return ENOMEM;

	if (qm != NULL) {
		/* The part after the question mark */
		cp = qm + 1;

		/* Input device ID parameter */
		if (str_lcmp(cp, "idev=", str_length("idev=")) == 0) {
			cp += str_length("idev=");

			rc = str_uint64_t(cp, &endptr, 10, false, &idev_id);
			if (rc != EOK)
				goto error;

			*ridev_id = idev_id;
			cp = endptr;
		}
	}

	if (*cp != '\0') {
		rc = EINVAL;
		goto error;
	}

	return EOK;
error:
	free(*osvc);
	*osvc = NULL;
	return rc;
}

/** Create new user interface.
 *
 * @param ospec Output specification or @c UI_DISPLAY_DEFAULT to use
 *              the default display service, UI_CONSOLE_DEFAULT to use
 *		the default console service, UI_DISPLAY_NULL to use
 *		dummy output.
 * @param rui Place to store pointer to new UI
 * @return EOK on success or an error code
 */
errno_t ui_create(const char *ospec, ui_t **rui)
{
	errno_t rc;
	display_t *display;
	console_ctrl_t *console;
	console_gc_t *cgc;
	ui_winsys_t ws;
	char *osvc;
	sysarg_t cols;
	sysarg_t rows;
	sysarg_t idev_id;
	ui_t *ui;

	rc = ui_ospec_parse(ospec, &ws, &osvc, &idev_id);
	if (rc != EOK)
		return rc;

	if (ws == ui_ws_display || ws == ui_ws_any) {
		rc = display_open((str_cmp(osvc, "") != 0) ? osvc :
		    DISPLAY_DEFAULT, &display);
		if (rc != EOK)
			goto disp_fail;

		rc = ui_create_disp(display, &ui);
		if (rc != EOK) {
			display_close(display);
			goto disp_fail;
		}

		free(osvc);
		ui->myoutput = true;
		ui->idev_id = idev_id;
		*rui = ui;
		return EOK;
	}

disp_fail:
	if (ws == ui_ws_console || ws == ui_ws_any) {
		console = console_init(stdin, stdout);
		if (console == NULL)
			goto cons_fail;

		rc = console_get_size(console, &cols, &rows);
		if (rc != EOK) {
			console_done(console);
			goto cons_fail;
		}

		console_cursor_visibility(console, false);

		/* ws == ui_ws_console */
		rc = ui_create_cons(console, &ui);
		if (rc != EOK) {
			console_done(console);
			goto cons_fail;
		}

		rc = console_gc_create(console, NULL, &cgc);
		if (rc != EOK) {
			ui_destroy(ui);
			console_done(console);
			goto cons_fail;
		}

		free(osvc);

		ui->cgc = cgc;
		ui->rect.p0.x = 0;
		ui->rect.p0.y = 0;
		ui->rect.p1.x = cols;
		ui->rect.p1.y = rows;

		(void) ui_paint(ui);
		ui->myoutput = true;
		*rui = ui;
		return EOK;
	}

cons_fail:
	if (ws == ui_ws_null) {
		free(osvc);
		rc = ui_create_disp(NULL, &ui);
		if (rc != EOK)
			return rc;

		ui->myoutput = true;
		*rui = ui;
		return EOK;
	}

	free(osvc);
	return EINVAL;
}

/** Create new user interface using console service.
 *
 * @param rui Place to store pointer to new UI
 * @return EOK on success or an error code
 */
errno_t ui_create_cons(console_ctrl_t *console, ui_t **rui)
{
	ui_t *ui;
	errno_t rc;

	ui = calloc(1, sizeof(ui_t));
	if (ui == NULL)
		return ENOMEM;

	rc = ui_clickmatic_create(ui, &ui->clickmatic);
	if (rc != EOK) {
		free(ui);
		return rc;
	}

	ui->console = console;
	list_initialize(&ui->windows);
	fibril_mutex_initialize(&ui->lock);
	*rui = ui;
	return EOK;
}

/** Create new user interface using display service.
 *
 * @param disp Display
 * @param rui Place to store pointer to new UI
 * @return EOK on success or an error code
 */
errno_t ui_create_disp(display_t *disp, ui_t **rui)
{
	ui_t *ui;
	errno_t rc;

	ui = calloc(1, sizeof(ui_t));
	if (ui == NULL)
		return ENOMEM;

	rc = ui_clickmatic_create(ui, &ui->clickmatic);
	if (rc != EOK) {
		free(ui);
		return rc;
	}

	ui->display = disp;
	list_initialize(&ui->windows);
	fibril_mutex_initialize(&ui->lock);
	*rui = ui;
	return EOK;
}

/** Destroy user interface.
 *
 * @param ui User interface or @c NULL
 */
void ui_destroy(ui_t *ui)
{
	if (ui == NULL)
		return;

	if (ui->myoutput) {
		if (ui->cgc != NULL)
			console_gc_delete(ui->cgc);
		if (ui->console != NULL) {
			console_cursor_visibility(ui->console, true);
			console_done(ui->console);
		}
		if (ui->display != NULL)
			display_close(ui->display);
	}

	free(ui);
}

static void ui_cons_event_process(ui_t *ui, cons_event_t *event)
{
	ui_window_t *awnd;
	ui_evclaim_t claim;
	pos_event_t pos;

	awnd = ui_window_get_active(ui);
	if (awnd == NULL)
		return;

	switch (event->type) {
	case CEV_KEY:
		ui_lock(ui);
		ui_window_send_kbd(awnd, &event->ev.key);
		ui_unlock(ui);
		break;
	case CEV_POS:
		pos = event->ev.pos;
		/* Translate event to window-relative coordinates */
		pos.hpos -= awnd->dpos.x;
		pos.vpos -= awnd->dpos.y;

		claim = ui_wdecor_pos_event(awnd->wdecor, &pos);
		/* Note: If event is claimed, awnd might not be valid anymore */
		if (claim == ui_unclaimed) {
			ui_lock(ui);
			ui_window_send_pos(awnd, &pos);
			ui_unlock(ui);
		}

		break;
	case CEV_RESIZE:
		ui_lock(ui);
		ui_window_send_resize(awnd);
		ui_unlock(ui);
		break;
	}
}

/** Execute user interface.
 *
 * Return task exit code of zero and block unitl the application starts
 * the termination process by calling ui_quit(@a ui).
 *
 * @param ui User interface
 */
void ui_run(ui_t *ui)
{
	cons_event_t event;
	usec_t timeout;
	errno_t rc;

	/* Only return command prompt if we are running in a separate window */
	if (ui->display != NULL)
		task_retval(0);

	while (!ui->quit) {
		if (ui->console != NULL) {
			timeout = 100000;
			rc = console_get_event_timeout(ui->console,
			    &event, &timeout);

			/* Do we actually have an event? */
			if (rc == EOK) {
				ui_cons_event_process(ui, &event);
			} else if (rc != ETIMEOUT) {
				/* Error, quit */
				break;
			}
		} else {
			fibril_usleep(100000);
		}
	}
}

/** Repaint UI (only used in fullscreen mode).
 *
 * This is used when an area is exposed in fullscreen mode.
 *
 * @param ui UI
 * @return @c EOK on success or an error code
 */
errno_t ui_paint(ui_t *ui)
{
	errno_t rc;
	gfx_context_t *gc;
	ui_window_t *awnd;
	gfx_color_t *color = NULL;

	/* In case of null output */
	if (ui->cgc == NULL)
		return EOK;

	gc = console_gc_get_ctx(ui->cgc);

	rc = gfx_color_new_ega(0x11, &color);
	if (rc != EOK)
		return rc;

	rc = gfx_set_color(gc, color);
	if (rc != EOK) {
		gfx_color_delete(color);
		return rc;
	}

	rc = gfx_fill_rect(gc, &ui->rect);
	if (rc != EOK) {
		gfx_color_delete(color);
		return rc;
	}

	gfx_color_delete(color);

	/* XXX Should repaint all windows */
	awnd = ui_window_get_active(ui);
	if (awnd == NULL)
		return EOK;

	rc = ui_wdecor_paint(awnd->wdecor);
	if (rc != EOK)
		return rc;

	return ui_window_paint(awnd);
}

/** Free up console for other users.
 *
 * Release console resources for another application (that the current
 * task is starting). After the other application finishes, resume
 * operation with ui_resume(). No calls to UI must happen inbetween
 * and no events must be processed (i.e. the calling function must not
 * return control to UI.
 *
 * @param ui UI
 * @return EOK on success or an error code
 */
errno_t ui_suspend(ui_t *ui)
{
	errno_t rc;

	assert(!ui->suspended);

	if (ui->cgc == NULL) {
		ui->suspended = true;
		return EOK;
	}

	(void) console_set_caption(ui->console, "");
	rc = console_gc_suspend(ui->cgc);
	if (rc != EOK)
		return rc;

	ui->suspended = true;
	return EOK;
}

/** Resume suspended UI.
 *
 * Reclaim console resources (after child application has finished running)
 * and restore UI operation previously suspended by calling ui_suspend().
 *
 * @param ui UI
 * @return EOK on success or an error code
 */
errno_t ui_resume(ui_t *ui)
{
	errno_t rc;
	ui_window_t *awnd;
	sysarg_t col;
	sysarg_t row;
	cons_event_t ev;

	assert(ui->suspended);

	if (ui->cgc == NULL) {
		ui->suspended = false;
		return EOK;
	}

	rc = console_get_pos(ui->console, &col, &row);
	if (rc != EOK)
		return rc;

	/*
	 * Here's a little heuristic to help determine if we need
	 * to pause before returning to the UI. If we are in the
	 * top-left corner, chances are the screen is empty and
	 * there is no need to pause.
	 */
	if (col != 0 || row != 0) {
		printf("Press any key or button to continue...\n");

		while (true) {
			rc = console_get_event(ui->console, &ev);
			if (rc != EOK)
				return EIO;

			if (ev.type == CEV_KEY && ev.ev.key.type == KEY_PRESS)
				break;

			if (ev.type == CEV_POS && ev.ev.pos.type == POS_PRESS)
				break;
		}
	}

	rc = console_gc_resume(ui->cgc);
	if (rc != EOK)
		return rc;

	ui->suspended = false;

	awnd = ui_window_get_active(ui);
	if (awnd != NULL)
		(void) console_set_caption(ui->console, awnd->wdecor->caption);

	rc = gfx_cursor_set_visible(console_gc_get_ctx(ui->cgc), false);
	if (rc != EOK)
		return rc;

	return EOK;
}

/** Determine if UI is suspended.
 *
 * @param ui UI
 * @return @c true iff UI is suspended
 */
bool ui_is_suspended(ui_t *ui)
{
	return ui->suspended;
}

/** Lock UI.
 *
 * Block UI from calling window callbacks. @c ui_lock() and @c ui_unlock()
 * must be used when accessing UI resources from a fibril (as opposed to
 * from a window callback).
 *
 * @param ui UI
 */
void ui_lock(ui_t *ui)
{
	if (ui->display != NULL)
		display_lock(ui->display);
	fibril_mutex_lock(&ui->lock);
}

/** Unlock UI.
 *
 * Allow UI to call window callbacks. @c ui_lock() and @c ui_unlock()
 * must be used when accessing window resources from a fibril (as opposed to
 * from a window callback).
 *
 * @param ui UI
 */
void ui_unlock(ui_t *ui)
{
	fibril_mutex_unlock(&ui->lock);
	if (ui->display != NULL)
		display_unlock(ui->display);
}

/** Terminate user interface.
 *
 * Calling this function causes the user interface to terminate
 * (i.e. exit from ui_run()). This would be typically called from
 * an event handler.
 *
 * @param ui User interface
 */
void ui_quit(ui_t *ui)
{
	ui->quit = true;
}

/** Determine if we are running in text mode.
 *
 * @param ui User interface
 * @return @c true iff we are running in text mode
 */
bool ui_is_textmode(ui_t *ui)
{
	/*
	 * XXX Currently console is always text and display is always
	 * graphics, but this need not always be true.
	 */
	return (ui->console != NULL);
}

/** Determine if we are emulating windows.
 *
 * @param ui User interface
 * @return @c true iff we are running in text mode
 */
bool ui_is_fullscreen(ui_t *ui)
{
	return (ui->display == NULL);
}

/** Get UI screen rectangle.
 *
 * @param ui User interface
 * @param rect Place to store bounding rectangle
 */
errno_t ui_get_rect(ui_t *ui, gfx_rect_t *rect)
{
	display_info_t info;
	sysarg_t cols, rows;
	errno_t rc;

	if (ui->display != NULL) {
		rc = display_get_info(ui->display, &info);
		if (rc != EOK)
			return rc;

		*rect = info.rect;
	} else if (ui->console != NULL) {
		rc = console_get_size(ui->console, &cols, &rows);
		if (rc != EOK)
			return rc;

		rect->p0.x = 0;
		rect->p0.y = 0;
		rect->p1.x = cols;
		rect->p1.y = rows;
	} else {
		return ENOTSUP;
	}

	return EOK;
}

/** Get clickmatic from UI.
 *
 * @pararm ui UI
 * @return Clickmatic
 */
ui_clickmatic_t *ui_get_clickmatic(ui_t *ui)
{
	return ui->clickmatic;
}

/** @}
 */
