/* $OpenBSD$ */

/*
 * Copyright (c) 2020 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_customize_init(struct window_mode_entry *,
			     struct cmd_find_state *, struct args *);
static void		 window_customize_free(struct window_mode_entry *);
static void		 window_customize_resize(struct window_mode_entry *,
			      u_int, u_int);
static void		 window_customize_key(struct window_mode_entry *,
			     struct client *, struct session *,
			     struct winlink *, key_code, struct mouse_event *);

#define WINDOW_CUSTOMIZE_DEFAULT_FORMAT \
	"#{?is_option," \
		"#{?option_is_global,,#[reverse](#{option_scope})#[default] }" \
		"#[ignore]" \
		"#{option_value}#{?option_unit, #{option_unit},}" \
	"," \
		"#{key}" \
	"}"

static const struct menu_item window_customize_menu_items[] = {
	{ "Select", '\r', NULL },
	{ "Expand", KEYC_RIGHT, NULL },
	{ "", KEYC_NONE, NULL },
	{ "Tag", 't', NULL },
	{ "Tag All", '\024', NULL },
	{ "Tag None", 'T', NULL },
	{ "", KEYC_NONE, NULL },
	{ "Cancel", 'q', NULL },

	{ NULL, KEYC_NONE, NULL }
};

const struct window_mode window_customize_mode = {
	.name = "options-mode",
	.default_format = WINDOW_CUSTOMIZE_DEFAULT_FORMAT,

	.init = window_customize_init,
	.free = window_customize_free,
	.resize = window_customize_resize,
	.key = window_customize_key,
};

enum window_customize_scope {
	WINDOW_CUSTOMIZE_NONE,
	WINDOW_CUSTOMIZE_KEY,
	WINDOW_CUSTOMIZE_SERVER,
	WINDOW_CUSTOMIZE_GLOBAL_SESSION,
	WINDOW_CUSTOMIZE_SESSION,
	WINDOW_CUSTOMIZE_GLOBAL_WINDOW,
	WINDOW_CUSTOMIZE_WINDOW,
	WINDOW_CUSTOMIZE_PANE
};

enum window_customize_change {
	WINDOW_CUSTOMIZE_UNSET,
	WINDOW_CUSTOMIZE_RESET,
};

struct window_customize_itemdata {
	struct window_customize_modedata	*data;
	enum window_customize_scope		 scope;

	char					*table;
	key_code				 key;

	struct options				*oo;
	char					*name;
	int					 idx;
};

struct window_customize_modedata {
	struct window_pane			 *wp;
	int					  dead;
	int					  references;

	struct mode_tree_data			 *data;
	char					 *format;
	int					  hide_global;

	struct window_customize_itemdata	**item_list;
	u_int					  item_size;

	struct cmd_find_state			  fs;
	enum window_customize_change		  change;
};

static uint64_t
window_customize_get_tag(struct options_entry *o, int idx,
    const struct options_table_entry *oe)
{
	uint64_t	offset;

	if (oe == NULL)
		return ((uint64_t)o);
	offset = ((char *)oe - (char *)options_table) / sizeof *options_table;
	return ((2ULL << 62)|(offset << 32)|((idx + 1) << 1)|1);
}

static struct options *
window_customize_get_tree(enum window_customize_scope scope,
    struct cmd_find_state *fs)
{
	switch (scope) {
	case WINDOW_CUSTOMIZE_NONE:
	case WINDOW_CUSTOMIZE_KEY:
		return (NULL);
	case WINDOW_CUSTOMIZE_SERVER:
		return (global_options);
	case WINDOW_CUSTOMIZE_GLOBAL_SESSION:
		return (global_s_options);
	case WINDOW_CUSTOMIZE_SESSION:
		return (fs->s->options);
	case WINDOW_CUSTOMIZE_GLOBAL_WINDOW:
		return (global_w_options);
	case WINDOW_CUSTOMIZE_WINDOW:
		return (fs->w->options);
	case WINDOW_CUSTOMIZE_PANE:
		return (fs->wp->options);
	}
	return (NULL);
}

static int
window_customize_check_item(struct window_customize_modedata *data,
    struct window_customize_itemdata *item, struct cmd_find_state *fsp)
{
	struct cmd_find_state	fs;

	if (fsp == NULL)
		fsp = &fs;

	if (cmd_find_valid_state(&data->fs))
		cmd_find_copy_state(fsp, &data->fs);
	else
		cmd_find_from_pane(fsp, data->wp, 0);
	return (item->oo == window_customize_get_tree(item->scope, fsp));
}

static int
window_customize_get_key(struct window_customize_itemdata *item,
    struct key_table **ktp, struct key_binding **bdp)
{
	struct key_table	*kt;
	struct key_binding	*bd;

	kt = key_bindings_get_table(item->table, 0);
	if (kt == NULL)
		return (0);
	bd = key_bindings_get(kt, item->key);
	if (bd == NULL)
		return (0);

	if (ktp != NULL)
		*ktp = kt;
	if (bdp != NULL)
		*bdp = bd;
	return (1);
}

static char *
window_customize_scope_text(enum window_customize_scope scope,
    struct cmd_find_state *fs)
{
	char	*s;
	u_int	 idx;

	switch (scope) {
	case WINDOW_CUSTOMIZE_PANE:
		window_pane_index(fs->wp, &idx);
		xasprintf(&s, "pane %u", idx);
		break;
	case WINDOW_CUSTOMIZE_SESSION:
		xasprintf(&s, "session %s", fs->s->name);
		break;
	case WINDOW_CUSTOMIZE_WINDOW:
		xasprintf(&s, "window %u", fs->wl->idx);
		break;
	default:
		s = xstrdup("");
		break;
	}
	return (s);
}

static struct window_customize_itemdata *
window_customize_add_item(struct window_customize_modedata *data)
{
	struct window_customize_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_customize_free_item(struct window_customize_itemdata *item)
{
	free(item->table);
	free(item->name);
	free(item);
}

static void
window_customize_build_array(struct window_customize_modedata *data,
    struct mode_tree_item *top, enum window_customize_scope scope,
    struct options_entry *o, struct format_tree *ft)
{
	const struct options_table_entry	*oe = options_table_entry(o);
	struct options				*oo = options_owner(o);
	struct window_customize_itemdata	*item;
	struct options_array_item		*ai;
	char					*name, *value, *text;
	u_int					 idx;
	uint64_t				 tag;

	ai = options_array_first(o);
	while (ai != NULL) {
		idx = options_array_item_index(ai);

		xasprintf(&name, "%s[%u]", options_name(o), idx);
		format_add(ft, "option_name", "%s", name);
		value = options_to_string(o, idx, 0);
		format_add(ft, "option_value", "%s", value);

		item = window_customize_add_item(data);
		item->scope = scope;
		item->oo = oo;
		item->name = xstrdup(options_name(o));
		item->idx = idx;

		text = format_expand(ft, data->format);
		tag = window_customize_get_tag(o, idx, oe);
		mode_tree_add(data->data, top, item, tag, name, text, -1);
		free(text);

		free(name);
		free(value);

		ai = options_array_next(ai);
	}
}

static void
window_customize_build_option(struct window_customize_modedata *data,
    struct mode_tree_item *top, enum window_customize_scope scope,
    struct options_entry *o, struct format_tree *ft,
    const char *filter, struct cmd_find_state *fs)
{
	const struct options_table_entry	*oe = options_table_entry(o);
	struct options				*oo = options_owner(o);
	const char				*name = options_name(o);
	struct window_customize_itemdata	*item;
	char					*text, *expanded, *value;
	int					 global = 0, array = 0;
	uint64_t				 tag;

	if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK))
		return;
	if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY))
		array = 1;

	if (scope == WINDOW_CUSTOMIZE_SERVER ||
	    scope == WINDOW_CUSTOMIZE_GLOBAL_SESSION ||
	    scope == WINDOW_CUSTOMIZE_GLOBAL_WINDOW)
		global = 1;
	if (data->hide_global && global)
		return;

	format_add(ft, "option_name", "%s", name);
	format_add(ft, "option_is_global", "%d", global);
	format_add(ft, "option_is_array", "%d", array);

	text = window_customize_scope_text(scope, fs);
	format_add(ft, "option_scope", "%s", text);
	free(text);

	if (oe != NULL && oe->unit != NULL)
		format_add(ft, "option_unit", "%s", oe->unit);
	else
		format_add(ft, "option_unit", "%s", "");

	if (!array) {
		value = options_to_string(o, -1, 0);
		format_add(ft, "option_value", "%s", value);
		free(value);
	}

	if (filter != NULL) {
		expanded = format_expand(ft, filter);
		if (!format_true(expanded)) {
			free(expanded);
			return;
		}
		free(expanded);
	}
	item = window_customize_add_item(data);
	item->oo = oo;
	item->scope = scope;
	item->name = xstrdup(name);
	item->idx = -1;

	if (array)
		text = NULL;
	else
		text = format_expand(ft, data->format);
	tag = window_customize_get_tag(o, -1, oe);
	top = mode_tree_add(data->data, top, item, tag, name, text, 0);
	free(text);

	if (array)
		window_customize_build_array(data, top, scope, o, ft);
}

static void
window_customize_find_user_options(struct options *oo, const char ***list,
    u_int *size)
{
	struct options_entry	*o;
	const char		*name;
	u_int			 i;

	o = options_first(oo);
	while (o != NULL) {
		name = options_name(o);
		if (*name != '@') {
			o = options_next(o);
			continue;
		}
		for (i = 0; i < *size; i++) {
			if (strcmp((*list)[i], name) == 0)
				break;
		}
		if (i != *size) {
			o = options_next(o);
			continue;
		}
		*list = xreallocarray(*list, (*size) + 1, sizeof **list);
		(*list)[(*size)++] = name;

		o = options_next(o);
	}
}

static void
window_customize_build_options(struct window_customize_modedata *data,
    const char *title, uint64_t tag,
    enum window_customize_scope scope0, struct options *oo0,
    enum window_customize_scope scope1, struct options *oo1,
    enum window_customize_scope scope2, struct options *oo2,
    struct format_tree *ft, const char *filter, struct cmd_find_state *fs)
{
	struct mode_tree_item		 *top;
	struct options_entry		 *o = NULL, *loop;
	const char			**list = NULL, *name;
	u_int				  size = 0, i;
	enum window_customize_scope	  scope;

	top = mode_tree_add(data->data, NULL, NULL, tag, title, NULL, 0);
	mode_tree_no_tag(top);

	/*
	 * We get the options from the first tree, but build it using the
	 * values from the other two. Any tree can have user options so we need
	 * to build a separate list of them.
	 */

	window_customize_find_user_options(oo0, &list, &size);
	if (oo1 != NULL)
		window_customize_find_user_options(oo1, &list, &size);
	if (oo2 != NULL)
		window_customize_find_user_options(oo2, &list, &size);

	for (i = 0; i < size; i++) {
		if (oo2 != NULL)
			o = options_get(oo0, list[i]);
		if (o == NULL && oo1 != NULL)
			o = options_get(oo1, list[i]);
		if (o == NULL)
			o = options_get(oo2, list[i]);
		if (options_owner(o) == oo2)
			scope = scope2;
		else if (options_owner(o) == oo1)
			scope = scope1;
		else
			scope = scope0;
		window_customize_build_option(data, top, scope, o, ft, filter,
		    fs);
	}
	free(list);

	loop = options_first(oo0);
	while (loop != NULL) {
		name = options_name(loop);
		if (*name == '@') {
			loop = options_next(loop);
			continue;
		}
		if (oo2 != NULL)
			o = options_get(oo2, name);
		else if (oo1 != NULL)
			o = options_get(oo1, name);
		else
			o = loop;
		if (options_owner(o) == oo2)
			scope = scope2;
		else if (options_owner(o) == oo1)
			scope = scope1;
		else
			scope = scope0;
		window_customize_build_option(data, top, scope, o, ft, filter,
		    fs);
		loop = options_next(loop);
	}
}

