/*
 * wlt - Terminals
 *
 * Copyright (c) 2012 David Herrmann <dh.herrmann@googlemail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * Wayland Terminal console helpers
 */

#include <errno.h>
#include <linux/input.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wayland-client.h>
#include <xkbcommon/xkbcommon.h>
#include "conf.h"
#include "eloop.h"
#include "log.h"
#include "pty.h"
#include "shl_misc.h"
#include "text.h"
#include "tsm_unicode.h"
#include "tsm_screen.h"
#include "tsm_vte.h"
#include "wlt_main.h"
#include "wlt_terminal.h"
#include "wlt_toolkit.h"

#define LOG_SUBSYSTEM "wlt_terminal"

struct wlt_terminal {
	struct ev_eloop *eloop;
	struct wlt_window *wnd;
	struct wlt_display *disp;
	struct wlt_widget *widget;
	struct wlt_shm_buffer buffer;
	struct wlt_rect alloc;

	struct tsm_screen *scr;
	struct tsm_vte *vte;
	struct kmscon_pty *pty;
	struct ev_fd *pty_fd;
	bool pty_open;

	struct kmscon_font_attr font_attr;
	struct kmscon_font *font_normal;
	unsigned int cols;
	unsigned int rows;

	wlt_terminal_cb cb;
	void *data;

	int pointer_x;
	int pointer_y;
	bool in_selection;
	bool selection_started;
	int sel_start_x;
	int sel_start_y;

	int paste_fd;
	struct ev_fd *paste;
	struct wl_data_source *copy;
	char *copy_buf;
	int copy_len;
};

static int draw_cell(struct tsm_screen *scr,
		     uint32_t id, const uint32_t *ch, size_t len,
		     unsigned int chwidth,
		     unsigned int posx, unsigned int posy,
		     const struct tsm_screen_attr *attr, void *data)
{
	struct wlt_terminal *term = data;
	const struct kmscon_glyph *glyph;
	int ret;
	unsigned int x, y, tmp, width, height, i, r, g, b;
	uint8_t *dst, *src;
	const struct uterm_video_buffer *buf;
	unsigned int fr, fg, fb, br, bg, bb;
	uint32_t val;

	if (!chwidth)
		return 0;

	if (!len)
		ret = kmscon_font_render_empty(term->font_normal, &glyph);
	else
		ret = kmscon_font_render(term->font_normal, id, ch, len,
					 &glyph);

	if (ret) {
		ret = kmscon_font_render_inval(term->font_normal, &glyph);
		if (ret)
			return ret;
	}

	buf = &glyph->buf;
	x = posx * term->font_normal->attr.width;
	y = posy * term->font_normal->attr.height;

	if (attr->inverse) {
		fr = attr->br;
		fg = attr->bg;
		fb = attr->bb;
		br = attr->fr;
		bg = attr->fg;
		bb = attr->fb;
	} else {
		fr = attr->fr;
		fg = attr->fg;
		fb = attr->fb;
		br = attr->br;
		bg = attr->bg;
		bb = attr->bb;
	}

	tmp = x + buf->width;
	if (tmp < x || x >= term->buffer.width)
		return 0;
	if (tmp > term->buffer.width)
		width = term->buffer.width - x;
	else
		width = buf->width;

	tmp = y + buf->height;
	if (tmp < y || y >= term->buffer.height)
		return 0;
	if (tmp > term->buffer.height)
		height = term->buffer.height - y;
	else
		height = buf->height;

	dst = term->buffer.data;
	dst = &dst[y * term->buffer.stride + x * 4];
	src = buf->data;

	/* Division by 256 instead of 255 increases
	 * speed by like 20% on slower machines.
	 * Downside is, full white is 254/254/254
	 * instead of 255/255/255. */
	while (height--) {
		for (i = 0; i < width; ++i) {
			if (src[i] == 0) {
				r = br;
				g = bg;
				b = bb;
			} else if (src[i] == 255) {
				r = fr;
				g = fg;
				b = fb;
			} else {
				r = fr * src[i] +
				    br * (255 - src[i]);
				r /= 256;
				g = fg * src[i] +
				    bg * (255 - src[i]);
				g /= 256;
				b = fb * src[i] +
				    bb * (255 - src[i]);
				b /= 256;
			}
			val = (0xff << 24) | (r << 16) | (g << 8) | b;
			((uint32_t*)dst)[i] = val;
		}
		dst += term->buffer.stride;
		src += buf->stride;
	}

	return 0;
}

