/*
 * Samba Unix/Linux SMB client library
 * Registry Editor
 * Copyright (C) Christopher Davis 2014
 *
 * 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 3 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 "regedit_list.h"
#include "regedit.h"

#define CLAMP(x, low, high) \
	(((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))

struct multilist {
	WINDOW *window;
	WINDOW *pad;

	unsigned window_height;
	unsigned window_width;
	unsigned start_row;
	unsigned cursor_row;

	unsigned ncols;
	struct multilist_column *columns;

	const void *data;
	unsigned nrows;
	const void *current_row;
	const struct multilist_accessors *cb;
};

/* data getters */
static const void *data_get_first_row(struct multilist *list)
{
	SMB_ASSERT(list->cb->get_first_row);
	return list->cb->get_first_row(list->data);
}

static const void *data_get_next_row(struct multilist *list, const void *row)
{
	SMB_ASSERT(list->cb->get_next_row);
	return list->cb->get_next_row(list->data, row);
}

static const void *data_get_prev_row(struct multilist *list, const void *row)
{
	const void *tmp, *next;

	if (list->cb->get_prev_row) {
		return list->cb->get_prev_row(list->data, row);
	}

	tmp = data_get_first_row(list);
	if (tmp == row) {
		return NULL;
	}

	for (; tmp && (next = data_get_next_row(list, tmp)) != row;
	     tmp = next) {
	}

	SMB_ASSERT(tmp != NULL);

	return tmp;
}

static unsigned data_get_row_count(struct multilist *list)
{
	unsigned i;
	const void *row;

	if (list->cb->get_row_count)
		return list->cb->get_row_count(list->data);

	for (i = 0, row = data_get_first_row(list);
	     row != NULL;
	     ++i, row = data_get_next_row(list, row)) {
	}

	return i;
}

static const void *data_get_row_n(struct multilist *list, size_t n)
{
	unsigned i;
	const void *row;

	if (list->cb->get_row_n)
		return list->cb->get_row_n(list->data, n);

	for (i = 0, row = data_get_first_row(list);
	     i < n && row != NULL;
	     ++i, row = data_get_next_row(list, row)) {
	}

	return row;
}

static const char *data_get_column_header(struct multilist *list, unsigned col)
{
	SMB_ASSERT(list->cb->get_column_header);
	return list->cb->get_column_header(list->data, col);
}

static const char *data_get_item_label(struct multilist *list, const void *row,
				       unsigned col)
{
	SMB_ASSERT(list->cb->get_item_label);
	return list->cb->get_item_label(row, col);
}

static const char *data_get_item_prefix(struct multilist *list, const void *row,
					unsigned col)
{
	if (list->cb->get_item_prefix)
		return list->cb->get_item_prefix(row, col);
	return "";
}

static int multilist_free(struct multilist *list)
{
	if (list->pad) {
		delwin(list->pad);
	}

	return 0;
}

struct multilist *multilist_new(TALLOC_CTX *ctx, WINDOW *window,
				const struct multilist_accessors *cb,
				unsigned ncol)
{
	struct multilist *list;

	SMB_ASSERT(ncol > 0);

	list = talloc_zero(ctx, struct multilist);
	if (list == NULL) {
		return NULL;
	}
	talloc_set_destructor(list, multilist_free);

	list->cb = cb;
	list->ncols = ncol;
	list->columns = talloc_zero_array(list, struct multilist_column, ncol);
	if (list->columns == NULL) {
		talloc_free(list);
		return NULL;
	}
	multilist_set_window(list, window);

	return list;
}

struct multilist_column *multilist_column_config(struct multilist *list,
						 unsigned col)
{
	SMB_ASSERT(col < list->ncols);
	return &list->columns[col];
}

static void put_padding(WINDOW *win, size_t col_width, size_t item_len)
{
	size_t amt;

	SMB_ASSERT(item_len <= col_width);

	amt = col_width - item_len;
	while (amt--) {
		waddch(win, ' ');
	}
}

static void put_item(struct multilist *list, WINDOW *win, unsigned col,
		     const char *item, int attr)
{
	bool append_sep = true;
	unsigned i;
	size_t len;
	struct multilist_column *col_info;
	bool trim = false;

	SMB_ASSERT(col < list->ncols);
	SMB_ASSERT(item != NULL);

	if (col == list->ncols - 1) {
		append_sep = false;
	}
	col_info = &list->columns[col];

	len = strlen(item);
	if (len > col_info->width) {
		len = col_info->width;
		trim = true;
	}

	if (col_info->align_right) {
		put_padding(win, col_info->width, len);
	}
	for (i = 0; i < len; ++i) {
		if (i == len - 1 && trim) {
			waddch(win, '~' | attr);
		} else {
			waddch(win, item[i] | attr);
		}
	}
	if (!col_info->align_right) {
		put_padding(win, col_info->width, len);
	}

	if (append_sep) {
		waddch(win, ' ');
		waddch(win, '|');
		waddch(win, ' ');
	}
}