static void
window_customize_build_keys(struct window_customize_modedata *data,
    struct key_table *kt, struct format_tree *ft, const char *filter,
    struct cmd_find_state *fs, u_int number)
{
	struct mode_tree_item			*top, *child, *mti;
	struct window_customize_itemdata	*item;
	struct key_binding			*bd;
	char					*title, *text, *tmp, *expanded;
	const char				*flag;
	uint64_t				 tag;

	tag = (1ULL << 62)|((uint64_t)number << 54)|1;

	xasprintf(&title, "Key Table - %s", kt->name);
	top = mode_tree_add(data->data, NULL, NULL, tag, title, NULL, 0);
	mode_tree_no_tag(top);
	free(title);

	ft = format_create_from_state(NULL, NULL, fs);
	format_add(ft, "is_option", "0");
	format_add(ft, "is_key", "1");

	bd = key_bindings_first(kt);
	while (bd != NULL) {
		format_add(ft, "key", "%s", key_string_lookup_key(bd->key, 0));
		if (bd->note != NULL)
			format_add(ft, "key_note", "%s", bd->note);
		if (filter != NULL) {
			expanded = format_expand(ft, filter);
			if (!format_true(expanded)) {
				free(expanded);
				continue;
			}
			free(expanded);
		}

		item = window_customize_add_item(data);
		item->scope = WINDOW_CUSTOMIZE_KEY;
		item->table = xstrdup(kt->name);
		item->key = bd->key;
		item->name = xstrdup(key_string_lookup_key(item->key, 0));
		item->idx = -1;

		expanded = format_expand(ft, data->format);
		child = mode_tree_add(data->data, top, item, (uint64_t)bd,
		    expanded, NULL, 0);
		free(expanded);

		tmp = cmd_list_print(bd->cmdlist, 0);
		xasprintf(&text, "#[ignore]%s", tmp);
		free(tmp);
		mti = mode_tree_add(data->data, child, item,
		    tag|(bd->key << 3)|(0 << 1)|1, "Command", text, -1);
		mode_tree_draw_as_parent(mti);
		mode_tree_no_tag(mti);
		free(text);

		if (bd->note != NULL)
			xasprintf(&text, "#[ignore]%s", bd->note);
		else
			text = xstrdup("");
		mti = mode_tree_add(data->data, child, item,
		    tag|(bd->key << 3)|(1 << 1)|1, "Note", text, -1);
		mode_tree_draw_as_parent(mti);
		mode_tree_no_tag(mti);
		free(text);

		if (bd->flags & KEY_BINDING_REPEAT)
			flag = "on";
		else
			flag = "off";
		mti = mode_tree_add(data->data, child, item,
		    tag|(bd->key << 3)|(2 << 1)|1, "Repeat", flag, -1);
		mode_tree_draw_as_parent(mti);
		mode_tree_no_tag(mti);

		bd = key_bindings_next(kt, bd);
	}