static void draw_background(struct wlt_terminal *term)
{
	uint8_t *dst;
	uint32_t *line;
	unsigned int i, j, w, h;

	/* when maximized, we might have a right and bottom border. So draw
	 * a black background for everything beyond grid-size.
	 * TODO: we should catch the color from tsm_screen instead of using
	 * black background by default. */
	w = term->buffer.width;
	w /= term->font_normal->attr.width;
	w *= term->font_normal->attr.width;

	h = term->buffer.height;
	h /= term->font_normal->attr.height;
	h *= term->font_normal->attr.height;

	dst = term->buffer.data;
	for (i = 0; i < term->buffer.height; ++i) {
		line = (uint32_t*)dst;
		if (i >= h)
			j = 0;
		else
			j = w;
		for ( ; j < term->buffer.width; ++j)
			line[j] = 0xff << 24;
		dst += term->buffer.stride;
	}
}

static void widget_redraw(struct wlt_widget *widget, unsigned int flags,
			  void *data)
{
	struct wlt_terminal *term = data;

	draw_background(term);
	tsm_screen_draw(term->scr, NULL, draw_cell, NULL, term);
}

static void widget_resize(struct wlt_widget *widget, unsigned int flags,
			  struct wlt_rect *alloc, void *data)
{
	struct wlt_terminal *term = data;
	int ret;

	wlt_window_get_buffer(term->wnd, alloc, &term->buffer);
	memcpy(&term->alloc, alloc, sizeof(*alloc));

	/* don't allow children */
	alloc->width = 0;
	alloc->height = 0;

	term->cols = term->buffer.width / term->font_normal->attr.width;
	if (term->cols < 1)
		term->cols = 1;
	term->rows = term->buffer.height / term->font_normal->attr.height;
	if (term->rows < 1)
		term->rows = 1;

	ret = tsm_screen_resize(term->scr, term->cols, term->rows);
	if (ret)
		log_error("cannot resize TSM screen: %d", ret);
	kmscon_pty_resize(term->pty, term->cols, term->rows);
}

static void widget_prepare_resize(struct wlt_widget *widget,
				  unsigned int flags,
				  unsigned int width, unsigned int height,
				  unsigned int *min_width,
				  unsigned int *min_height,
				  unsigned int *new_width,
				  unsigned int *new_height,
				  void *data)
{
	struct wlt_terminal *term = data;
	unsigned int w, h;

	/* We are a catch-all handler. That is, we use all space that is
	 * available. We must be called _last_, which is guaranteed by
	 * registering the widget as last widget.
	 * All previous handlers put their size constraints into the arguemnts
	 * and we need to make sure to not break them.
	 * Every redraw-handler is guaranteed to work for every size, but still,
	 * we should try to avoid invalid-sizes to not generate artifacts. */

	if (flags & WLT_WINDOW_MAXIMIZED ||
	    flags & WLT_WINDOW_FULLSCREEN) {
		/* if maximized, always use requested size */
		*new_width = width;
		*new_height = height;
	} else {
		/* In normal mode, we want the console to "snap" to grid-sizes.
		 * That is, resizing is in steps instead of smooth. To guarantee
		 * that, we use the font-width/height and try to make the
		 * console as big as possible to fit the needed size.
		 * However, we also must make sure the minimal size is always
		 * guaranteed. */

		if (*new_width >= width) {
			*new_width += term->font_normal->attr.width;
		} else {
			w = width - *new_width;
			w /= term->font_normal->attr.width;
			if (!w)
				w = 1;
			w *= term->font_normal->attr.width;
			*new_width += w;
		}

		if (*new_width < *min_width) {
			w = *min_width - *new_width;
			w /= term->font_normal->attr.width;
			w += 1;
			w *= term->font_normal->attr.width;
			*new_width += w;
		}

		if (*new_height >= height) {
			*new_height += term->font_normal->attr.height;
		} else {
			h = height - *new_height;
			h /= term->font_normal->attr.height;
			if (!h)
				h = 1;
			h *= term->font_normal->attr.height;
			*new_height += h;
		}

		if (*new_height < *min_height) {
			h = *min_height - *new_height;
			h /= term->font_normal->attr.height;
			h += 1;
			h *= term->font_normal->attr.height;
			*new_height += h;
		}
	}
}

