#include <assert.h>
#include <stdlib.h>
#include "newsraft.h"

// Note to the future.
// TODO: explain why we need all these *_unprotected functions

size_t list_menu_height;
size_t list_menu_width;

static WINDOW **windows = NULL;
static size_t windows_count = 0;
static size_t scrolloff;
static size_t horizontal_shift = 0;
static struct wstring *list_fmtout = NULL;
static uint64_t menu_age = 0;

static struct menu_state *menus = NULL;
static struct menu_state *menu  = NULL;

bool
is_current_menu_a_pager(void)
{
	return menu != NULL && menu->enumerator == &is_pager_pos_valid ? true : false;
}

bool
adjust_list_menu(void)
{
	INFO("Adjusting list menu.");
	for (size_t i = 0; i < windows_count; ++i) {
		delwin(windows[i]);
	}
	windows = newsraft_realloc(windows, sizeof(WINDOW *) * (list_menu_height + 1));
	for (size_t i = 0; i < list_menu_height; ++i) {
		windows[i] = newwin(i);
	}
	windows_count = list_menu_height;
	scrolloff = get_cfg_uint(NULL, CFG_SCROLLOFF);
	if (scrolloff > (list_menu_height / 2)) {
		scrolloff = list_menu_height / 2;
	}
	wstr_set(&list_fmtout, NULL, 0, 200);
	return true;
}

void
free_list_menu(void)
{
	for (size_t i = 0; i < windows_count; ++i) {
		delwin(windows[i]);
	}
	newsraft_free(windows);
	free_wstring(list_fmtout);
}

void
list_menu_writer(size_t index, WINDOW *w)
{
	if (menu->enumerator(menu, index) == true) {
		struct format_arg args[100];
		do_format(list_fmtout, menu->entry_format->ptr, menu->get_args(menu, index, args, LENGTH(args)));
		if (list_fmtout->len > horizontal_shift) {
			waddwstr(w, list_fmtout->ptr + horizontal_shift);
		}
		wbkgd(w, menu->paint_action(menu, index, index == menu->view_sel));
	}
}

static inline void
expose_entry_of_the_list_menu_unprotected(size_t index)
{
	if (index < menu->view_min || (index - menu->view_min) >= windows_count) {
		WARN("Ignoring list entry expose because of zero size.");
		return;
	}
	WINDOW *w = windows[index - menu->view_min];
	werase(w);
	wmove(w, 0);
	wbkgd(w, (struct config_color){TB_DEFAULT, TB_DEFAULT, TB_DEFAULT});
	wattrset(w, TB_DEFAULT);
	menu->printer(index, w);
}

void
expose_entry_of_the_list_menu(size_t index)
{
	pthread_mutex_lock(&interface_lock);
	if (index >= menu->view_min && index <= menu->view_max) {
		expose_entry_of_the_list_menu_unprotected(index);
		tb_present();
	}
	pthread_mutex_unlock(&interface_lock);
}

void
expose_all_visible_entries_of_the_list_menu_unprotected(void)
{
	for (size_t i = menu->view_min; i <= menu->view_max; ++i) {
		expose_entry_of_the_list_menu_unprotected(i);
	}
	tb_present();
	update_status_window_content_unprotected();
}

void
expose_all_visible_entries_of_the_list_menu(void)
{
	pthread_mutex_lock(&interface_lock);
	expose_all_visible_entries_of_the_list_menu_unprotected();
	pthread_mutex_unlock(&interface_lock);
}

void
redraw_list_menu_unprotected(void)
{
	if (list_menu_height < 1) {
		WARN("Ignoring list view redraw because of zero size.");
		return;
	}
	const size_t limit = menu->view_sel > scrolloff ? menu->view_sel - scrolloff : 0;
	if (menu->view_min > limit) {
		menu->view_min = limit;
	}
	menu->view_max = menu->view_min + (list_menu_height - 1);
	if (menu->view_sel + scrolloff > menu->view_max) {
		menu->view_max = menu->view_sel + scrolloff;
	}
	while (menu->view_max >= list_menu_height && !menu->enumerator(menu, menu->view_max)) {
		menu->view_max -= 1;
	}
	menu->view_min = menu->view_max - (list_menu_height - 1);
	expose_all_visible_entries_of_the_list_menu_unprotected();
}