	format_free(ft);
}

static void
window_customize_build(void *modedata,
    __unused struct mode_tree_sort_criteria *sort_crit, __unused uint64_t *tag,
    const char *filter)
{
	struct window_customize_modedata	*data = modedata;
	struct cmd_find_state			 fs;
	struct format_tree			*ft;
	u_int					 i;
	struct key_table			*kt;

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

	if (cmd_find_valid_state(&data->fs))
		cmd_find_copy_state(&fs, &data->fs);
	else
		cmd_find_from_pane(&fs, data->wp, 0);

	ft = format_create_from_state(NULL, NULL, &fs);
	format_add(ft, "is_option", "1");
	format_add(ft, "is_key", "0");

	window_customize_build_options(data, "Server Options",
	    (3ULL << 62)|(OPTIONS_TABLE_SERVER << 1)|1,
	    WINDOW_CUSTOMIZE_SERVER, global_options,
	    WINDOW_CUSTOMIZE_NONE, NULL,
	    WINDOW_CUSTOMIZE_NONE, NULL,
	    ft, filter, &fs);
	window_customize_build_options(data, "Session Options",
	    (3ULL << 62)|(OPTIONS_TABLE_SESSION << 1)|1,
	    WINDOW_CUSTOMIZE_GLOBAL_SESSION, global_s_options,
	    WINDOW_CUSTOMIZE_SESSION, fs.s->options,
	    WINDOW_CUSTOMIZE_NONE, NULL,
	    ft, filter, &fs);
	window_customize_build_options(data, "Window & Pane Options",
	    (3ULL << 62)|(OPTIONS_TABLE_WINDOW << 1)|1,
	    WINDOW_CUSTOMIZE_GLOBAL_WINDOW, global_w_options,
	    WINDOW_CUSTOMIZE_WINDOW, fs.w->options,
	    WINDOW_CUSTOMIZE_PANE, fs.wp->options,
	    ft, filter, &fs);

	format_free(ft);
	ft = format_create_from_state(NULL, NULL, &fs);

	i = 0;
	kt = key_bindings_first_table();
	while (kt != NULL) {
		if (!RB_EMPTY(&kt->key_bindings)) {
			window_customize_build_keys(data, kt, ft, filter, &fs,
			    i);
			if (++i == 256)
				break;
		}
		kt = key_bindings_next_table(kt);
	}

	format_free(ft);
}