static void paste_event(struct ev_fd *fd, int mask, void *data)
{
	struct wlt_terminal *term = data;
	char buf[4096];
	int ret;

	if (mask & EV_READABLE) {
		ret = read(term->paste_fd, buf, sizeof(buf));
		if (ret == 0) {
			goto err_close;
		} else if (ret < 0) {
			if (errno == EAGAIN)
				return;
			log_error("error on paste-fd (%d): %m", errno);
			goto err_close;
		}

		kmscon_pty_write(term->pty, buf, ret);
		return;
	}

	if (mask & EV_ERR) {
		log_error("error on paste FD");
		goto err_close;
	}

	if (mask & EV_HUP)
		goto err_close;

	return;

err_close:
	close(term->paste_fd);
	ev_eloop_rm_fd(term->paste);
	term->paste = NULL;
}

static void copy_target(void *data, struct wl_data_source *w_source,
			const char *target)
{
}

static void copy_send(void *data, struct wl_data_source *w_source,
		      const char *mime, int32_t fd)
{
	struct wlt_terminal *term = data;
	int ret;

	/* TODO: make this non-blocking */
	ret = write(fd, term->copy_buf, term->copy_len);
	if (ret != term->copy_len)
		log_warning("cannot write whole selection: %d/%d", ret,
			    term->copy_len);
	close(fd);
}

static void copy_cancelled(void *data, struct wl_data_source *w_source)
{
	struct wlt_terminal *term = data;

	wl_data_source_destroy(w_source);
	if (term->copy == w_source)
		term->copy = NULL;
}

static const struct wl_data_source_listener copy_listener = {
	.target = copy_target,
	.send = copy_send,
	.cancelled = copy_cancelled,
};

static bool widget_key(struct wlt_widget *widget, unsigned int mask,
		       uint32_t sym, uint32_t ascii, uint32_t state,
		       bool handled, void *data)
{
	struct wlt_terminal *term = data;
	uint32_t ucs4;
	struct kmscon_font *font;
	int ret;

	if (handled || state != WL_KEYBOARD_KEY_STATE_PRESSED)
		return false;

	ucs4 = xkb_keysym_to_utf32(sym) ? : TSM_VTE_INVALID;

	if (conf_grab_matches(wlt_conf.grab_scroll_up,
			      mask, 1, &sym)) {
		tsm_screen_sb_up(term->scr, 1);
		wlt_window_schedule_redraw(term->wnd);
		return true;
	}
	if (conf_grab_matches(wlt_conf.grab_scroll_down,
			      mask, 1, &sym)) {
		tsm_screen_sb_down(term->scr, 1);
		wlt_window_schedule_redraw(term->wnd);
		return true;
	}
	if (conf_grab_matches(wlt_conf.grab_page_up,
			      mask, 1, &sym)) {
		tsm_screen_sb_page_up(term->scr, 1);
		wlt_window_schedule_redraw(term->wnd);
		return true;
	}
	if (conf_grab_matches(wlt_conf.grab_page_down,
			      mask, 1, &sym)) {
		tsm_screen_sb_page_down(term->scr, 1);
		wlt_window_schedule_redraw(term->wnd);
		return true;
	}

	if (conf_grab_matches(wlt_conf.grab_zoom_in,
			      mask, 1, &sym)) {
		if (term->font_attr.points + 1 < term->font_attr.points)
			return true;

		++term->font_attr.points;
		ret = kmscon_font_find(&font, &term->font_attr,
				       wlt_conf.font_engine);
		if (ret) {
			--term->font_attr.points;
			log_error("cannot create font");
		} else {
			kmscon_font_unref(term->font_normal);
			term->font_normal = font;
			wlt_window_schedule_redraw(term->wnd);
		}
		return true;
	}
	if (conf_grab_matches(wlt_conf.grab_zoom_out,
			      mask, 1, &sym)) {
		if (term->font_attr.points - 1 < 1)
			return true;

		--term->font_attr.points;
		ret = kmscon_font_find(&font, &term->font_attr,
				       wlt_conf.font_engine);
		if (ret) {
			++term->font_attr.points;
			log_error("cannot create font");
		} else {
			kmscon_font_unref(term->font_normal);
			term->font_normal = font;
			wlt_window_schedule_redraw(term->wnd);
		}
		return true;
	}

	if (conf_grab_matches(wlt_conf.grab_paste,
			      mask, 1, &sym)) {
		if (term->paste) {
			log_debug("cannot paste selection, previous paste still in progress");
			return true;
		}

		ret = wlt_display_get_selection_fd(term->disp,
						"text/plain;charset=utf-8");
		if (ret == -ENOENT) {
			log_debug("no selection to paste");
			return true;
		} else if (ret == -EAGAIN) {
			log_debug("unknown mime-time for pasting selection");
			return true;
		} else if (ret < 0) {
			log_error("cannot paste selection: %d", ret);
			return true;
		}

		term->paste_fd = ret;
		ret = ev_eloop_new_fd(term->eloop, &term->paste, ret,
				      EV_READABLE, paste_event, term);
		if (ret) {
			close(ret);
			log_error("cannot create eloop fd: %d", ret);
			return true;
		}

		return true;
	}

	if (conf_grab_matches(wlt_conf.grab_copy,
			      mask, 1, &sym)) {
		if (term->copy) {
			wl_data_source_destroy(term->copy);
			free(term->copy_buf);
			term->copy = NULL;
		}

		ret = wlt_display_new_data_source(term->disp, &term->copy);
		if (ret) {
			log_error("cannot create data source");
			return true;
		}

		term->copy_len = tsm_screen_selection_copy(term->scr,
							   &term->copy_buf);
		if (term->copy_len < 0) {
			if (term->copy_len != -ENOENT)
				log_error("cannot copy TSM selection: %d",
					  term->copy_len);
			wl_data_source_destroy(term->copy);
			term->copy = NULL;
			return true;
		}

		wl_data_source_offer(term->copy, "text/plain;charset=utf-8");
		wl_data_source_add_listener(term->copy, &copy_listener, term);
		wlt_display_set_selection(term->disp, term->copy);

		return true;
	}

	if (tsm_vte_handle_keyboard(term->vte, sym, ascii, mask, ucs4)) {
		tsm_screen_sb_reset(term->scr);
		wlt_window_schedule_redraw(term->wnd);
		return true;
	}

	return false;
}