void
reset_list_menu_unprotected(void)
{
	if (menu->enumerator(menu, menu->view_sel) == false) {
		menu->view_sel = 0;
		menu->view_min = 0;
	}
	redraw_list_menu_unprotected();
}

static size_t
obtain_list_entries_count_unprotected(struct menu_state *m)
{
	size_t i = 0;
	while (m->enumerator(m, i) == true) {
		i += 1;
	}
	return i;
}

static void
change_list_view_unprotected(struct menu_state *m, size_t new_sel, bool is_wrappable)
{
	if (list_menu_height < 1) {
		WARN("Ignoring list view change because of zero size.");
		return;
	}
	if (is_wrappable && get_cfg_bool(NULL, CFG_SCROLLWRAP)) {
		if (new_sel > m->view_sel) {
			// Tried to go forward
			if (m->enumerator(m, m->view_sel) && !m->enumerator(m, m->view_sel + 1)) {
				new_sel = 0;
			}
		} else if (new_sel == 0 && m->view_sel == 0) {
			// Tried to go backward
			new_sel = obtain_list_entries_count_unprotected(m);
		}
	}
	while (m->enumerator(m, new_sel) == false && new_sel > 0) {
		new_sel -= 1;
	}
	if (m->enumerator(m, new_sel) == false) {
		return;
	}
	if (new_sel + scrolloff > m->view_max) {
		m->view_max = new_sel + scrolloff;
		while (m->view_max >= list_menu_height && m->enumerator(m, m->view_max) == false) {
			m->view_max -= 1;
		}
		m->view_min = m->view_max - (list_menu_height - 1);
		m->view_sel = new_sel;
		if (m == menu) {
			expose_all_visible_entries_of_the_list_menu_unprotected();
		}
	} else if (((new_sel >= scrolloff) && (new_sel - scrolloff < m->view_min))
		|| ((new_sel < scrolloff) && (m->view_min > 0)))
	{
		if (new_sel >= scrolloff) {
			m->view_min = new_sel - scrolloff;
			if ((scrolloff == list_menu_height / 2) && (list_menu_height % 2 == 0)) {
				// Makes scrolling with huge scrolloff work consistently in both directions.
				m->view_min += 1;
			}
		} else {
			m->view_min = 0;
		}
		m->view_max = m->view_min + (list_menu_height - 1);
		m->view_sel = new_sel;
		if (m == menu) {
			expose_all_visible_entries_of_the_list_menu_unprotected();
		}
	} else if (new_sel != m->view_sel) {
		if (m == menu) {
			wbkgd(windows[m->view_sel - m->view_min], m->paint_action(m, m->view_sel, false));
			m->view_sel = new_sel;
			wbkgd(windows[m->view_sel - m->view_min], m->paint_action(m, m->view_sel, true));
			tb_present();
		} else {
			m->view_sel = new_sel;
		}
	}
}

static void
change_list_view_filtered_unprotected(struct menu_state *m, bool (*filter)(struct menu_state *, size_t), bool is_forward)
{
	if (!filter) {
		return;
	}
	if (is_forward) {
		for (size_t i = m->view_sel + 1, j = 0; j < 2; i = 0, ++j) {
			while (m->enumerator(m, i) == true) {
				if (filter(m, i) == true) {
					change_list_view_unprotected(m, i, false);
					j = 2;
					break;
				}
				i += 1;
			}
			if (!get_cfg_bool(NULL, CFG_SCROLLWRAP)) {
				break;
			}
		}
	} else {
		for (size_t i = m->view_sel, j = 0; j < 2; ++j) {
			while (i > 0 && m->enumerator(m, i - 1) == true) {
				if (filter(m, i - 1) == true) {
					change_list_view_unprotected(m, i - 1, false);
					j = 2;
					break;
				}
				i -= 1;
			}
			if (j < 2) {
				i = obtain_list_entries_count_unprotected(m);
			}
			if (!get_cfg_bool(NULL, CFG_SCROLLWRAP)) {
				break;
			}
		}
	}
}