static void
window_customize_draw_key(__unused struct window_customize_modedata *data,
    struct window_customize_itemdata *item, struct screen_write_ctx *ctx,
    u_int sx, u_int sy)
{
	struct screen		*s = ctx->s;
	u_int			 cx = s->cx, cy = s->cy;
	struct key_table	*kt;
	struct key_binding	*bd, *default_bd;
	const char		*note, *period = "";
	char			*cmd, *default_cmd;

	if (item == NULL || !window_customize_get_key(item, &kt, &bd))
		return;

	note = bd->note;
	if (note == NULL)
		note = "There is no note for this key.";
	if (*note != '\0' && note[strlen (note) - 1] != '.')
		period = ".";
	if (!screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, "%s%s",
	    note, period))
		return;
	screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */
	if (s->cy >= cy + sy - 1)
		return;

	if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
	    &grid_default_cell, "This key is in the %s table.", kt->name))
		return;
	if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
	    &grid_default_cell, "This key %s repeat.",
	    (bd->flags & KEY_BINDING_REPEAT) ? "does" : "does not"))
		return;
	screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */
	if (s->cy >= cy + sy - 1)
		return;

	cmd = cmd_list_print(bd->cmdlist, 0);
	if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
	    &grid_default_cell, "Command: %s", cmd)) {
		free(cmd);
		return;
	}
	default_bd = key_bindings_get_default(kt, bd->key);
	if (default_bd != NULL) {
		default_cmd = cmd_list_print(default_bd->cmdlist, 0);
		if (strcmp(cmd, default_cmd) != 0 &&
		    !screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
		    &grid_default_cell, "The default is: %s", default_cmd)) {
			free(default_cmd);
			free(cmd);
			return;
		}
		free(default_cmd);
	}
	free(cmd);
}

static void
window_customize_draw_option(struct window_customize_modedata *data,
    struct window_customize_itemdata *item, struct screen_write_ctx *ctx,
    u_int sx, u_int sy)
{
	struct screen				 *s = ctx->s;
	u_int					  cx = s->cx, cy = s->cy;
	int					  idx;
	struct options_entry			 *o, *parent;
	struct options				 *go, *wo;
	const struct options_table_entry	 *oe;
	struct grid_cell			  gc;
	const char				**choice, *text, *name;
	const char				 *space = "", *unit = "";
	char					 *value = NULL, *expanded;
	char					 *default_value = NULL;
	char					  choices[256] = "";
	struct cmd_find_state			  fs;
	struct format_tree			 *ft;

	if (!window_customize_check_item(data, item, &fs))
		return;
	name = item->name;
	idx = item->idx;

	o = options_get(item->oo, name);
	if (o == NULL)
		return;
	oe = options_table_entry(o);

	if (oe != NULL && oe->unit != NULL) {
		space = " ";
		unit = oe->unit;
	}
	ft = format_create_from_state(NULL, NULL, &fs);

	if (oe == NULL)
		text = "This is a user option.";
	else if (oe->text == NULL)
		text = "This option doesn't have a description.";
	else
		text = oe->text;
	if (!screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, "%s",
	    text))
		goto out;
	screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */
	if (s->cy >= cy + sy - 1)
		goto out;

	if (oe == NULL)
		text = "user";
	else if ((oe->scope & (OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE)) ==
	    (OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE))
		text = "window and pane";
	else if (oe->scope & OPTIONS_TABLE_WINDOW)
		text = "window";
	else if (oe->scope & OPTIONS_TABLE_SESSION)
		text = "session";
	else
		text = "server";
	if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
	    &grid_default_cell, "This is a %s option.", text))
		goto out;
	if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) {
		if (idx != -1) {
			if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy),
			    0, &grid_default_cell,
			    "This is an array option, index %u.", idx))
				goto out;
		} else {
			if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy),
			    0, &grid_default_cell, "This is an array option."))
				goto out;
		}
		if (idx == -1)
			goto out;
	}
	screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */
	if (s->cy >= cy + sy - 1)
		goto out;

	value = options_to_string(o, idx, 0);
	if (oe != NULL && idx == -1) {
		default_value = options_default_to_string(oe);
		if (strcmp(default_value, value) == 0) {
			free(default_value);
			default_value = NULL;
		}
	}
	if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
	    &grid_default_cell, "Option value: %s%s%s", value, space, unit))
		goto out;
	if (oe == NULL || oe->type == OPTIONS_TABLE_STRING) {
		expanded = format_expand(ft, value);
		if (strcmp(expanded, value) != 0) {
			if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy),
			    0, &grid_default_cell, "This expands to: %s",
			    expanded))
				goto out;
		}
		free(expanded);
	}
	if (oe != NULL && oe->type == OPTIONS_TABLE_CHOICE) {
		for (choice = oe->choices; *choice != NULL; choice++) {
			strlcat(choices, *choice, sizeof choices);
			strlcat(choices, ", ", sizeof choices);
		}
		choices[strlen(choices) - 2] = '\0';
		if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
		    &grid_default_cell, "Available values are: %s",
		    choices))
			goto out;
	}
	if (oe != NULL && oe->type == OPTIONS_TABLE_COLOUR) {
		if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 1,
		    &grid_default_cell, "This is a colour option: "))
			goto out;
		memcpy(&gc, &grid_default_cell, sizeof gc);
		gc.fg = options_get_number(item->oo, name);
		if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &gc,
		    "EXAMPLE"))
			goto out;
	}
	if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_STYLE)) {
		if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 1,
		    &grid_default_cell, "This is a style option: "))
			goto out;
		style_apply(&gc, item->oo, name, ft);
		if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &gc,
		    "EXAMPLE"))
			goto out;
	}
	if (default_value != NULL) {
		if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
		    &grid_default_cell, "The default is: %s%s%s", default_value,
		    space, unit))
			goto out;
	}

	screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */
	if (s->cy > cy + sy - 1)
		goto out;
	if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) {
		wo = NULL;
		go = NULL;
	} else {
		switch (item->scope) {
		case WINDOW_CUSTOMIZE_PANE:
			wo = options_get_parent(item->oo);
			go = options_get_parent(wo);
			break;
		case WINDOW_CUSTOMIZE_WINDOW:
		case WINDOW_CUSTOMIZE_SESSION:
			wo = NULL;
			go = options_get_parent(item->oo);
			break;
		default:
			wo = NULL;
			go = NULL;
			break;
		}
	}
	if (wo != NULL && options_owner(o) != wo) {
		parent = options_get_only(wo, name);
		if (parent != NULL) {
			value = options_to_string(parent, -1 , 0);
			if (!screen_write_text(ctx, s->cx, sx,
			    sy - (s->cy - cy), 0, &grid_default_cell,
			    "Window value (from window %u): %s%s%s", fs.wl->idx,
			    value, space, unit))
				goto out;
		}
	}
	if (go != NULL && options_owner(o) != go) {
		parent = options_get_only(go, name);
		if (parent != NULL) {
			value = options_to_string(parent, -1 , 0);
			if (!screen_write_text(ctx, s->cx, sx,
			    sy - (s->cy - cy), 0, &grid_default_cell,
			    "Global value: %s%s%s", value, space, unit))
				goto out;
		}
	}

