/* $OpenBSD$ */

/*
 * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/types.h>

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

#include "tmux.h"

static struct screen	*window_switch_init(struct window_mode_entry *,
			     struct cmd_find_state *, struct args *);
static void		 window_switch_free(struct window_mode_entry *);
static void		 window_switch_resize(struct window_mode_entry *, u_int,
			     u_int);
static void		 window_switch_key(struct window_mode_entry *,
			     struct client *, struct session *,
			     struct winlink *, key_code, struct mouse_event *);
static enum prompt_result window_switch_prompt_callback(void *, const char *,
			     enum prompt_key_result);

#define WINDOW_SWITCH_DEFAULT_COMMAND "switch-client -Zt '%%'"

#define WINDOW_SWITCH_DEFAULT_FORMAT		\
	"#{?window_format," \
		"#{window_name} " \
		"#[dim]#{session_name}:#{window_index}#{window_flags}#[default] " \
		"#[dim]#{pane_current_command}#[default] " \
	        "#[dim]#{?#{!=:#{pane_title},#{host_short}},#{pane_title},}#[default]" \
	"," \
		"#{session_name} " \
		"#[dim]#{session_windows} windows#[default] " \
		"#{?session_attached,attached,#[dim]detached#[default]} " \
		"#[dim]#{window_name}#[default]" \
	"}"

const struct window_mode window_switch_mode = {
	.name = "switch-mode",
	.default_format = WINDOW_SWITCH_DEFAULT_FORMAT,

	.init = window_switch_init,
	.free = window_switch_free,
	.resize = window_switch_resize,
	.key = window_switch_key,
};

enum window_switch_type {
	WINDOW_SWITCH_TYPE_SESSION,
	WINDOW_SWITCH_TYPE_WINDOW
};

struct window_switch_itemdata {
	enum window_switch_type	 type;
	int			 session;
	int			 winlink;

	uint64_t		 tag;
	char			*text;
	bitstr_t		*match;

	u_int			 score;
	u_int			 order;
};

struct window_switch_modedata {
	struct window_pane		 *wp;
	struct screen			  screen;
	int				  zoomed;

	char				 *format;
	char				 *command;

	enum window_switch_type		  type;
	char				 *filter;
	struct prompt			 *prompt;
	u_int				  prompt_cx;

	struct window_switch_itemdata	**item_list;
	u_int				  item_size;

	struct window_switch_itemdata	**matches;
	u_int				  matches_size;

	u_int				  current;
	u_int				  offset;
};

static void
window_switch_free_item(struct window_switch_itemdata *item)
{
	free(item->match);
	free(item->text);
	free(item);
}

static struct window_switch_itemdata *
window_switch_add_item(struct window_switch_modedata *data)
{
	struct window_switch_itemdata	*item;

	data->item_list = xreallocarray(data->item_list, data->item_size + 1,
	    sizeof *data->item_list);
	item = data->item_list[data->item_size++] = xcalloc(1, sizeof *item);
	return (item);
}

static void
window_switch_add_session(struct window_switch_modedata *data,
    struct session *s, u_int *order)
{
	struct window_switch_itemdata	*item;
	struct format_tree		*ft;

	ft = format_create(NULL, NULL, FORMAT_NONE, 0);
	format_defaults(ft, NULL, s, NULL, NULL);

	item = window_switch_add_item(data);
	item->type = WINDOW_SWITCH_TYPE_SESSION;
	item->session = s->id;
	item->winlink = -1;
	item->tag = (uint64_t)s;
	item->order = (*order)++;
	item->text = format_expand(ft, data->format);

	format_free(ft);
}

static void
window_switch_add_window(struct window_switch_modedata *data,
    struct winlink *wl, u_int *order)
{
	struct window_switch_itemdata	*item;
	struct format_tree		*ft;

	ft = format_create(NULL, NULL, FORMAT_NONE, 0);
	format_defaults(ft, NULL, wl->session, wl, NULL);

	item = window_switch_add_item(data);
	item->type = WINDOW_SWITCH_TYPE_WINDOW;
	item->session = wl->session->id;
	item->winlink = wl->idx;
	item->tag = (uint64_t)wl;
	item->order = (*order)++;
	item->text = format_expand(ft, data->format);

	format_free(ft);
}

static int
window_switch_compare(const void *a0, const void *b0)
{
	struct window_switch_itemdata	*const *a = a0;
	struct window_switch_itemdata	*const *b = b0;

	if ((*a)->score > (*b)->score)
		return (-1);
	if ((*a)->score < (*b)->score)
		return (1);
	if ((*a)->order < (*b)->order)
		return (-1);
	if ((*a)->order > (*b)->order)
		return (1);
	return (0);
}

static void
window_switch_build(struct window_switch_modedata *data)
{
	struct window_switch_itemdata	 *item, **m = NULL;
	const char			 *f = data->filter;
	u_int				  ns, nw, i, n = 0, order = 0;
	u_int				  sx = screen_size_x(&data->screen);
	struct session			**sl;
	struct winlink			**wl;
	struct sort_criteria		  sort_crit;

	sort_crit.order = SORT_NAME;
	sort_crit.reversed = 0;

	for (i = 0; i < data->item_size; i++)
		window_switch_free_item(data->item_list[i]);
	free(data->item_list);
	data->item_list = NULL;
	data->item_size = 0;

	switch (data->type) {
	case WINDOW_SWITCH_TYPE_SESSION:
		sl = sort_get_sessions(&ns, &sort_crit);
		for (i = 0; i < ns; i++)
			window_switch_add_session(data, sl[i], &order);
		break;
	case WINDOW_SWITCH_TYPE_WINDOW:
		wl = sort_get_winlinks(&nw, &sort_crit);
		for (i = 0; i < nw; i++)
			window_switch_add_window(data, wl[i], &order);
		break;
	}

	for (i = 0; i < data->item_size; i++) {
		item = data->item_list[i];
		if (*f == '\0') {
			m = xreallocarray(m, n + 1, sizeof *m);
			m[n++] = item;
			continue;
		}

		item->match = fuzzy_match(f, item->text, sx, &item->score);
		if (item->match == NULL)
			continue;
		m = xreallocarray(m, n + 1, sizeof *m);
		m[n++] = item;
	}
	qsort(m, n, sizeof *m, window_switch_compare);

	free(data->matches);
	data->matches = m;
	data->matches_size = n;
}

static u_int
window_switch_visible(struct window_switch_modedata *data)
{
	u_int	sy = screen_size_y(&data->screen);

	if (sy <= 1)
		return (0);
	return (sy - 1);
}

static void
window_switch_set_current(struct window_switch_modedata *data, u_int current)
{
	u_int	visible = window_switch_visible(data);

	if (data->matches_size == 0) {
		data->current = 0;
		data->offset = 0;
		return;
	}

	if (current > data->matches_size - 1)
		current = data->matches_size - 1;
	data->current = current;

	if (data->current < data->offset)
		data->offset = data->current;
	else if (visible != 0 && data->current >= data->offset + visible)
		data->offset = data->current - visible + 1;
}

static void
window_switch_draw_screen(struct window_mode_entry *wme)
{
	struct window_pane		*wp = wme->wp;
	struct window_switch_modedata	*data = wme->data;
	struct options			*oo = wp->options;
	struct screen_write_ctx		 ctx;
	struct screen			*s = &data->screen;
	u_int				 sx = screen_size_x(s), i, j;
	u_int				 sy = screen_size_y(s), visible, idx;
	struct window_switch_itemdata	*item;
	struct grid_cell		 mgc, sgc, gc;
	const struct grid_cell		*dgc = &grid_default_cell;
	struct prompt_draw_data		 pdd;
	screen_write_start(&ctx, s);
	screen_write_clearscreen(&ctx, 8);

	if (sy <= 1) {
		screen_write_stop(&ctx);
		return;
	}

	style_apply(&mgc, oo, "switch-mode-match-style", NULL);
	style_apply(&sgc, oo, "mode-style", NULL);

	visible = window_switch_visible(data);
	for (i = 0; i < visible; i++) {
		idx = data->offset + i;
		if (idx >= data->matches_size)
			break;
		item = data->matches[idx];

		screen_write_cursormove(&ctx, 0, i, 0);
		if (idx != data->current)
			format_draw(&ctx, dgc, sx, item->text, NULL, 0);
		else {
			screen_write_clearendofline(&ctx, sgc.bg);
			format_draw(&ctx, &sgc, sx, item->text, NULL, 0);
		}

		if (item->match == NULL)
			continue;
		for (j = 0; j < sx; j++) {
			if (!bit_test(item->match, j))
				continue;
			grid_get_cell(s->grid, j, i, &gc);
			gc.attr = mgc.attr;
			gc.fg = mgc.fg;
			gc.bg = mgc.bg;
			screen_write_cursormove(&ctx, j, i, 0);
			screen_write_cell(&ctx, &gc);
		}
	}

	if (data->prompt != NULL) {
		pdd.ctx = &ctx;
		pdd.cursor_x = &data->prompt_cx;
		pdd.area_x = 0;
		pdd.area_width = sx;
		pdd.prompt_line = sy - 1;
		s->mode |= MODE_CURSOR;
		prompt_draw(data->prompt, &pdd);
		screen_write_cursormove(&ctx, data->prompt_cx, sy - 1, 0);
	}
	screen_write_stop(&ctx);
}

static struct screen *
window_switch_init(struct window_mode_entry *wme,
    struct cmd_find_state *fs, struct args *args)
{
	struct window_pane		*wp = wme->wp;
	struct window_switch_modedata	*data;
	struct screen			*s;
	struct prompt_create_data	 pd;

	wme->data = data = xcalloc(1, sizeof *data);
	data->wp = wp;

	if (args_has(args, 'w'))
		data->type = WINDOW_SWITCH_TYPE_WINDOW;
	else
		data->type = WINDOW_SWITCH_TYPE_SESSION;

	data->filter = xstrdup("");
	if (args == NULL || !args_has(args, 'F'))
		data->format = xstrdup(WINDOW_SWITCH_DEFAULT_FORMAT);
	else
		data->format = xstrdup(args_get(args, 'F'));
	if (args == NULL || args_count(args) == 0)
		data->command = xstrdup(WINDOW_SWITCH_DEFAULT_COMMAND);
	else
		data->command = xstrdup(args_string(args, 0));

	memset(&pd, 0, sizeof pd);
	prompt_set_options(&pd, fs->s);
	pd.fs = fs;
	pd.prompt = "(search) ";
	pd.input = "";
	pd.type = PROMPT_TYPE_SEARCH;
	pd.flags = PROMPT_INCREMENTAL|PROMPT_NOFORMAT|PROMPT_ISMODE|
	    PROMPT_EDITARROWS;
	pd.inputcb = window_switch_prompt_callback;
	pd.data = data;
	data->prompt = prompt_create(&pd);
	prompt_update(data->prompt, "(search) ", data->filter);

	if (!args_has(args, 'Z'))
		data->zoomed = -1;
	else {
		data->zoomed = (wp->window->flags & WINDOW_ZOOMED);
		if (!data->zoomed && window_zoom(wp) == 0)
			server_redraw_window(wp->window);
	}

	s = &data->screen;
	screen_init(s, screen_size_x(&wp->base), screen_size_y(&wp->base), 0);

	window_switch_build(data);
	prompt_incremental_start(data->prompt);
	window_switch_draw_screen(wme);

	return (s);
}

static void
window_switch_free(struct window_mode_entry *wme)
{
	struct window_switch_modedata	*data = wme->data;
	u_int				 i;

	if (data->zoomed == 0)
		server_unzoom_window(wme->wp->window);

	for (i = 0; i < data->item_size; i++)
		window_switch_free_item(data->item_list[i]);
	free(data->item_list);

	free(data->matches);
	free(data->filter);
	prompt_free(data->prompt);
	free(data->format);
	free(data->command);
	screen_free(&data->screen);

	free(data);
}

static void
window_switch_resize(struct window_mode_entry *wme, u_int sx, u_int sy)
{
	struct window_switch_modedata	*data = wme->data;
	struct screen			*s = &data->screen;

	screen_resize(s, sx, sy, 0);
	window_switch_build(data);
	window_switch_set_current(data, data->current);
	window_switch_draw_screen(wme);
}

static int
window_switch_run_command(struct window_switch_modedata *data, struct client *c)
{
	struct window_switch_itemdata	*item;
	struct cmd_find_state		 fs;
	struct session			*s;
	struct winlink			*wl;
	char				*target = NULL;
	struct cmdq_state		*state;
	char				*command, *error;
	enum cmd_parse_status		 status;

	if (data->matches_size == 0)
		return (0);
	item = data->matches[data->current];

	cmd_find_clear_state(&fs, 0);
	switch (item->type) {
	case WINDOW_SWITCH_TYPE_SESSION:
		s = session_find_by_id(item->session);
		if (s != NULL) {
			xasprintf(&target, "=%s:", s->name);
			cmd_find_from_session(&fs, s, 0);
		}
		break;
	case WINDOW_SWITCH_TYPE_WINDOW:
		s = session_find_by_id(item->session);
		if (s != NULL) {
			wl = winlink_find_by_index(&s->windows, item->winlink);
			if (s != NULL && wl != NULL) {
				xasprintf(&target, "=%s:%u.", s->name, wl->idx);
				cmd_find_from_winlink(&fs, wl, 0);
			}
		}
		break;
	}
	if (target == NULL)
		return (0);

	command = cmd_template_replace(data->command, target, 1);
	if (command != NULL && *command != '\0') {
		state = cmdq_new_state(&fs, NULL, 0);
		status = cmd_parse_and_append(command, NULL, c, state, &error);
		if (status == CMD_PARSE_ERROR) {
			if (c != NULL) {
				*error = toupper((u_char)*error);
				status_message_set(c, -1, 1, 0, 0, "%s", error);
			}
			free(error);
		}
		cmdq_free_state(state);
	}
	free(command);
	free(target);
	return (1);
}

static enum prompt_result
window_switch_prompt_callback(void *arg, const char *s,
    enum prompt_key_result key)
{
	struct window_switch_modedata	*data = arg;

	if (key != PROMPT_KEY_HANDLED)
		return (PROMPT_CONTINUE);

	if (s == NULL)
		s = "";
	else if (*s != '\0')
		s++;

	free(data->filter);
	data->filter = xstrdup(s);
	window_switch_build(data);
	data->current = 0;
	data->offset = 0;

	return (PROMPT_CONTINUE);
}

static void
window_switch_key(struct window_mode_entry *wme, struct client *c,
    __unused struct session *s, __unused struct winlink *wl, key_code key,
    struct mouse_event *m)
{
	struct window_pane		*wp = wme->wp;
	struct window_switch_modedata	*data = wme->data;
	u_int				 visible, current = data->current;
	u_int				 x, y, size = data->matches_size;
	enum prompt_key_result		 result;
	int				 redraw = 0;

	if (KEYC_IS_MOUSE(key)) {
		if (m == NULL || cmd_mouse_at(wp, m, &x, &y, 0) != 0)
			return;
		if (data->prompt != NULL && screen_size_y(&data->screen) != 0 &&
		    y == screen_size_y(&data->screen) - 1 &&
		    MOUSE_BUTTONS(m->b) == MOUSE_BUTTON_1 && !MOUSE_DRAG(m->b) &&
		    !MOUSE_RELEASE(m->b)) {
			result = prompt_mouse(data->prompt, x, 0,
			    screen_size_x(&data->screen), &redraw);
			if (redraw || result == PROMPT_KEY_HANDLED) {
				window_switch_draw_screen(wme);
				wp->flags |= PANE_REDRAW;
			}
			return;
		}
		switch (key) {
		case KEYC_WHEELUP_PANE:
			if (size != 0 && current != 0)
				window_switch_set_current(data, current - 1);
			goto moved;
		case KEYC_WHEELDOWN_PANE:
			if (size != 0 && current != size - 1)
				window_switch_set_current(data, current + 1);
			goto moved;
		case KEYC_MOUSEDOWN1_PANE:
		case KEYC_DOUBLECLICK1_PANE:
			if (y >= window_switch_visible(data) ||
			    data->offset + y >= size)
				return;
			window_switch_set_current(data, data->offset + y);
			if (key == KEYC_DOUBLECLICK1_PANE) {
				if (window_switch_run_command(data, c))
					window_pane_reset_mode(wp);
				return;
			}
			goto moved;
		}
		return;
	}

	switch (key) {
	case 'p'|KEYC_CTRL:
	case 'k'|KEYC_CTRL:
		key = KEYC_UP;
		break;
	case 'n'|KEYC_CTRL:
	case 'j'|KEYC_CTRL:
		key = KEYC_DOWN;
		break;
	}

	switch (key) {
	case '\r':
		if (window_switch_run_command(data, c))
			window_pane_reset_mode(wp);
		return;
	case '\033': /* Escape */
	case '['|KEYC_CTRL:
	case 'c'|KEYC_CTRL:
	case 'g'|KEYC_CTRL:
		window_pane_reset_mode(wp);
		return;
	}

	if (data->prompt != NULL) {
		result = prompt_key(data->prompt, key, &redraw);
		if (redraw) {
			window_switch_draw_screen(wme);
			wp->flags |= PANE_REDRAW;
		}
		if (result == PROMPT_KEY_HANDLED ||
		    result == PROMPT_KEY_NOT_HANDLED)
			return;
		current = data->current;
		size = data->matches_size;
	}

	switch (key) {
	case KEYC_UP:
		if (size == 0)
			goto moved;
		if (current == 0)
			window_switch_set_current(data, size - 1);
		else
			window_switch_set_current(data, current - 1);
		goto moved;
	case KEYC_DOWN:
		if (size == 0)
			goto moved;
		if (current == size - 1)
			window_switch_set_current(data, 0);
		else
			window_switch_set_current(data, current + 1);
		goto moved;
	case KEYC_PPAGE:
		visible = window_switch_visible(data);
		if (current >= visible)
			window_switch_set_current(data, current - visible);
		else
			window_switch_set_current(data, 0);
		goto moved;
	case KEYC_NPAGE:
		visible = window_switch_visible(data);
		window_switch_set_current(data, current + visible);
		goto moved;
	case KEYC_HOME:
		window_switch_set_current(data, 0);
		goto moved;
	case KEYC_END:
		if (size > 0)
			window_switch_set_current(data, size - 1);
		goto moved;
	}

moved:
	window_switch_draw_screen(wme);
	wp->flags |= PANE_REDRAW;
}
