/* vifm
 * Copyright (C) 2012 xaizek.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "column_view.h"

#include <assert.h> /* assert() */
#include <stddef.h> /* NULL size_t */
#include <stdlib.h> /* malloc() free() */
#include <string.h> /* memmove() memset() strcpy() strdup() strlen() */

#include "../compat/reallocarray.h"
#include "../utils/macros.h"
#include "../utils/str.h"
#include "../utils/utf8.h"
#include "../utils/utils.h"

/* Character used to fill gaps in lines. */
#define GAP_FILL_CHAR ' '

/* Holds general column information. */
typedef struct
{
	int column_id;    /* Unique column id. */
	column_func func; /* Function, which prints column value. */
	void *data;       /* Data to be passed to the function. */
}
column_desc_t;

/* Column information including calculated values. */
typedef struct
{
	column_info_t info; /* Column properties specified by the client. */
	int start;          /* Start position of the column. */
	int width;          /* Calculated width of the column. */
	int print_width;    /* Print width (less or equal to the width field). */
	column_desc_t desc; /* Column description. */
}
column_t;

/* Column view description structure.  Typedef is in the header file. */
struct columns_t
{
	int max_width;  /* Maximum width of one line of the view. */
	int count;      /* Number of columns in the list. */
	column_t *list; /* Array of columns of count length. */
};

static int extend_column_desc_list(void);
static void init_new_column_desc(column_desc_t *desc, int column_id,
		column_func func, void *data);
static int column_id_present(int column_id);
static int extend_column_list(columns_t *cols);
static void init_new_column(column_t *col, column_info_t info);
static void mark_for_recalculation(columns_t *cols);
static const column_desc_t * get_column_func(int column_id);
static void adjust_match_range(size_t cut_from, size_t cut_to, size_t ell_len,
		size_t *match_from, size_t *match_to, int *use_marks);
static AlignType decorate_output(const column_t *col, format_info_t *info,
		char buf[], size_t buf_len, int max_line_width);
static int calculate_max_width(const column_t *col, int len,
		int max_line_width);
static int calculate_start_pos(const column_t *col, const char buf[],
		AlignType align);
static void fill_gap_pos(void *format_data, int from, int to, int column_id);
static int get_width_on_screen(const char str[]);
static void recalculate_if_needed(columns_t *cols, int max_width);
static void recalculate(columns_t *cols, int max_width);
static void update_widths(columns_t *cols, int max_width);
static int update_abs_and_rel_widths(columns_t *cols, int *max_width);
static void update_auto_widths(columns_t *cols, int auto_count, int max_width);
static void update_start_positions(columns_t *cols);

/* Number of registered column descriptors. */
static int col_desc_count;
/* List of column descriptors. */
static column_desc_t *col_descs;
/* Column print function. */
static column_line_print_func print_func;
/* Optional function to query column match. */
static column_line_match_func match_func;
/* String to be used in place of ellipsis. */
static const char *ellipsis = "...";
/* Strings to be used when highlighted text goes out of bounds. */
static const char *left_marks = "<<<";
static const char *right_marks = ">>>";
static const char *middle_marks = ">..<";

void
columns_set_line_print_func(column_line_print_func func)
{
	print_func = func;
}

void
columns_set_line_match_func(column_line_match_func func)
{
	match_func = func;
}

void
columns_set_ellipsis(const char ell[])
{
	ellipsis = ell;
}

int
columns_add_column_desc(int column_id, column_func func, void *data)
{
	if(!column_id_present(column_id) && extend_column_desc_list() == 0)
	{
		init_new_column_desc(&col_descs[col_desc_count - 1], column_id, func, data);
		return 0;
	}
	return 1;
}

/* Extends column descriptors list by one element, but doesn't initialize it at
 * all.  Returns zero on success. */
static int
extend_column_desc_list(void)
{
	column_desc_t *mem_ptr;
	mem_ptr = reallocarray(col_descs, col_desc_count + 1, sizeof(*mem_ptr));
	if(mem_ptr == NULL)
	{
		return 1;
	}
	col_descs = mem_ptr;
	col_desc_count++;
	return 0;
}

/* Fills column description structure with initial values. */
static void
init_new_column_desc(column_desc_t *desc, int column_id, column_func func,
		void *data)
{
	desc->column_id = column_id;
	desc->func = func;
	desc->data = data;
}

columns_t *
columns_create(void)
{
	columns_t *const result = malloc(sizeof(*result));
	if(result == NULL)
	{
		return NULL;
	}
	result->count = 0;
	result->list = NULL;
	mark_for_recalculation(result);
	return result;
}

void
columns_free(columns_t *cols)
{
	if(cols != NULL)
	{
		columns_clear(cols);
		free(cols);
	}
}