out:
	free(value);
	free(default_value);
	format_free(ft);
}

static void
window_customize_draw(void *modedata, void *itemdata,
    struct screen_write_ctx *ctx, u_int sx, u_int sy)
{
	struct window_customize_modedata	*data = modedata;
	struct window_customize_itemdata	*item = itemdata;

	if (item == NULL)
		return;

	if (item->scope == WINDOW_CUSTOMIZE_KEY)
		window_customize_draw_key(data, item, ctx, sx, sy);
	else
		window_customize_draw_option(data, item, ctx, sx, sy);
}

static void
window_customize_menu(void *modedata, struct client *c, key_code key)
{
	struct window_customize_modedata	*data = modedata;
	struct window_pane			*wp = data->wp;
	struct window_mode_entry		*wme;

	wme = TAILQ_FIRST(&wp->modes);
	if (wme == NULL || wme->data != modedata)
		return;
	window_customize_key(wme, c, NULL, NULL, key, NULL);
}

static u_int
window_customize_height(__unused void *modedata, __unused u_int height)
{
	return (12);
}

static struct screen *
window_customize_init(struct window_mode_entry *wme, struct cmd_find_state *fs,
    struct args *args)
{
	struct window_pane			*wp = wme->wp;
	struct window_customize_modedata	*data;
	struct screen				*s;

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

	memcpy(&data->fs, fs, sizeof data->fs);

	if (args == NULL || !args_has(args, 'F'))
		data->format = xstrdup(WINDOW_CUSTOMIZE_DEFAULT_FORMAT);
	else
		data->format = xstrdup(args_get(args, 'F'));

	data->data = mode_tree_start(wp, args, window_customize_build,
	    window_customize_draw, NULL, window_customize_menu,
	    window_customize_height, NULL, data, window_customize_menu_items,
	    NULL, 0, &s);
	mode_tree_zoom(data->data, args);

	mode_tree_build(data->data);
	mode_tree_draw(data->data);

	return (s);
}

static void
window_customize_destroy(struct window_customize_modedata *data)
{
	u_int	i;

	if (--data->references != 0)
		return;

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

	free(data->format);

	free(data);
}

static void
window_customize_free(struct window_mode_entry *wme)
{
	struct window_customize_modedata *data = wme->data;

	if (data == NULL)
		return;

	data->dead = 1;
	mode_tree_free(data->data);
	window_customize_destroy(data);
}

static void
window_customize_resize(struct window_mode_entry *wme, u_int sx, u_int sy)
{
	struct window_customize_modedata	*data = wme->data;

	mode_tree_resize(data->data, sx, sy);
}

static void
window_customize_free_callback(void *modedata)
{
	window_customize_destroy(modedata);
}

static void
window_customize_free_item_callback(void *itemdata)
{
	struct window_customize_itemdata	*item = itemdata;
	struct window_customize_modedata	*data = item->data;

	window_customize_free_item(item);
	window_customize_destroy(data);
}

static int
window_customize_set_option_callback(struct client *c, void *itemdata,
    const char *s, __unused int done)
{
	struct window_customize_itemdata	*item = itemdata;
	struct window_customize_modedata	*data = item->data;
	struct options_entry			*o;
	const struct options_table_entry	*oe;
	struct options				*oo = item->oo;
	const char				*name = item->name;
	char					*cause;
	int					 idx = item->idx;

	if (s == NULL || *s == '\0' || data->dead)
		return (0);
	if (item == NULL || !window_customize_check_item(data, item, NULL))
		return (0);
	o = options_get(oo, name);
	if (o == NULL)
		return (0);
	oe = options_table_entry(o);

	if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) {
		if (idx == -1) {
			for (idx = 0; idx < INT_MAX; idx++) {
				if (options_array_get(o, idx) == NULL)
					break;
			}
		}
		if (options_array_set(o, idx, s, 0, &cause) != 0)
			goto fail;
	} else {
		if (options_from_string(oo, oe, name, s, 0, &cause) != 0)
			goto fail;
	}

	options_push_changes(item->name);
	mode_tree_build(data->data);
	mode_tree_draw(data->data);
	data->wp->flags |= PANE_REDRAW;

	return (0);