static void put_header(struct multilist *list)
{
	unsigned col;
	const char *header;

	if (!list->cb->get_column_header) {
		return;
	}

	wmove(list->window, 0, 0);
	for (col = 0; col < list->ncols; ++col) {
		header = data_get_column_header(list, col);
		SMB_ASSERT(header != NULL);
		put_item(list, list->window, col, header,
			 A_BOLD | COLOR_PAIR(PAIR_YELLOW_BLUE));
	}
}

static WERROR put_data(struct multilist *list)
{
	const void *row;
	int ypos;
	unsigned col;
	const char *prefix, *item;
	char *tmp;

	for (ypos = 0, row = data_get_first_row(list);
	     row != NULL;
	     row = data_get_next_row(list, row), ++ypos) {
		wmove(list->pad, ypos, 0);
		for (col = 0; col < list->ncols; ++col) {
			prefix = data_get_item_prefix(list, row, col);
			SMB_ASSERT(prefix != NULL);
			item = data_get_item_label(list, row, col);
			SMB_ASSERT(item != NULL);
			tmp = talloc_asprintf(list, "%s%s", prefix, item);
			if (tmp == NULL) {
				return WERR_NOT_ENOUGH_MEMORY;
			}
			put_item(list, list->pad, col, tmp, 0);
			talloc_free(tmp);
		}
	}

	return WERR_OK;
}

#define MIN_WIDTH 3
static struct multilist_column *find_widest_column(struct multilist *list)
{
	unsigned col;
	struct multilist_column *colp;

	SMB_ASSERT(list->ncols > 0);
	colp = &list->columns[0];

	for (col = 1; col < list->ncols; ++col) {
		if (list->columns[col].width > colp->width) {
			colp = &list->columns[col];
		}
	}

	if (colp->width < MIN_WIDTH) {
		return NULL;
	}

	return colp;
}

static WERROR calc_column_widths(struct multilist *list)
{
	const void *row;
	unsigned col;
	size_t len;
	const char *item;
	size_t width, total_width, overflow;
	struct multilist_column *colp;

	/* calculate the maximum widths for each column */
	for (col = 0; col < list->ncols; ++col) {
		len = 0;
		if (list->cb->get_column_header) {
			item = data_get_column_header(list, col);
			len = strlen(item);
		}
		list->columns[col].width = len;
	}

	for (row = data_get_first_row(list);
	     row != NULL;
	     row = data_get_next_row(list, row)) {
		for (col = 0; col < list->ncols; ++col) {
			item = data_get_item_prefix(list, row, col);
			SMB_ASSERT(item != NULL);
			len = strlen(item);

			item = data_get_item_label(list, row, col);
			SMB_ASSERT(item != NULL);
			len += strlen(item);
			if (len > list->columns[col].width) {
				list->columns[col].width = len;
			}
		}
	}

	/* calculate row width */
	for (width = 0, col = 0; col < list->ncols; ++col) {
		width += list->columns[col].width;
	}
	/* width including column spacing and separations */
	total_width = width + (list->ncols - 1) * 3;
	/* if everything fits, we're done */
	if (total_width <= list->window_width) {
		return WERR_OK;
	}

	overflow = total_width - list->window_width;

	/* attempt to trim as much as possible to fit all the columns to
	   the window */
	while (overflow && (colp = find_widest_column(list))) {
		colp->width--;
		overflow--;
	}

	return WERR_OK;
}

static void highlight_current_row(struct multilist *list)
{
	mvwchgat(list->pad, list->cursor_row, 0, -1, A_REVERSE, 0, NULL);
}

static void unhighlight_current_row(struct multilist *list)
{
	mvwchgat(list->pad, list->cursor_row, 0, -1, A_NORMAL, 0, NULL);
}

const void *multilist_get_data(struct multilist *list)
{
	return list->data;
}

WERROR multilist_set_data(struct multilist *list, const void *data)
{
	WERROR rv;

	SMB_ASSERT(list->window != NULL);
	list->data = data;

	calc_column_widths(list);

	if (list->pad) {
		delwin(list->pad);
	}
	/* construct a pad that is exactly the width of the window, and
	   as tall as required to fit all data rows. */
	list->nrows = data_get_row_count(list);
	list->pad = newpad(MAX(list->nrows, 1), list->window_width);
	if (list->pad == NULL) {
		return WERR_NOT_ENOUGH_MEMORY;
	}

	/* add the column headers to the window and render all rows to
	   the pad. */
	werase(list->window);
	put_header(list);
	rv = put_data(list);
	if (!W_ERROR_IS_OK(rv)) {
		return rv;
	}

	/* initialize the cursor */
	list->start_row = 0;
	list->cursor_row = 0;
	list->current_row = data_get_first_row(list);
	highlight_current_row(list);

	return WERR_OK;
}

