/*
 * Copyright 2008-2013 Various Authors
 * Copyright 2006 <ft@bewatermyfriend.org>
 *
 * heavily based on filters.c
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

#include "help.h"
#include "window.h"
#include "search.h"
#include "misc.h"
#include "xmalloc.h"
#include "keys.h"
#include "command_mode.h"
#include "ui_curses.h"
#include "options.h"
#include "cmdline.h"

#include <stdio.h>

struct window *help_win;
struct searchable *help_searchable;

static LIST_HEAD(help_head);
static struct list_head *bound_head;
static struct list_head *bound_tail;
static struct list_head *unbound_head;
static struct list_head *unbound_tail;

static inline void help_entry_to_iter(struct help_entry *e, struct iter *iter)
{
	iter->data0 = &help_head;
	iter->data1 = e;
	iter->data2 = NULL;
}

static GENERIC_ITER_PREV(help_get_prev, struct help_entry, node)
static GENERIC_ITER_NEXT(help_get_next, struct help_entry, node)

static int help_search_get_current(void *data, struct iter *iter, enum search_direction dir)
{
	return window_get_sel(help_win, iter);
}

static int help_search_matches(void *data, struct iter *iter, const char *text)
{
	int matched = 0;
	char **words = get_words(text);

	if (words[0] != NULL) {
		struct help_entry *ent;
		int i;

		ent = iter_to_help_entry(iter);
		for (i = 0; ; i++) {
			if (words[i] == NULL) {
				window_set_sel(help_win, iter);
				matched = 1;
				break;
			}
			if (ent->type == HE_TEXT) {
				if (!u_strcasestr(ent->text, words[i]))
					break;
			} else if (ent->type == HE_BOUND) {
				if (!u_strcasestr(ent->binding->cmd, words[i]) &&
					!u_strcasestr(ent->binding->key->name, words[i]))
					break;
			} else if (ent->type == HE_UNBOUND) {
				if (!u_strcasestr(ent->command->name, words[i]))
					break;
			} else if (ent->type == HE_OPTION) {
				if (!u_strcasestr(ent->option->name, words[i]))
					break;
			}
		}
	}
	free_str_array(words);
	return matched;
}

static const struct searchable_ops help_search_ops = {
	.get_prev = help_get_prev,
	.get_next = help_get_next,
	.get_current = help_search_get_current,
	.matches = help_search_matches
};

static void help_add_text(const char *s)
{
	struct help_entry *ent;
	ent = xnew(struct help_entry, 1);
	ent->type = HE_TEXT;
	ent->text = s;
	list_add_tail(&ent->node, &help_head);
}

static void help_add_defaults(void)
{
	struct cmus_opt *opt;

	help_add_text("Keybindings");
	help_add_text("-----------");
	bound_head = help_head.prev;
	help_add_text("");
	help_add_text("Unbound Commands");
	help_add_text("----------------");
	unbound_head = help_head.prev;
	help_add_text("");
	help_add_text("Options");
	help_add_text("-------");

	list_for_each_entry(opt, &option_head, node) {
		struct help_entry *ent = xnew(struct help_entry, 1);

		ent->type = HE_OPTION;
		ent->option = opt;
		list_add_tail(&ent->node, &help_head);
	}

	bound_tail = bound_head->next;
	unbound_tail = unbound_head->next;
}

void help_remove_unbound(struct command *cmd)
{
	struct help_entry *ent;
	struct iter i;
	list_for_each_entry(ent, &help_head, node) {
		if (ent->type != HE_UNBOUND)
			continue;
		if (ent->command == cmd) {
			help_entry_to_iter(ent, &i);
			window_row_vanishes(help_win, &i);
			list_del(&ent->node);
			free(ent);
			return;
		}
	}
}

static void list_add_sorted(struct list_head *new, struct list_head *head,
		struct list_head *tail,
		int (*cmp)(struct list_head *, struct list_head *))
{
	struct list_head *item = tail->prev;

	while (item != head) {
		if (cmp(new, item) >= 0)
			break;
		item = item->prev;
	}
	/* add after item */
	list_add(new, item);
}