void
columns_clear(columns_t *cols)
{
	int i;
	for(i = 0U; i < cols->count; ++i)
	{
		free(cols->list[i].info.literal);
	}

	free(cols->list);
	cols->list = NULL;
	cols->count = 0;
}

void
columns_clear_column_descs(void)
{
	free(col_descs);
	col_descs = NULL;
	col_desc_count = 0;
}

void
columns_add_column(columns_t *cols, column_info_t info)
{
	assert(info.text_width <= info.full_width &&
			"Text width should be bigger than full width.");
	assert(column_id_present(info.column_id) && "Unknown column id.");
	if(extend_column_list(cols) == 0)
	{
		init_new_column(&cols->list[cols->count - 1], info);
		mark_for_recalculation(cols);
	}
}

/* Checks whether a column with such column_id is already in the list of column
 * descriptors. Returns non-zero if yes. */
static int
column_id_present(int column_id)
{
	if(column_id == FILL_COLUMN_ID)
	{
		/* This pseudo-column is always "present". */
		return 1;
	}

	int i;
	/* Validate column_id. */
	for(i = 0; i < col_desc_count; i++)
	{
		if(col_descs[i].column_id == column_id)
		{
			return 1;
		}
	}
	return 0;
}

/* Extends columns list by one element, but doesn't initialize it at all.
 * Returns zero on success. */
static int
extend_column_list(columns_t *cols)
{
	column_t *mem_ptr;
	mem_ptr = reallocarray(cols->list, cols->count + 1, sizeof(*mem_ptr));
	if(mem_ptr == NULL)
	{
		return 1;
	}
	cols->list = mem_ptr;
	cols->count++;
	return 0;
}

/* Marks columns structure as one that need to be recalculated. */
static void
mark_for_recalculation(columns_t *cols)
{
	cols->max_width = -1;
}

/* Fills column structure with initial values. */
static void
init_new_column(column_t *col, column_info_t info)
{
	col->info = info;
	if(info.literal != NULL)
	{
		col->info.literal = strdup(info.literal);
	}

	col->start = -1;
	col->width = -1;
	col->print_width = -1;

	const column_desc_t *desc = get_column_func(info.column_id);
	if(desc != NULL)
	{
		col->desc = *desc;
	}
}

/* Returns a pointer to column formatting function by the column id or NULL on
 * unknown column_id. */
static const column_desc_t *
get_column_func(int column_id)
{
	int i;
	for(i = 0; i < col_desc_count; i++)
	{
		column_desc_t *col_desc = &col_descs[i];
		if(col_desc->column_id == column_id)
		{
			return col_desc;
		}
	}

	assert((column_id == FILL_COLUMN_ID) && "Unknown column id");
	return NULL;
}

void
columns_format_line(columns_t *cols, void *format_data, int max_line_width)
{
	char prev_col_buf[1024 + 1];
	int prev_col_start = 0;
	prev_col_buf[0] = '\0';

	int i;
	int prev_col_end = 0;
	int prev_col_id = FILL_COLUMN_ID;

	recalculate_if_needed(cols, max_line_width);

	for(i = 0; i < cols->count; ++i)
	{
		/* Use big buffer to hold whole item so there will be no issues with right
		 * aligned fields. */
		char col_buffer[sizeof(prev_col_buf)];
		char full_column[sizeof(prev_col_buf)];
		const column_t *const col = &cols->list[i];
		format_info_t info = {
			.data = format_data,
			.id = col->info.column_id,
			.real_id = col->info.column_id,
			.width = col->print_width,
			.match_from = -1,
			.match_to = -1,
		};

		if(col->info.literal == NULL)
		{
			col->desc.func(col->desc.data, sizeof(col_buffer), col_buffer, &info);
		}
		else
		{
			copy_str(col_buffer, sizeof(col_buffer), col->info.literal);
		}

		if(match_func != NULL)
		{
			match_func(col_buffer, &info, &info.match_from, &info.match_to);
		}

		strcpy(full_column, col_buffer);

		AlignType align = decorate_output(col, &info, col_buffer,
				sizeof(col_buffer), max_line_width);
		const int cur_col_start = calculate_start_pos(col, col_buffer, align);
		int print_start = MIN(cur_col_start, col->start);

		/* Ensure that we are not trying to draw current column in the middle of a
		 * character inside previous column. */
		if(prev_col_end > print_start)
		{
			const int prev_col_max_width = (cur_col_start > prev_col_start)
			                             ? (cur_col_start - prev_col_start)
			                             : 0;
			const size_t break_point = utf8_strsnlen(prev_col_buf,
					prev_col_max_width);
			prev_col_buf[break_point] = '\0';
			int real_prev_end = prev_col_start + get_width_on_screen(prev_col_buf);
			fill_gap_pos(format_data, real_prev_end, print_start, prev_col_id);
			print_start = real_prev_end;
		}
		else
		{
			fill_gap_pos(format_data, prev_col_end, print_start, prev_col_id);
		}

		/* Gap filling is done per column.  This one is for the current one. */
		fill_gap_pos(format_data, print_start, cur_col_start, col->info.column_id);

		print_func(col_buffer, cur_col_start, align, full_column, &info);

		prev_col_end = cur_col_start + get_width_on_screen(col_buffer);
		prev_col_id = col->info.column_id;

		/* Store information about the current column for usage on the next
		 * iteration. */
		strcpy(prev_col_buf, col_buffer);
		prev_col_start = cur_col_start;
	}

	fill_gap_pos(format_data, prev_col_end, max_line_width, prev_col_id);
}