static unsigned get_window_height(struct multilist *list)
{
	unsigned height;

	height = list->window_height;
	if (height > 0 && list->cb->get_column_header) {
		height--;
	}

	/* Clamp to some sensible values */
	return CLAMP(height, 1, 16384);
}

static void fix_start_row(struct multilist *list)
{
	unsigned height;

	/* adjust start_row so that the cursor appears on the screen */

	height = get_window_height(list);
	if (list->cursor_row < list->start_row) {
		list->start_row = list->cursor_row;
	} else if (list->cursor_row >= list->start_row + height) {
		list->start_row = list->cursor_row - height + 1;
	}
	if (list->nrows > height && list->nrows - list->start_row < height) {
		list->start_row = list->nrows - height;
	}
}

WERROR multilist_set_window(struct multilist *list, WINDOW *window)
{
	int maxy, maxx;
	bool rerender = false;

	getmaxyx(window, maxy, maxx);

	/* rerender pad if window width is different. */
	if (list->data && maxx != list->window_width) {
		rerender = true;
	}

	list->window = window;
	list->window_width = maxx;
	list->window_height = maxy;
	list->start_row = 0;
	if (rerender) {
		const void *row = multilist_get_current_row(list);
		WERROR rv = multilist_set_data(list, list->data);
		if (W_ERROR_IS_OK(rv) && row) {
			multilist_set_current_row(list, row);
		}
		return rv;
	} else {
		put_header(list);
		fix_start_row(list);
	}

	return WERR_OK;
}

void multilist_refresh(struct multilist *list)
{
	int window_start_row, height;

	if (list->nrows == 0) {
		return;
	}

	/* copy from pad, starting at start_row, to the window, accounting
	   for the column header (if present). */
	height = MIN(list->window_height, list->nrows);
	window_start_row = 0;
	if (list->cb->get_column_header) {
		window_start_row = 1;
		if (height < list->window_height) {
			height++;
		}
	}
	copywin(list->pad, list->window, list->start_row, 0,
		window_start_row, 0, height - 1, list->window_width - 1,
		false);
}

void multilist_driver(struct multilist *list, int c)
{
	unsigned page;
	const void *tmp = NULL;

	if (list->nrows == 0) {
		return;
	}

	switch (c) {
	case ML_CURSOR_UP:
		if (list->cursor_row == 0) {
			return;
		}
		unhighlight_current_row(list);
		list->cursor_row--;
		tmp = data_get_prev_row(list, list->current_row);
		break;
	case ML_CURSOR_DOWN:
		if (list->cursor_row == list->nrows - 1) {
			return;
		}
		unhighlight_current_row(list);
		list->cursor_row++;
		tmp = data_get_next_row(list, list->current_row);
		break;
	case ML_CURSOR_PGUP:
		if (list->cursor_row == 0) {
			return;
		}
		unhighlight_current_row(list);
		page = get_window_height(list);
		if (page > list->cursor_row) {
			list->cursor_row = 0;
		} else {
			list->cursor_row -= page;
			list->start_row -= page;
		}
		tmp = data_get_row_n(list, list->cursor_row);
		break;
	case ML_CURSOR_PGDN:
		if (list->cursor_row == list->nrows - 1) {
			return;
		}
		unhighlight_current_row(list);
		page = get_window_height(list);
		if (page > list->nrows - list->cursor_row - 1) {
			list->cursor_row = list->nrows - 1;
		} else {
			list->cursor_row += page;
			list->start_row += page;
		}
		tmp = data_get_row_n(list, list->cursor_row);
		break;
	case ML_CURSOR_HOME:
		if (list->cursor_row == 0) {
			return;
		}
		unhighlight_current_row(list);
		list->cursor_row = 0;
		tmp = data_get_row_n(list, list->cursor_row);
		break;
	case ML_CURSOR_END:
		if (list->cursor_row == list->nrows - 1) {
			return;
		}
		unhighlight_current_row(list);
		list->cursor_row = list->nrows - 1;
		tmp = data_get_row_n(list, list->cursor_row);
		break;
	}

	SMB_ASSERT(tmp);
	list->current_row = tmp;
	highlight_current_row(list);
	fix_start_row(list);
}

const void *multilist_get_current_row(struct multilist *list)
{
	return list->current_row;
}

void multilist_set_current_row(struct multilist *list, const void *row)
{
	unsigned i;
	const void *tmp;

	for (i = 0, tmp = data_get_first_row(list);
	     tmp != NULL;
	     ++i, tmp = data_get_next_row(list, tmp)) {
		if (tmp == row) {
			unhighlight_current_row(list);
			list->cursor_row = i;
			list->current_row = row;
			highlight_current_row(list);
			fix_start_row(list);
			return;
		}
	}
}