static void pointer_motion(struct wlt_widget *widget,
			   unsigned int x, unsigned int y, void *data)
{
	struct wlt_terminal *term = data;
	unsigned int posx, posy;

	if (!wlt_rect_contains(&term->alloc, x, y)) {
		term->pointer_x = -1;
		term->pointer_y = -1;
		return;
	} else if (term->pointer_x == x - term->alloc.x &&
		   term->pointer_y == y - term->alloc.y) {
		return;
	} else {
		term->pointer_x = x - term->alloc.x;
		term->pointer_y = y - term->alloc.y;
	}

	if (term->in_selection) {
		if (!term->selection_started) {
			term->selection_started = true;
			posx = term->sel_start_x / term->font_normal->attr.width;
			posy = term->sel_start_y / term->font_normal->attr.height;

			tsm_screen_selection_start(term->scr, posx, posy);
		} else {
			posx = term->pointer_x / term->font_normal->attr.width;
			posy = term->pointer_y / term->font_normal->attr.height;

			tsm_screen_selection_target(term->scr, posx, posy);
		}

		wlt_window_schedule_redraw(term->wnd);
	}
}

static void pointer_enter(struct wlt_widget *widget,
			  unsigned int x, unsigned int y, void *data)
{
	struct wlt_terminal *term = data;

	pointer_motion(widget, x, y, term);
}

static void pointer_leave(struct wlt_widget *widget, void *data)
{
	struct wlt_terminal *term = data;

	term->pointer_x = -1;
	term->pointer_y = -1;
}

static void pointer_button(struct wlt_widget *widget,
			   uint32_t button, uint32_t state, void *data)
{
	struct wlt_terminal *term = data;

	if (button != BTN_LEFT)
		return;

	if (state == WL_POINTER_BUTTON_STATE_PRESSED) {
		if (!term->in_selection &&
		    term->pointer_x >= 0 && term->pointer_y >= 0) {
			term->in_selection = true;
			term->selection_started = false;

			term->sel_start_x = term->pointer_x;
			term->sel_start_y = term->pointer_y;
		}
	} else {
		if (term->pointer_x == term->sel_start_x &&
		    term->pointer_y == term->sel_start_y) {
			tsm_screen_selection_reset(term->scr);
			wlt_window_schedule_redraw(term->wnd);
		}

		term->in_selection = false;
	}
}

static void vte_event(struct tsm_vte *vte, const char *u8, size_t len,
		      void *data)
{
	struct wlt_terminal *term = data;

	kmscon_pty_write(term->pty, u8, len);
}

static void pty_input(struct kmscon_pty *pty, const char *u8, size_t len,
		      void *data)
{
	struct wlt_terminal *term = data;

	if (!len) {
		term->pty_open = false;
		if (term->cb)
			term->cb(term, WLT_TERMINAL_HUP, term->data);
	} else {
		tsm_vte_input(term->vte, u8, len);
		wlt_window_schedule_redraw(term->wnd);
	}
}