fail:
	*cause = toupper((u_char)*cause);
	status_message_set(c, -1, 1, 0, "%s", cause);
	free(cause);
	return (0);
}

static void
window_customize_set_option(struct client *c,
    struct window_customize_modedata *data,
    struct window_customize_itemdata *item, int global, int pane)
{
	struct options_entry			*o;
	const struct options_table_entry	*oe;
	struct options				*oo;
	struct window_customize_itemdata	*new_item;
	int					 flag, idx = item->idx;
	enum window_customize_scope		 scope = WINDOW_CUSTOMIZE_NONE;
	u_int					 choice;
	const char				*name = item->name, *space = "";
	char					*prompt, *value, *text;
	struct cmd_find_state			 fs;

	if (item == NULL || !window_customize_check_item(data, item, &fs))
		return;
	o = options_get(item->oo, name);
	if (o == NULL)
		return;

	oe = options_table_entry(o);
	if (oe != NULL && ~oe->scope & OPTIONS_TABLE_PANE)
		pane = 0;
	if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) {
		scope = item->scope;
		oo = item->oo;
	} else {
		if (global) {
			switch (item->scope) {
			case WINDOW_CUSTOMIZE_NONE:
			case WINDOW_CUSTOMIZE_KEY:
			case WINDOW_CUSTOMIZE_SERVER:
			case WINDOW_CUSTOMIZE_GLOBAL_SESSION:
			case WINDOW_CUSTOMIZE_GLOBAL_WINDOW:
				scope = item->scope;
				break;
			case WINDOW_CUSTOMIZE_SESSION:
				scope = WINDOW_CUSTOMIZE_GLOBAL_SESSION;
				break;
			case WINDOW_CUSTOMIZE_WINDOW:
			case WINDOW_CUSTOMIZE_PANE:
				scope = WINDOW_CUSTOMIZE_GLOBAL_WINDOW;
				break;
			}
		} else {
			switch (item->scope) {
			case WINDOW_CUSTOMIZE_NONE:
			case WINDOW_CUSTOMIZE_KEY:
			case WINDOW_CUSTOMIZE_SERVER:
			case WINDOW_CUSTOMIZE_SESSION:
				scope = item->scope;
				break;
			case WINDOW_CUSTOMIZE_WINDOW:
			case WINDOW_CUSTOMIZE_PANE:
				if (pane)
					scope = WINDOW_CUSTOMIZE_PANE;
				else
					scope = WINDOW_CUSTOMIZE_WINDOW;
				break;
			case WINDOW_CUSTOMIZE_GLOBAL_SESSION:
				scope = WINDOW_CUSTOMIZE_SESSION;
				break;
			case WINDOW_CUSTOMIZE_GLOBAL_WINDOW:
				if (pane)
					scope = WINDOW_CUSTOMIZE_PANE;
				else
					scope = WINDOW_CUSTOMIZE_WINDOW;
				break;
			}
		}
		if (scope == item->scope)
			oo = item->oo;
		else
			oo = window_customize_get_tree(scope, &fs);
	}

	if (oe != NULL && oe->type == OPTIONS_TABLE_FLAG) {
		flag = options_get_number(oo, name);
		options_set_number(oo, name, !flag);
	} else if (oe != NULL && oe->type == OPTIONS_TABLE_CHOICE) {
		choice = options_get_number(oo, name);
		if (oe->choices[choice + 1] == NULL)
			choice = 0;
		else
			choice++;
		options_set_number(oo, name, choice);
	} else {
		text = window_customize_scope_text(scope, &fs);
		if (*text != '\0')
			space = ", for ";
		else if (scope != WINDOW_CUSTOMIZE_SERVER)
			space = ", global";
		if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) {
			if (idx == -1) {
				xasprintf(&prompt, "(%s[+]%s%s) ", name, space,
				    text);
			} else {
				xasprintf(&prompt, "(%s[%d]%s%s) ", name, idx,
				    space, text);
			}
		} else
			xasprintf(&prompt, "(%s%s%s) ", name, space, text);
		free(text);

		value = options_to_string(o, idx, 0);

		new_item = xcalloc(1, sizeof *new_item);
		new_item->data = data;
		new_item->scope = scope;
		new_item->oo = oo;
		new_item->name = xstrdup(name);
		new_item->idx = idx;

		data->references++;
		status_prompt_set(c, NULL, prompt, value,
		    window_customize_set_option_callback,
		    window_customize_free_item_callback, new_item,
		    PROMPT_NOFORMAT);

		free(prompt);
		free(value);
	}
}

static void
window_customize_unset_option(struct window_customize_modedata *data,
    struct window_customize_itemdata *item)
{
	struct options_entry	*o;

	if (item == NULL || !window_customize_check_item(data, item, NULL))
		return;

	o = options_get(item->oo, item->name);
	if (o == NULL)
		return;
	if (item->idx != -1 && item == mode_tree_get_current(data->data))
		mode_tree_up(data->data, 0);
	options_remove_or_default(o, item->idx, NULL);
}

static void
window_customize_reset_option(struct window_customize_modedata *data,
    struct window_customize_itemdata *item)
{
	struct options		*oo;
	struct options_entry	*o;

