// License: MPL-2.0
// (c) 2022 Julian Hurst <ark@mansus.space>

use libtui;
use libtui::widget;
use fmt;
use os;
use strings;
use io;
use strio;
use unix::tty;
use regex;
use fnmatch;
use wcwidth;
use set;

export type listwidget = struct {
	widget: widget::widget,
	items: []str,
	marked: set::set,
	cursor: size,
	//listeners: []listener,
	frame: frame,
	sz: ttysize,
};

export type frame = struct {
	start: u16,
	// largest value is nb of items
	end: u16,
};

export type ttysize = struct {
	rows: u16,
	cols: u16,
};

// An input listener on a list widget. The returning value is intended to be
// used as a signal that will be returned by [[notify]] in order to trigger
// certain more global ui events (terminate the program, change widget focus,
// etc.). To register a listener with a listwidget, use [[addlistener]].
export type listener = *fn(l: *listwidget, r: (rune | libtui::specialkey)) bool;

// Create a new list with the given items. The given items will be duplicated
// using [[strings::dupall]]], so the original items can be freed.
export fn newlist(ui: libtui::ttyui, items: str...) listwidget = {
	let sz = libtui::getwinsize(ui)!;
	let rows = sz.rows - 2;
	//let rows: (u16 | size) = if (sz.rows - 2 < len(items)) {
		//yield sz.rows - 2;
	//} else {
		//yield len(items);
	//};
	let w = listwidget {
		widget = widget::widget {
			print = &print,
			finish = &finish,
			ui = ui,
			...
		},
		items = strings::dupall(items),
		marked = set::set {...},
		cursor = 0z,
		//listeners = [],
		frame = frame {
			start = 0u16,
			end = rows: u16,
		},
		sz = ttysize {
			rows = rows: u16,
			cols = sz.columns,
		},
	};
	return w;
};

// Free the list's items, marked items and call the common widget finish
// function [[widget::finishcommon]].
export fn finish(list: *widget::widget) void = {
	const list = list: *listwidget;
	strings::freeall(list.items);
	set::finish(&list.marked);
	widget::finishcommon(list);
};

// Set the list's items. The given items are duplicated via [[strings::dupall]].
// If the length of the given items is smaller than the list's current items,
// the cursor will be set to 0z and [[reframe]] will be called to reset the
// frame. 
export fn setitems(list: *listwidget, items: str...) void = {
	const doreset = len(items) < len(list.items);
	list.items = strings::dupall(items);
	if (doreset) {
		reset(list);
	};
};

export fn reset(list: *listwidget) void = {
	list.cursor = 0z;
	list.frame.start = 0u16;
	list.frame.end = list.sz.rows;
};

// Add a listener to the given list.
//export fn addlistener(list: *listwidget, l: listener) void = {
	//append(list.listeners, l);
//};

const SELECTED: str = "\x1B[104;1m\x1B[30m";
const MARKED: str = "\x1B[46;1m\x1B[30m";
const RESET: str = "\x1B[0m";

// Print the list's items while truncating the items to not be wider than the
// list.sz.cols.
export fn print(list: *widget::widget) (void | widget::error) = {
	const list = list: *listwidget;
	//let sz = libtui::getwinsize(list.ui)?;
	//let rows: (u16 | size) = if (sz.rows - 2 < len(list.items)) {
		//yield sz.rows - 2;
	//} else {
		//yield len(list.items);
	//};

	//fmt::fprintln(os::stderr, rows)!;

	//list.frame.end = list.frame.start + list.sz.rows;

	let st = strio::dynamic();
	strio::concat(&st, "\r")?;
	let end = if (list.frame.end < len(list.items)) {
		yield list.frame.end;
	} else {
		yield len(list.items);
	};
	for (let i = list.frame.start; i < end: u16; i += 1) {
		const item = list.items[i];
		const truncitem = wcwidth::truncate(item, list.sz.cols);
		if (list.cursor == i) {
			strio::concat(&st, SELECTED, truncitem, RESET)?;
			//libtui::print(list.ui, strings::concat("\x1B[31;1m> ", list.items[i], "\x1B[0m"));
		} else if (set::contains(list.marked, i) is size){
			strio::concat(&st, MARKED, truncitem, RESET)?;
			//libtui::print(list.ui, list.items[i]);
		} else {
			strio::concat(&st, truncitem)?;
		};
		strio::concat(&st, "\r\n")?;
	};
	// unsupported?
	//io::copy(list.ui.f, &st)?;
	let s = strio::string(&st);
	libtui::print(list.widget.ui, s);
	io::close(&st)?;
};

// Notify (call) the listwidget's listeners with the listwidget and r as a
// parameter.  Returns true if a listener returned true, false otherwise.
export fn notify(l: *listwidget, r: (rune | libtui::specialkey)) bool = {
	for (let i = 0z; i < len(l.widget.listeners); i += 1) {
		if (l.widget.listeners[i](l, r)) {
			return true;
		};
	};
	return false;
};

// Reset the list's frame based on the cursor. Returns whether the frame was
// updated.
export fn reframe(l: *listwidget) bool = {
	let reframed: bool = false;
	if (l.cursor < l.frame.start) {
		l.frame.start = l.cursor: u16;
		l.frame.end = l.frame.start + l.sz.rows;
		//l.frame.end -= l.frame.start - l.cursor: u16;
		reframed = true;
	};
	if (l.cursor >= l.frame.end) {
		l.frame.start += l.cursor: u16 - l.frame.end + 1;
		l.frame.end = l.cursor: u16 + 1;
		reframed = true;
	};
	return reframed;
};