static void pty_event(struct ev_fd *fd, int mask, void *data)
{
	struct wlt_terminal *term = data;

	kmscon_pty_dispatch(term->pty);
}

static void widget_destroy(struct wlt_widget *widget, void *data)
{
	struct wlt_terminal *term = data;

	if (term->paste) {
		ev_eloop_rm_fd(term->paste);
		close(term->paste_fd);
	}
	tsm_vte_unref(term->vte);
	tsm_screen_unref(term->scr);
	free(term);
}

int wlt_terminal_new(struct wlt_terminal **out, struct wlt_window *wnd)
{
	struct wlt_terminal *term;
	int ret;

	if (!out || !wnd)
		return -EINVAL;

	term = malloc(sizeof(*term));
	if (!term)
		return -ENOMEM;
	memset(term, 0, sizeof(*term));
	term->wnd = wnd;
	term->disp = wlt_window_get_display(wnd);
	term->eloop = wlt_window_get_eloop(wnd);
	term->cols = 80;
	term->rows = 24;

	term->font_attr.ppi = wlt_conf.font_ppi;
	term->font_attr.points = wlt_conf.font_size;
	term->font_attr.bold = false;
	term->font_attr.italic = false;
	term->font_attr.width = 0;
	term->font_attr.height = 0;
	strncpy(term->font_attr.name, wlt_conf.font_name,
		KMSCON_FONT_MAX_NAME - 1);
	term->font_attr.name[KMSCON_FONT_MAX_NAME - 1] = 0;

	ret = kmscon_font_find(&term->font_normal, &term->font_attr,
			       wlt_conf.font_engine);
	if (ret) {
		log_error("cannot create font");
		goto err_free;
	}

	ret = tsm_screen_new(&term->scr, log_llog);
	if (ret) {
		log_error("cannot create tsm-screen object");
		goto err_font;
	}
	tsm_screen_set_max_sb(term->scr, wlt_conf.sb_size);

	ret = tsm_vte_new(&term->vte, term->scr, vte_event, term, log_llog);
	if (ret) {
		log_error("cannot create tsm-vte object");
		goto err_scr;
	}
	tsm_vte_set_palette(term->vte, wlt_conf.palette);

	ret = kmscon_pty_new(&term->pty, pty_input, term);
	if (ret) {
		log_error("cannot create pty object");
		goto err_vte;
	}
	kmscon_pty_set_term(term->pty, "xterm-256color");

	ret = kmscon_pty_set_term(term->pty, wlt_conf.term);
	if (ret)
		goto err_pty;

	ret = kmscon_pty_set_argv(term->pty, wlt_conf.argv);
	if (ret)
		goto err_pty;

	ret = ev_eloop_new_fd(term->eloop, &term->pty_fd,
			      kmscon_pty_get_fd(term->pty),
			      EV_READABLE, pty_event, term);
	if (ret)
		goto err_pty;

	ret = wlt_window_create_widget(wnd, &term->widget, term);
	if (ret) {
		log_error("cannot create terminal widget");
		goto err_pty_fd;
	}

	wlt_widget_set_destroy_cb(term->widget, widget_destroy);
	wlt_widget_set_redraw_cb(term->widget, widget_redraw);
	wlt_widget_set_resize_cb(term->widget, widget_prepare_resize,
				 widget_resize);
	wlt_widget_set_keyboard_cb(term->widget, widget_key);
	wlt_widget_set_pointer_cb(term->widget, pointer_enter, pointer_leave,
				  pointer_motion, pointer_button);
	*out = term;
	return 0;

err_pty_fd:
	ev_eloop_rm_fd(term->pty_fd);
err_pty:
	kmscon_pty_unref(term->pty);
err_vte:
	tsm_vte_unref(term->vte);
err_scr:
	tsm_screen_unref(term->scr);
err_font:
	kmscon_font_unref(term->font_normal);
err_free:
	free(term);
	return ret;
}

void wlt_terminal_destroy(struct wlt_terminal *term)
{
	if (!term)
		return;

	wlt_widget_destroy(term->widget);
}

int wlt_terminal_open(struct wlt_terminal *term, wlt_terminal_cb cb,
		      void *data)
{
	int ret;

	if (!term)
		return -EINVAL;

	if (term->pty_open)
		return -EALREADY;

	term->cb = cb;
	term->data = data;

	kmscon_pty_close(term->pty);
	ret = kmscon_pty_open(term->pty, term->cols, term->rows);
	if (ret)
		return ret;

	term->pty_open = true;
	return 0;
}