	if (item == NULL || !window_customize_check_item(data, item, NULL))
		return;
	if (item->idx != -1)
		return;

	oo = item->oo;
	while (oo != NULL) {
		o = options_get_only(item->oo, item->name);
		if (o != NULL)
			options_remove_or_default(o, -1, NULL);
		oo = options_get_parent(oo);
	}
}

static int
window_customize_set_command_callback(struct client *c, void *itemdata,
    const char *s, __unused int done)
{
	struct window_customize_itemdata	*item = itemdata;
	struct window_customize_modedata	*data = item->data;
	struct key_binding			*bd;
	struct cmd_parse_result			*pr;
	char					*error;

	if (s == NULL || *s == '\0' || data->dead)
		return (0);
	if (item == NULL || !window_customize_get_key(item, NULL, &bd))
		return (0);

	pr = cmd_parse_from_string(s, NULL);
	switch (pr->status) {
	case CMD_PARSE_EMPTY:
		error = xstrdup("empty command");
		goto fail;
	case CMD_PARSE_ERROR:
		error = pr->error;
		goto fail;
	case CMD_PARSE_SUCCESS:
		break;
	}
	cmd_list_free(bd->cmdlist);
	bd->cmdlist = pr->cmdlist;

	mode_tree_build(data->data);
	mode_tree_draw(data->data);
	data->wp->flags |= PANE_REDRAW;

	return (0);

fail:
	*error = toupper((u_char)*error);
	status_message_set(c, -1, 1, 0, "%s", error);
	free(error);
	return (0);
}

static int
window_customize_set_note_callback(__unused struct client *c, void *itemdata,
    const char *s, __unused int done)
{
	struct window_customize_itemdata	*item = itemdata;
	struct window_customize_modedata	*data = item->data;
	struct key_binding			*bd;

	if (s == NULL || *s == '\0' || data->dead)
		return (0);
	if (item == NULL || !window_customize_get_key(item, NULL, &bd))
		return (0);

	free((void *)bd->note);
	bd->note = xstrdup(s);

	mode_tree_build(data->data);
	mode_tree_draw(data->data);
	data->wp->flags |= PANE_REDRAW;

	return (0);
}

static void
window_customize_set_key(struct client *c,
    struct window_customize_modedata *data,
    struct window_customize_itemdata *item)
{
	key_code				 key = item->key;
	struct key_binding			*bd;
	const char				*s;
	char					*prompt, *value;
	struct window_customize_itemdata	*new_item;

	if (item == NULL || !window_customize_get_key(item, NULL, &bd))
		return;

	s = mode_tree_get_current_name(data->data);
	if (strcmp(s, "Repeat") == 0)
		bd->flags ^= KEY_BINDING_REPEAT;
	else if (strcmp(s, "Command") == 0) {
		xasprintf(&prompt, "(%s) ", key_string_lookup_key(key, 0));
		value = cmd_list_print(bd->cmdlist, 0);

		new_item = xcalloc(1, sizeof *new_item);
		new_item->data = data;
		new_item->scope = item->scope;
		new_item->table = xstrdup(item->table);
		new_item->key = key;

		data->references++;
		status_prompt_set(c, NULL, prompt, value,
		    window_customize_set_command_callback,
		    window_customize_free_item_callback, new_item,
		    PROMPT_NOFORMAT);
		free(prompt);
		free(value);
	} else if (strcmp(s, "Note") == 0) {
		xasprintf(&prompt, "(%s) ", key_string_lookup_key(key, 0));

		new_item = xcalloc(1, sizeof *new_item);
		new_item->data = data;
		new_item->scope = item->scope;
		new_item->table = xstrdup(item->table);
		new_item->key = key;

		data->references++;
		status_prompt_set(c, NULL, prompt,
		    (bd->note == NULL ? "" : bd->note),
		    window_customize_set_note_callback,
		    window_customize_free_item_callback, new_item,
		    PROMPT_NOFORMAT);
		free(prompt);
	}
}

static void
window_customize_unset_key(struct window_customize_modedata *data,
    struct window_customize_itemdata *item)
{
	struct key_table	*kt;
	struct key_binding	*bd;

	if (item == NULL || !window_customize_get_key(item, &kt, &bd))
		return;

	if (item == mode_tree_get_current(data->data)) {
		mode_tree_collapse_current(data->data);
		mode_tree_up(data->data, 0);
	}
	key_bindings_remove(kt->name, bd->key);
}

static void
window_customize_reset_key(struct window_customize_modedata *data,
    struct window_customize_itemdata *item)
{
	struct key_table	*kt;
	struct key_binding	*dd, *bd;

	if (item == NULL || !window_customize_get_key(item, &kt, &bd))
		return;

	dd = key_bindings_get_default(kt, bd->key);
	if (dd != NULL && bd->cmdlist == dd->cmdlist)
		return;
	if (dd == NULL && item == mode_tree_get_current(data->data)) {
		mode_tree_collapse_current(data->data);
		mode_tree_up(data->data, 0);
	}
	key_bindings_reset(kt->name, bd->key);
}

static void
window_customize_change_each(void *modedata, void *itemdata,
    __unused struct client *c, __unused key_code key)
{
	struct window_customize_modedata	*data = modedata;
	struct window_customize_itemdata	*item = itemdata;

	switch (data->change) {
	case WINDOW_CUSTOMIZE_UNSET:
		if (item->scope == WINDOW_CUSTOMIZE_KEY)
			window_customize_unset_key(data, item);
		else
			window_customize_unset_option(data, item);
		break;
	case WINDOW_CUSTOMIZE_RESET:
		if (item->scope == WINDOW_CUSTOMIZE_KEY)
			window_customize_reset_key(data, item);
		else
			window_customize_reset_option(data, item);
		break;
	}
	if (item->scope != WINDOW_CUSTOMIZE_KEY)
		options_push_changes(item->name);
}