/* Adds decorations like ellipsis to the output.  Returns actual align type used
 * for the column (might not match col->info.align). */
static AlignType
decorate_output(const column_t *col, format_info_t *info, char buf[],
		size_t buf_len, int max_line_width)
{
	char * (*add_ellipsis)(const char str[], size_t max_width, const char ell[]);
	void (*get_cut_range)(const char str[], size_t max_width, size_t *cut_from,
			size_t *cut_to);

	const int len = get_width_on_screen(buf);
	const int max_col_width = calculate_max_width(col, len, max_line_width);
	const int too_long = len > max_col_width;
	AlignType result;
	const char *ell = (col->info.cropping == CT_ELLIPSIS ? ellipsis : "");
	const char *marks;
	char *ellipsed;

	if(!too_long)
	{
		return (col->info.align == AT_RIGHT ? AT_RIGHT : AT_LEFT);
	}

	if(col->info.align == AT_MIDDLE)
	{
		add_ellipsis = middle_ellipsis;
		get_cut_range = get_middle_cut_range;

		marks = middle_marks;
		result = AT_LEFT;
	}
	else if(col->info.align == AT_LEFT)
	{
		add_ellipsis = right_ellipsis;
		get_cut_range = get_right_cut_range;

		marks = right_marks;
		result = AT_LEFT;
	}
	else
	{
		add_ellipsis = left_ellipsis;
		get_cut_range = get_left_cut_range;

		marks = left_marks;
		result = AT_RIGHT;
	}

	if(info->match_from != info->match_to)
	{
		/* If the line is cut, search match offsets might need to be adjusted. */
		size_t match_from = info->match_from;
		size_t match_to = info->match_to;
		size_t cut_from = 0;
		size_t cut_to = 0;
		size_t ell_len = strlen(ell);
		size_t ell_width = utf8_strsw(ell);
		int use_marks;

		get_cut_range(buf, max_col_width - ell_width, &cut_from, &cut_to);
		adjust_match_range(cut_from, cut_to, ell_len, &match_from, &match_to,
				&use_marks);

		if(use_marks)
		{
			ell = marks;
			size_t new_ell_len = strlen(ell);
			size_t new_ell_width = utf8_strsw(ell);
			if(new_ell_len != ell_len || new_ell_width != ell_width)
			{
				/* If ellipsis length or width has changed, readjust match range. */
				match_from = info->match_from;
				match_to = info->match_to;
				get_cut_range(buf, max_col_width - new_ell_width, &cut_from, &cut_to);
				adjust_match_range(cut_from, cut_to, new_ell_len, &match_from,
						&match_to, &use_marks);
			}
		}

		info->match_from = match_from;
		info->match_to = match_to;
	}

	ellipsed = add_ellipsis(buf, max_col_width, ell);

	copy_str(buf, buf_len, ellipsed);
	free(ellipsed);

	return result;
}

/* Adjusts search match offsets according to cut range and ellipsis length.
 * Sets use_marks argument to non-zero if match range overlaps ellipsis,
 * otherwise to zero. */
static void
adjust_match_range(size_t cut_from, size_t cut_to, size_t ell_len,
		size_t *match_from, size_t *match_to, int *use_marks)
{
	*use_marks = 0;

	if(*match_from >= cut_from && *match_from < cut_to)
	{
		*match_from = cut_from;
		*use_marks = 1;
	}
	else if(*match_from >= cut_to)
	{
		*match_from = cut_from + ell_len + *match_from - cut_to;
	}

	if(*match_to > cut_from && *match_to <= cut_to)
	{
		*match_to = cut_from + ell_len;
		*use_marks = 1;
	}
	else if(*match_to > cut_to)
	{
		*match_to = cut_from + ell_len + *match_to - cut_to;
	}

	if(*match_from < cut_from && *match_to > cut_from)
	{
		*use_marks = 1;
	}
}

/* Calculates maximum width for outputting content of the col. */
static int
calculate_max_width(const column_t *col, int len, int max_line_width)
{
	if(col->info.cropping != CT_NONE)
	{
		return col->print_width;
	}

	const int left_bound = (col->info.align == AT_LEFT ? col->start : 0);
	const int max_col_width = max_line_width - left_bound;
	return MIN(len, max_col_width);
}