bool
handle_list_menu_control(struct menu_state *m, input_id cmd, const struct wstring *arg)
{
	pthread_mutex_lock(&interface_lock);
	if (cmd == INPUT_SELECT_NEXT || cmd == INPUT_JUMP_TO_NEXT) {
		change_list_view_unprotected(m, m->view_sel + 1, true);
	} else if (cmd == INPUT_SELECT_PREV || cmd == INPUT_JUMP_TO_PREV) {
		change_list_view_unprotected(m, m->view_sel > 1 ? m->view_sel - 1 : 0, true);
	} else if (cmd == INPUT_JUMP_TO_NEXT_UNREAD) {
		change_list_view_filtered_unprotected(m, m->unread_state, true);
	} else if (cmd == INPUT_JUMP_TO_PREV_UNREAD) {
		change_list_view_filtered_unprotected(m, m->unread_state, false);
	} else if (cmd == INPUT_JUMP_TO_NEXT_IMPORTANT && m->run == &items_menu_loop) {
		change_list_view_filtered_unprotected(m, important_item_condition, true);
	} else if (cmd == INPUT_JUMP_TO_PREV_IMPORTANT && m->run == &items_menu_loop) {
		change_list_view_filtered_unprotected(m, important_item_condition, false);
	} else if (cmd == INPUT_JUMP_TO_NEXT_ERROR) {
		change_list_view_filtered_unprotected(m, m->failed_state, true);
	} else if (cmd == INPUT_JUMP_TO_PREV_ERROR) {
		change_list_view_filtered_unprotected(m, m->failed_state, false);
	} else if (cmd == INPUT_SELECT_NEXT_PAGE) {
		change_list_view_unprotected(m, m->view_sel + list_menu_height, true);
	} else if (cmd == INPUT_SELECT_NEXT_PAGE_HALF) {
		change_list_view_unprotected(m, m->view_sel + list_menu_height / 2, true);
	} else if (cmd == INPUT_SELECT_PREV_PAGE) {
		change_list_view_unprotected(m, m->view_sel > list_menu_height ? m->view_sel - list_menu_height : 0, true);
	} else if (cmd == INPUT_SELECT_PREV_PAGE_HALF) {
		size_t step = list_menu_height / 2;
		change_list_view_unprotected(m, m->view_sel > step ? m->view_sel - step : 0, true);
	} else if (cmd == INPUT_SELECT_FIRST) {
		change_list_view_unprotected(m, 0, false);
	} else if (cmd == INPUT_SELECT_LAST) {
		change_list_view_unprotected(m, obtain_list_entries_count_unprotected(m), false);
	} else if (cmd == INPUT_SHIFT_WEST) {
		size_t shift_delta = 1 + list_menu_width / 50;
		if (horizontal_shift >= shift_delta) {
			horizontal_shift -= shift_delta;
			expose_all_visible_entries_of_the_list_menu_unprotected();
		} else if (horizontal_shift > 0) {
			horizontal_shift = 0;
			expose_all_visible_entries_of_the_list_menu_unprotected();
		}
	} else if (cmd == INPUT_SHIFT_EAST) {
		horizontal_shift += 1 + list_menu_width / 50;
		expose_all_visible_entries_of_the_list_menu_unprotected();
	} else if (cmd == INPUT_SHIFT_RESET) {
		if (horizontal_shift != 0) {
			horizontal_shift = 0;
			expose_all_visible_entries_of_the_list_menu_unprotected();
		}
	} else if (cmd == INPUT_CLEAN_STATUS) {
		status_clean_unprotected();
	} else if (cmd == INPUT_SYSTEM_COMMAND || cmd == INPUT_SYSTEM_COMMAND_QUIET) {
		struct format_arg args[100];
		pthread_mutex_unlock(&interface_lock);
		run_formatted_command(arg, m->get_args(m, m->view_sel, args, LENGTH(args)), cmd == INPUT_SYSTEM_COMMAND_QUIET);
		return true;
	} else {
		pthread_mutex_unlock(&interface_lock);
		return false;
	}
	pthread_mutex_unlock(&interface_lock);
	return true;
}