static int
window_customize_change_current_callback(__unused struct client *c,
    void *modedata, const char *s, __unused int done)
{
	struct window_customize_modedata	*data = modedata;
	struct window_customize_itemdata	*item;

	if (s == NULL || *s == '\0' || data->dead)
		return (0);
	if (tolower((u_char) s[0]) != 'y' || s[1] != '\0')
		return (0);

	item = mode_tree_get_current(data->data);
	switch (data->change) {
	case WINDOW_CUSTOMIZE_UNSET:
		if (item->scope == WINDOW_CUSTOMIZE_KEY)
			window_customize_unset_key(data, item);
		else
			window_customize_unset_option(data, item);
		break;
	case WINDOW_CUSTOMIZE_RESET:
		if (item->scope == WINDOW_CUSTOMIZE_KEY)
			window_customize_reset_key(data, item);
		else
			window_customize_reset_option(data, item);
		break;
	}
	if (item->scope != WINDOW_CUSTOMIZE_KEY)
		options_push_changes(item->name);
	mode_tree_build(data->data);
	mode_tree_draw(data->data);
	data->wp->flags |= PANE_REDRAW;

	return (0);
}

static int
window_customize_change_tagged_callback(struct client *c, void *modedata,
    const char *s, __unused int done)
{
	struct window_customize_modedata	*data = modedata;

	if (s == NULL || *s == '\0' || data->dead)
		return (0);
	if (tolower((u_char) s[0]) != 'y' || s[1] != '\0')
		return (0);

	mode_tree_each_tagged(data->data, window_customize_change_each, c,
	    KEYC_NONE, 0);
	mode_tree_build(data->data);
	mode_tree_draw(data->data);
	data->wp->flags |= PANE_REDRAW;

	return (0);
}

static void
window_customize_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_customize_modedata	*data = wme->data;
	struct window_customize_itemdata	*item, *new_item;
	int					 finished, idx;
	char					*prompt;
	u_int					 tagged;

	item = mode_tree_get_current(data->data);
	finished = mode_tree_key(data->data, c, &key, m, NULL, NULL);
	if (item != (new_item = mode_tree_get_current(data->data)))
		item = new_item;

	switch (key) {
	case '\r':
	case 's':
		if (item == NULL)
			break;
		if (item->scope == WINDOW_CUSTOMIZE_KEY)
			window_customize_set_key(c, data, item);
		else {
			window_customize_set_option(c, data, item, 0, 1);
			options_push_changes(item->name);
		}
		mode_tree_build(data->data);
		break;
	case 'w':
		if (item == NULL || item->scope == WINDOW_CUSTOMIZE_KEY)
			break;
		window_customize_set_option(c, data, item, 0, 0);
		options_push_changes(item->name);
		mode_tree_build(data->data);
		break;
	case 'S':
	case 'W':
		if (item == NULL || item->scope == WINDOW_CUSTOMIZE_KEY)
			break;
		window_customize_set_option(c, data, item, 1, 0);
		options_push_changes(item->name);
		mode_tree_build(data->data);
		break;
	case 'd':
		if (item == NULL || item->idx != -1)
			break;
		xasprintf(&prompt, "Reset %s to default? ", item->name);
		data->references++;
		data->change = WINDOW_CUSTOMIZE_RESET;
		status_prompt_set(c, NULL, prompt, "",
		    window_customize_change_current_callback,
		    window_customize_free_callback, data,
		    PROMPT_SINGLE|PROMPT_NOFORMAT);
		free(prompt);
		break;
	case 'D':
		tagged = mode_tree_count_tagged(data->data);
		if (tagged == 0)
			break;
		xasprintf(&prompt, "Reset %u tagged to default? ", tagged);
		data->references++;
		data->change = WINDOW_CUSTOMIZE_RESET;
		status_prompt_set(c, NULL, prompt, "",
		    window_customize_change_tagged_callback,
		    window_customize_free_callback, data,
		    PROMPT_SINGLE|PROMPT_NOFORMAT);
		free(prompt);
		break;
	case 'u':
		if (item == NULL)
			break;
		idx = item->idx;
		if (idx != -1)
			xasprintf(&prompt, "Unset %s[%d]? ", item->name, idx);
		else
			xasprintf(&prompt, "Unset %s? ", item->name);
		data->references++;
		data->change = WINDOW_CUSTOMIZE_UNSET;
		status_prompt_set(c, NULL, prompt, "",
		    window_customize_change_current_callback,
		    window_customize_free_callback, data,
		    PROMPT_SINGLE|PROMPT_NOFORMAT);
		free(prompt);
		break;
	case 'U':
		tagged = mode_tree_count_tagged(data->data);
		if (tagged == 0)
			break;
		xasprintf(&prompt, "Unset %u tagged? ", tagged);
		data->references++;
		data->change = WINDOW_CUSTOMIZE_UNSET;
		status_prompt_set(c, NULL, prompt, "",
		    window_customize_change_tagged_callback,
		    window_customize_free_callback, data,
		    PROMPT_SINGLE|PROMPT_NOFORMAT);
		free(prompt);
		break;
	case 'H':
		data->hide_global = !data->hide_global;
		mode_tree_build(data->data);
		break;
	}
	if (finished)
		window_pane_reset_mode(wp);
	else {
		mode_tree_draw(data->data);
		wp->flags |= PANE_REDRAW;
	}
}