/* Calculates start position for outputting content of the col. */
static int
calculate_start_pos(const column_t *col, const char buf[], AlignType align)
{
	if(align == AT_LEFT)
	{
		return col->start;
	}

	const int end = col->start + col->width;
	const int len = get_width_on_screen(buf);
	return (end > len && align == AT_RIGHT) ? (end - len) : 0;
}

/* Prints gap filler (GAP_FILL_CHAR) in place of gaps.  Does nothing if to less
 * or equal to from. */
static void
fill_gap_pos(void *format_data, int from, int to, int column_id)
{
	if(to > from)
	{
		char gap[to - from + 1];
		memset(gap, GAP_FILL_CHAR, to - from);
		gap[to - from] = '\0';

		const format_info_t info = {
			.data = format_data,
			.id = FILL_COLUMN_ID,
			.real_id = column_id,
			.width = to - from,
		};
		print_func(gap, from, AT_LEFT, gap, &info);
	}
}

/* Returns number of character positions allocated by the string on the
 * screen.  On issues will try to do the best, to make visible at least
 * something. */
static int
get_width_on_screen(const char str[])
{
	int length = -1;

	wchar_t *const wide = to_wide(str);
	if(wide != NULL)
	{
		length = vifm_wcswidth(wide, (size_t)-1);
		free(wide);
	}

	if(length < 0)
	{
		length = utf8_nstrlen(str);
	}
	return length;
}

/* Checks if recalculation is needed and runs it if yes. */
static void
recalculate_if_needed(columns_t *cols, int max_width)
{
	if(!columns_matches_width(cols, max_width))
	{
		recalculate(cols, max_width);
	}
}

int
columns_matches_width(const columns_t *cols, int max_width)
{
	return cols->max_width == max_width;
}

/* Recalculates column widths and start offsets. */
static void
recalculate(columns_t *cols, int max_width)
{
	update_widths(cols, max_width);
	update_start_positions(cols);

	cols->max_width = max_width;
}

/* Recalculates column widths. */
static void
update_widths(columns_t *cols, int max_width)
{
	int width_left = max_width;
	const int auto_count = update_abs_and_rel_widths(cols, &width_left);
	update_auto_widths(cols, auto_count, width_left);
}

/* Recalculates widths of columns with absolute or percent widths.  Returns
 * number of columns with auto size type. */
static int
update_abs_and_rel_widths(columns_t *cols, int *max_width)
{
	int i;
	int auto_count = 0;
	int percent_count = 0;
	int width_left = *max_width;
	for(i = 0; i < cols->count; i++)
	{
		int effective_width;
		column_t *col = &cols->list[i];
		if(col->info.sizing == ST_ABSOLUTE)
		{
			effective_width = MIN(col->info.full_width, width_left);
		}
		else if(col->info.sizing == ST_PERCENT)
		{
			++percent_count;
			effective_width = MIN(col->info.full_width**max_width/100, width_left);
		}
		else
		{
			auto_count++;
			continue;
		}

		width_left -= effective_width;
		col->width = effective_width;
		if(col->info.sizing == ST_ABSOLUTE)
		{
			col->print_width = MIN(effective_width, col->info.text_width);
		}
		else
		{
			col->print_width = col->width;
		}
	}

	/* When there is no columns with automatic size, give unused size to last
	 * percent column if one exists.  This removes right "margin", which otherwise
	 * present. */
	if(auto_count == 0U && percent_count != 0U)
	{
		int i;
		for(i = cols->count - 1; i >= 0; --i)
		{
			column_t *col = &cols->list[i];
			if(col->info.sizing == ST_PERCENT)
			{
				col->width += width_left;
				col->print_width += width_left;
				width_left = 0U;
			}
		}
	}

	*max_width = width_left;
	return auto_count;
}

/* Recalculates widths of columns with automatic widths. */
static void
update_auto_widths(columns_t *cols, int auto_count, int max_width)
{
	int i;
	int auto_left = auto_count;
	int left = max_width;
	for(i = 0; i < cols->count; i++)
	{
		column_t *col = &cols->list[i];
		if(col->info.sizing == ST_AUTO)
		{
			auto_left--;

			col->width = (auto_left > 0) ? max_width/auto_count : left;
			col->print_width = col->width;

			left -= col->width;
		}
	}
}

/* Should be used after updating column widths. */
static void
update_start_positions(columns_t *cols)
{
	int i;
	for(i = 0; i < cols->count; i++)
	{
		if(i == 0)
		{
			cols->list[i].start = 0;
		}
		else
		{
			column_t *prev_col = &cols->list[i - 1];
			cols->list[i].start = prev_col->start + prev_col->width;
		}
	}
}

/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
/* vim: set cinoptions+=t0 filetype=c : */