static void
change_pager_view_unprotected(size_t new_sel)
{
	if (list_menu_height < 1) {
		WARN("Ignoring pager view change because of zero size.");
		return;
	}
	while (menu->enumerator(menu, new_sel) == false && new_sel > 0) {
		new_sel -= 1;
	}
	if (menu->enumerator(menu, new_sel) == false) {
		return;
	}
	size_t entries_count = obtain_list_entries_count_unprotected(menu);
	if (entries_count - new_sel < list_menu_height) {
		new_sel = entries_count > list_menu_height ? entries_count - list_menu_height : 0;
	}
	if (new_sel != menu->view_min) {
		menu->view_max = new_sel + (list_menu_height - 1);
		while (menu->view_max >= list_menu_height && menu->enumerator(menu, menu->view_max) == false) {
			menu->view_max -= 1;
		}
		menu->view_min = menu->view_max - (list_menu_height - 1);
		expose_all_visible_entries_of_the_list_menu_unprotected();
	}
}

bool
handle_pager_menu_control(input_id cmd)
{
	pthread_mutex_lock(&interface_lock);
	if (cmd == INPUT_SELECT_NEXT || cmd == INPUT_ENTER) {
		change_pager_view_unprotected(menu->view_min + 1);
	} else if (cmd == INPUT_SELECT_PREV) {
		change_pager_view_unprotected(menu->view_min > 0 ? menu->view_min - 1 : 0);
	} else if (cmd == INPUT_SELECT_NEXT_PAGE) {
		change_pager_view_unprotected(menu->view_min + list_menu_height);
	} else if (cmd == INPUT_SELECT_NEXT_PAGE_HALF) {
		change_pager_view_unprotected(menu->view_min + list_menu_height / 2);
	} else if (cmd == INPUT_SELECT_PREV_PAGE) {
		change_pager_view_unprotected(menu->view_min > list_menu_height ? menu->view_min - list_menu_height : 0);
	} else if (cmd == INPUT_SELECT_PREV_PAGE_HALF) {
		size_t step = list_menu_height / 2;
		change_pager_view_unprotected(menu->view_min > step ? menu->view_min - step : 0);
	} else if (cmd == INPUT_SELECT_FIRST) {
		change_pager_view_unprotected(0);
	} else if (cmd == INPUT_SELECT_LAST) {
		change_pager_view_unprotected(obtain_list_entries_count_unprotected(menu));
	} else if (cmd == INPUT_CLEAN_STATUS) {
		status_clean_unprotected();
	} else {
		pthread_mutex_unlock(&interface_lock);
		return false;
	}
	pthread_mutex_unlock(&interface_lock);
	return true;
}

static inline void
free_deleted_menus(bool purge_them_all)
{
	while (menus && (purge_them_all || menus->is_deleted)) {
		struct menu_state *tmp = menus;
		menus = menus->prev;
		free_string(tmp->name);
		free_string(tmp->search_token);
		free_items_list(tmp->items);
		newsraft_free(tmp->feeds_view);
		newsraft_free(tmp);
	}
}

void
free_menus(void)
{
	free_deleted_menus(true);
}