// Use signalfd and [[unix::signal::SIGWINCH]] and call this function in the
// handler to support resizing the list.
export fn resize(l: *listwidget, oldsz: ttysize) bool = {
	if (l.frame.end - l.frame.start != l.sz.rows) {
		if (l.cursor < (l.frame.end / 2)) {
			l.frame.end = if (l.frame.start + l.sz.rows > len(l.items)) {
				l.frame.start = len(l.items): u16 - l.sz.rows;
				yield len(l.items): u16;
			} else {
				yield l.frame.start + l.sz.rows;
			};
			for (l.cursor > l.frame.end) {
				l.frame.start += 1;
				l.frame.end += 1;
			};
		} else {
			l.frame.start = if (l.frame.end: int - l.sz.rows: int < 0) {
				l.frame.end = 0 + l.sz.rows;
				yield 0;
			} else {
				yield l.frame.end - l.sz.rows;
			};
			for (l.cursor < l.frame.start) {
				l.frame.start -= 1;
				l.frame.end -= 1;
			};
		};
		return true;
	};
	return false;
};

fn cursorinframe(l: *listwidget) bool = {
	return l.cursor >= l.frame.start && l.cursor < l.frame.end;
};

// Move the list's cursor down one item. Returns the new cursor.
export fn down(l: *listwidget) size = {
	if (l.cursor < len(l.items) - 1) {
		l.cursor += 1;
		reframe(l);
	};
	return l.cursor;
};

// Move the list's cursor up one item. Returns the new cursor.
export fn up(l: *listwidget) size = {
	if (l.cursor > 0) {
		l.cursor -= 1;
		reframe(l);
	};
	return l.cursor;
};

// Move the list's cursor up one page. Returns the new cursor.
export fn pageup(l: *listwidget) size = {
	if (l.cursor: int - l.sz.rows: int >= 0) {
		l.cursor -= l.sz.rows;
	} else {
		l.cursor = 0z;
	};
	reframe(l);
	return l.cursor;
};

// Move the list's cursor down one page. Returns the new cursor.
export fn pagedown(l: *listwidget) size = {
	if (l.cursor + l.sz.rows < len(l.items)) {
		l.cursor += l.sz.rows;
	} else {
		l.cursor = len(l.items) - 1;
	};
	reframe(l);
	return l.cursor;
};

// Move the list's cursor to the top (first item). Returns the new cursor.
export fn top(l: *listwidget) size = {
	l.cursor = 0;
	l.frame.start = 0;
	l.frame.end = l.frame.start + l.sz.rows;
	return l.cursor;
};

// Move the list's cursor to the bottom (last item). Returns the new cursor.
export fn bottom(l: *listwidget) size = {
	l.cursor = len(l.items) - 1;
	l.frame.end = len(l.items): u16;
	l.frame.start = l.frame.end - l.sz.rows;
	return l.cursor;
};

// Forward search through the list's items for an item containing s. Returns the
// new cursor.
export fn search(l: *listwidget, s: str) size = {
	for (let i = l.cursor + 1; i < len(l.items); i += 1) {
		if (strings::contains(l.items[i], s)) {
			l.cursor = i;
			reframe(l);
			return l.cursor;
		};
	};
	return l.cursor;
};

// Backwards search through the list's items for an item containing s. Returns
// the new cursor.
export fn rsearch(l: *listwidget, s: str) size = {
	// size wraps to max value for size when < 0
	for (let i = l.cursor: int - 1; i >= 0; i -= 1) {
		if (strings::contains(l.items[i], s)) {
			l.cursor = i: size;
			reframe(l);
			return l.cursor;
		};
	};
	return l.cursor;
};

// Toggles marking the currently selected item.
export fn tmark(l: *listwidget) void = {
	if (!set::add(&l.marked, l.cursor)) {
		set::del(&l.marked, l.cursor);
	};
};

// Clears all marked items.
export fn clearmarked(l: *listwidget) void = {
	set::clear(&l.marked);
};

// Marks items that contain s (case sensitive).
export fn containsmark(l: *listwidget, s: str) void = {
	for (let i = 0z; i < len(l.items); i += 1) {
		if (strings::contains(l.items[i], s)) {
			set::add(&l.marked, i);
		};
	};
};

// Marks items based on fnmatch (globbing syntax).
export fn fnmatchmark(l: *listwidget, s: str) void = {
	for (let i = 0z; i < len(l.items); i += 1) {
		if (fnmatch::fnmatch(s, l.items[i])) {
			set::add(&l.marked, i);
		};
	};
};

// Marks items according to a regular expression (POSIX ERE).
export fn regexmark(l: *listwidget, re: *regex::regex) void = {
	for (let i = 0z; i < len(l.items); i += 1) {
		if (regex::test(re, l.items[i])) {
			set::add(&l.marked, i);
		};
	};
};

// Returns the selected item or marked items if there are any. The returned
// value is borrowed from the list's items. If []str is returned, the slice must
// be freed.
export fn selected(l: listwidget) (str | []str) = {
	if (len(l.marked.items) > 0) {
		let result: []str = [];
		for (let i = 0z; i < len(l.marked.items); i += 1) {
			append(result, l.items[l.marked.items[i]]);
		};
		return result;
	} else {
		return l.items[l.cursor];
	};
};