static int bound_cmp(struct list_head *ai, struct list_head *bi)
{
	struct help_entry *a = container_of(ai, struct help_entry, node);
	struct help_entry *b = container_of(bi, struct help_entry, node);
	int ret = a->binding->ctx - b->binding->ctx;

	if (!ret)
		ret = strcmp(a->binding->key->name, b->binding->key->name);
	return ret;
}

static int unbound_cmp(struct list_head *ai, struct list_head *bi)
{
	struct help_entry *a = container_of(ai, struct help_entry, node);
	struct help_entry *b = container_of(bi, struct help_entry, node);

	return strcmp(a->command->name, b->command->name);
}

void help_add_unbound(struct command *cmd)
{
	struct help_entry *ent;

	ent = xnew(struct help_entry, 1);
	ent->type = HE_UNBOUND;
	ent->command = cmd;
	list_add_sorted(&ent->node, unbound_head, unbound_tail, unbound_cmp);
}

void help_add_all_unbound(void)
{
	int i;
	for (i = 0; commands[i].name; ++i)
		if (!commands[i].bc)
			help_add_unbound(&commands[i]);
}

void help_select(void)
{
	struct iter sel;
	struct help_entry *ent;
	char buf[OPTION_MAX_SIZE];

	if (!window_get_sel(help_win, &sel))
		return;

	ent = iter_to_help_entry(&sel);
	switch (ent->type) {
	case HE_BOUND:
		snprintf(buf, sizeof(buf), "bind -f %s %s %s",
				key_context_names[ent->binding->ctx],
				ent->binding->key->name,
				ent->binding->cmd);
		cmdline_set_text(buf);
		enter_command_mode();
		break;
	case HE_UNBOUND:
		snprintf(buf, sizeof(buf), "bind common <key> %s",
				ent->command->name);
		cmdline_set_text(buf);
		enter_command_mode();
		break;
	case HE_OPTION:
		snprintf(buf, sizeof(buf), "set %s=", ent->option->name);
		size_t len = strlen(buf);
		ent->option->get(ent->option->data, buf + len, sizeof(buf) - len);
		cmdline_set_text(buf);
		enter_command_mode();
		break;
	default:
		break;
	}
}

void help_toggle(void)
{
	struct iter sel;
	struct help_entry *ent;

	if (!window_get_sel(help_win, &sel))
		return;

	ent = iter_to_help_entry(&sel);
	switch (ent->type) {
	case HE_OPTION:
		if (ent->option->toggle) {
			ent->option->toggle(ent->option->data);
			help_win->changed = 1;
		}
		break;
	default:
		break;
	}
}

void help_remove(void)
{
	struct iter sel;
	struct help_entry *ent;

	if (!window_get_sel(help_win, &sel))
		return;

	ent = iter_to_help_entry(&sel);
	switch (ent->type) {
	case HE_BOUND:
		if (yes_no_query("Remove selected binding? [y/N]") == UI_QUERY_ANSWER_YES)
			key_unbind(key_context_names[ent->binding->ctx],
					ent->binding->key->name, 0);
		break;
	default:
		break;
	}
}

void help_add_bound(const struct binding *bind)
{
	struct help_entry *ent;
	ent = xnew(struct help_entry, 1);
	ent->type = HE_BOUND;
	ent->binding = bind;
	list_add_sorted(&ent->node, bound_head, bound_tail, bound_cmp);
}

void help_remove_bound(const struct binding *bind)
{
	struct help_entry *ent;
	struct iter i;
	list_for_each_entry(ent, &help_head, node) {
		if (ent->binding == bind) {
			help_entry_to_iter(ent, &i);
			window_row_vanishes(help_win, &i);
			list_del(&ent->node);
			free(ent);
			return;
		}
	}
}

void help_init(void)
{
	struct iter iter;

	help_win = window_new(help_get_prev, help_get_next);
	window_set_contents(help_win, &help_head);
	window_changed(help_win);
	help_add_defaults();

	iter.data0 = &help_head;
	iter.data1 = NULL;
	iter.data2 = NULL;
	help_searchable = searchable_new(NULL, &iter, &help_search_ops);
}

void help_exit(void)
{
	searchable_free(help_searchable);
	window_free(help_win);
}