size_t
get_menu_depth(void)
{
	size_t depth = 0;
	for (struct menu_state *i = menus; i != NULL && i->is_deleted == false; i = i->prev) {
		depth += 1;
	}
	return depth;
}

static void
update_unread_items_count_of_last_menu(void)
{
	if (menus && menus->feeds_full) {
		for (size_t i = 0; i < menus->feeds_full_size; ++i) {
			menus->feeds_full[i]->unread_count = db_count_items(&menus->feeds_full[i], 1, true);
		}
	}
}

struct menu_state *
setup_menu(struct menu_state *(*run)(struct menu_state *), const struct string *name, struct feed_entry **feeds, size_t feeds_count, uint32_t flags, const void *ctx)
{
	pthread_mutex_lock(&interface_lock);
	update_unread_items_count_of_last_menu();
	struct menu_state *new = newsraft_calloc(1, sizeof(*new));
	new->run             = run;
	new->feeds_full      = feeds;
	new->feeds_full_size = feeds_count;
	new->flags           = flags;
	new->prev            = menus;
	new->find_filter     = ctx;
	new->search_token    = pop_search_filter();
	if (!STRING_IS_EMPTY(name)) {
		cpyss(&new->name, name);
	} else if (!STRING_IS_EMPTY(new->search_token)) {
		cpyss(&new->name, new->search_token);
	} else if (new->find_filter) {
		new->name = convert_wstring_to_string(new->find_filter);
	} else if (feeds && feeds_count == 1) {
		cpyss(&new->name, STRING_IS_EMPTY(feeds[0]->name) ? feeds[0]->url : feeds[0]->name);
	}
	if ((flags & MENU_SWALLOW) && menus != NULL) {
		menus->is_deleted = true;
	}
	menus = new;
	pthread_mutex_unlock(&interface_lock);
	return menus;
}

struct menu_state *
close_menu(void)
{
	pthread_mutex_lock(&interface_lock);
	update_unread_items_count_of_last_menu();
	struct menu_state *next_menu = NULL;
	for (struct menu_state *i = menus; i != NULL; i = i->prev) {
		if (i->is_deleted == false) {
			i->is_deleted = true;
			break;
		}
	}
	for (struct menu_state *i = menus; i != NULL; i = i->prev) {
		if (i->is_deleted == false) {
			next_menu = i;
			next_menu->flags |= MENU_DISABLE_SETTINGS;
			break;
		}
	}
	pthread_mutex_unlock(&interface_lock);
	return next_menu;
}

void
start_menu(void)
{
	pthread_mutex_lock(&interface_lock);
	free_deleted_menus(false);
	menu = menus;

	// These methods are mandatory for all menus.
	assert(menu->enumerator);
	assert(menu->printer);

	horizontal_shift = 0;
	if (!menu->is_initialized) {
		menu->view_sel = 0;
		menu->view_min = 0;
		status_clean_unprotected();
	}
	redraw_list_menu_unprotected();
	menu->is_initialized = true;
	pthread_mutex_unlock(&interface_lock);
}

void
write_menu_path_string(struct string *names, struct menu_state *m)
{
	if (menus == NULL) {
		return;
	} else if (m == NULL) {
		write_menu_path_string(names, menus);
		return;
	} else if (m->prev != NULL) {
		write_menu_path_string(names, m->prev);
	}
	if (m->is_deleted == false && m->name != NULL && m->name->len > 0) {
		catas(names, "  >  ", 5);
		catss(names, m->name);
	}
}

void
raise_menu_age(void)
{
	pthread_mutex_lock(&interface_lock);
	menu_age += 1;
	yield_control_to_menu();
	pthread_mutex_unlock(&interface_lock);
}

uint64_t
fetch_menu_age(void)
{
	pthread_mutex_lock(&interface_lock);
	const uint64_t ret = menu_age;
	pthread_mutex_unlock(&interface_lock);
	return ret;
}
