/* Message formatting */
/* $Id$ */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#define DEBUG
#define log(args...)
//#define log(args...)	debug_msg

#include "util/conv.h"
#include "util/lists.h"
#include "util/string.h"
#include "viewer/common/messageline.h"

/* Message lines can be used to configure the appearence of the tabs messages,
 * titles (doc and term) and possibly also the statusline.
 *
 * The syntax:
 *
 *	%<widthflags><width><itemflags><item>
 *
 * The types:
 *
 *	(s)tring, (n)umber, (a)lignment, (b)ool, (d)ate
 *
 * The width flags:
 *
 * 	Type	Flag	Meaning
 *	----------------------------------------------------------------------
 *	all	<none>	No flags means add only if the text length <= width
 *	s	+	As above but fill with spaces to the given width
 *	n	0	Pad number item with zeroes to the given width
 *
 * The type specific flags:
 *
 * 	Type	Flag	Meaning
 *	----------------------------------------------------------------------
 *	s	^	If string trim from start
 *	s	$	If string trim from end
 *	s	/	If string trim up to some item specific trim character
 *			('/' for content type, could be '.' or '/' for uris)
 *
 * The items:
 *
 * 	Item	Type	Meaning
 * 	----------------------------------------------------------------------
 *	%t	s	The document title
 *	%c	s	Content type, flag '/' only subtype (html for text/html)
 *	%n	n	The current link number
 *
 * The TODO or 'would be nice' items:
 *
 *	%>	a	Align following items to the right
 *	%|	a	Align following items to the center
 *	%W	n	Total number of tabs
 *	%w	n	The tab (window) number
 *	%V	b	Visited. Bool could be shown using [+] if ...
 *	%v	d	Last visited. Could be either in form of a date string
 *			or simply [2d] for 2 days, [1w] for one week.
 *	%p	n	Percentage of page that has been scrooled.
 *	%p	n	Current (sub)page
 *	%P	n	Total number of (sub)pages
 *	%u	s	Current URI
 *
 * The grey area or 'probably not' items:
 *
 *	%b	s	The current link is bookmarked (done with leds)
 *	%{lua}	s	Output from Lua function "f"
 *	%d	s	The current date (requires date strings, Mutt)
 *	%s	s	Current status string? (a must for statusbar string)
 *	%l	s	The leds string?
 *	%m	s	Load meter |--->   |, with ending | does fsck animation
 *
 * The motivation:
 *
 * 	Finally a part of elinks which can potentialy torture the system ;)
 */

/* A @message_item is used to store a small item of the format string. */
struct message_item {
	/* Positioning stuff */
	LIST_HEAD(struct message_item);

	/* This @info is a pointer into @message_items array.  */
	struct message_item_info *info;

	/* Private item specific data:
	 * String item -> the string pointer. */
	void *data;

	/* Contraint to control the width of the the item item. Zero if no
	 * width were given for the item.  When the item is a string item
	 * it contains the string size. */
	int width;

	/* Various controls for the formatting. */
	enum {
		MSG_ALIGN_CENTER	= (1 << 0),
		MSG_ALIGN_RIGHT		= (1 << 1),
		MSG_NUMBER_PAD		= (1 << 2),
		MSG_TRIM_BASE		= (1 << 3),
		MSG_TRIM_END		= (1 << 4),
		MSG_TRIM_START		= (1 << 5),
		MSG_WIDTH_FILL		= (1 << 6),
	} flags;
};

/* For storing of hardcoded item info */
struct message_item_info {
	/* The item @id is the item character */
	unsigned char id;

	/* Subtype specific info is stored in @special. For mediatypes it is
	 * the trim char */
	int special;

	/* The @type is used only to know how to interpret the @subtype. Some
	 * type specific mangling of the item text might be necessary. */
	enum {
		MSG_STRING,
		MSG_NUMBER
	} type;

	/* The @subtype are used to lookup the function that will require the
	 * item data. */
	int subtype; /* -> enum msginfo_${type}_subtype */
};


/* The string message functions */

static unsigned char *
get_string_item_string(struct message_item *item)
{
	return item->data;
}

static unsigned char *
get_mediatype_item_string(struct message_item *item)
{
#ifdef	DEBUG
	return "text/html";
#else
#endif
}

static unsigned char *
get_doctitle_item_string(struct message_item *item)
{
#ifdef	DEBUG
	return "Some looong document title";
#else
	return print_current_title(item->data);
#endif
}

enum message_string_subtype {
	MSG_ITEM_DOCTITLE,
	MSG_ITEM_MEDIATYPE,
	MSG_ITEM_STRING,
};

typedef unsigned char *(*msginfo_string_func)(struct message_item *item);

static msginfo_string_func msginfo_string_funcs[] = {
	/* MSG_ITEM_DOCTITLE */		get_doctitle_item_string,
	/* MSG_ITEM_MEDIATYPE */	get_mediatype_item_string,
	/* MSG_ITEM_STRING */		get_string_item_string,
};


/* The number message functions */

static int
get_link_number_item_number(struct message_item *item)
{
#ifdef	DEBUG
	return 42;
#else
#endif
}

enum message_number_subtype {
	MSG_ITEM_LINK_NUMBER,
};

typedef int (*msginfo_number_func)(struct message_item *item);

static msginfo_number_func msginfo_number_funcs[] = {
	/* MSG_ITEM_LINK_NUMBER */	get_link_number_item_number,
};


/* Message line compiling and formatting lookup table */

static struct message_item_info message_items[] = {
	{  0 ,  0 , MSG_STRING, MSG_ITEM_STRING }, /* Keep first! */

	{ 't',  0 , MSG_STRING, MSG_ITEM_DOCTITLE },
	{ 'c', '/', MSG_STRING, MSG_ITEM_MEDIATYPE },
	{ 'n',  0 , MSG_NUMBER, MSG_ITEM_LINK_NUMBER },

	{  0 ,  0 , 0, 0 }, /* Keep last! */
};


/* The public interface */

/* String item inititialisation is long so it got its own initializer */
static inline struct message_item *
init_string_item(unsigned char *string, int length)
{
	struct message_item *item;

	item = mem_alloc(sizeof(struct message_item));
	if (!item)
		return NULL;

	item->data = memacpy(string, length);
	if (!item->data) {
		mem_free(item);
		return NULL;
	}

	/* Trim %%'s */
	{
		unsigned char *old = item->data;
		unsigned char *new = old;

		while (*old) {
			if (*old == '%' && *(old+1) == '%')
				old++;
			*new++ = *old++;
		}
		*new = '\0';
	}

	item->width = strlen(item->data);
	item->info  = &message_items[0];

	return item;
}

struct message_line *
init_message_line(unsigned char *format_string)
{
	unsigned char *format;
	struct message_line *line;
	int items = 0; /* If 0 after loop => error */

	line = mem_alloc(sizeof(struct message_line));
	if (!line)
		return NULL;

	line->format = format = stracpy(format_string);
	if (!format) {
		mem_free(line);
		return NULL;
	}

	log("format is [%s]", line->format);
	init_list(line->items);

	/* A bit ugly but we have to handle possible ending string item */
	while (*format) {
		struct message_item item;
		unsigned char *start_pos = format;

		log("format 0a is [%s]", format);
		/* Add chars to string item as long as not /%[^%]/ */
		do {
			if (*format == '%' && *(format+1) != '%')
				break;

			format++;
		} while (*format);

		/* Current string item should be added */
		if (start_pos != format) {
			int length = format - start_pos;
			struct message_item *string_item;

			string_item = init_string_item(start_pos, length);
			if (!string_item) {
				items = 0;
				break;
			}

			add_to_list_bottom(line->items, string_item);
			items++;

			/* Handle corner case where string_item has to be
			 * added as the last thing. */
			if (!*format) break;
		}
		log("format 0b is [%s]", format);

		format++;
		if (!*format) break;

		/* From here to end of loop is format items parsed */
		/* Part 1a: Handle width flags */
		log("format 1a is [%s]", format);
		item.flags = 0;
		if (*format == '+') {
			item.flags |= MSG_WIDTH_FILL;
			format++;
		} else if (*format == '0') {
			item.flags |= MSG_NUMBER_PAD;
			format++;
		}

		/* Part 1b: Handle width. */
		log("format 1b is [%s]", format);
		start_pos = format;
		while (*format && isdigit(*format))
			format++;

		if (!*format) break;

		log("start_pos [%s], format [%s]", start_pos, format);
		if (start_pos != format) {
			unsigned char tmp = *format;

			*format = 0;
			item.width = atoi(start_pos);
			log("width [%d]", item.width);
			*format = tmp;
		} else {
			item.width = 0;
		}

		/* Part 2a: Handle item flags. */
		log("format 2a is [%s]", format);
		if (*format == '/') {
			item.flags |= MSG_TRIM_BASE;
			format++;
		} else if (*format == '^') {
			item.flags |= MSG_TRIM_START;
			format++;
		} else if (*format == '$') {
			item.flags |= MSG_TRIM_END;
			format++;
		}

		/* Part 2b: Handle item. */
		/* Look up the id in the table */
		log("format 2b is [%s]", format);
		item.info = NULL;
		{
			int i;

			for (i = 1; message_items[i].id; i++)
				if (message_items[i].id == *format) {
					item.info = &message_items[i];
					break;
				}
		}

		if (item.info) {
			struct message_item *newitem;

			newitem = mem_alloc(sizeof(struct message_item));
			if (!newitem) {
				items = 0;
				break;
			}
			log("found info");

			newitem->info  = item.info;
			newitem->flags = item.flags;
			newitem->width = item.width;
			add_to_list_bottom(line->items, newitem);
			items++;
		}

		format++;
	}

	if (items == 0) {
		/* Something went wrong so clean up. */
		free_message_line(line);
		line = NULL;
	}

	return line;
}


void
free_message_line(struct message_line *line)
{
	while (!list_empty(line->items)) {
		struct message_item *item = line->items.next;

		del_from_list(item);
		if (item->info->type == MSG_STRING
		    && item->info->subtype == MSG_ITEM_STRING)
			mem_free(item->data);
		mem_free(item);
	}

	mem_free(line->format);
	mem_free(line);
}


/* The main string item formatting function.
 *
 * @item	The item to format
 * @msg		The message buffer to copy @string to
 * @msglen	The buffer size that the @
 * @trimchar	If non-NULL the character to trim up till
 * @shrink	0 no shrinking, 0< shrink from start, >0 from start
 * @width	Max width of the string. Ignored if > @msglen
 *
 * If you are working on a new flag feature here is probably where to
 * put it. */
static inline int
add_string_item(struct message_item *item, unsigned char *msg, int len,
		unsigned char *itemstr, unsigned char trimchar)
{
	int width = (item->width > 0 && item->width < len) ? item->width : len;
	int itemlen;

	log("add string: [%s] width [%d]", itemstr, width);
	if (trimchar) {
		unsigned char *trimstring;

		trimstring = strchr(itemstr, trimchar);
		if (trimstring)
			itemstr = trimstring + 1;
	}

	itemlen = strlen(itemstr);

	if (itemlen > width) {
		unsigned char *dotpos;

		/* We can't shrink since there's no room for '...' */
		if (width < 4)
			return 0;

		/* Shrink item and append or prefix '...' */
		if (item->flags & MSG_TRIM_END) {
			dotpos = msg + width - 3;
		} else if (item->flags & MSG_TRIM_START) {
			dotpos = msg;
			msg = dotpos + 3;
			itemstr = itemstr + (itemlen - width) + 3;
			log("trim start: itemstr [%s] - dotpos [%s]", itemstr, dotpos);
		} else {
			/* No shrinking allowed */
			return 0;
		}

		/* Shrink item and append or prefix '...' */
		itemlen = width;

		*dotpos++ = '.';
		*dotpos++ = '.';
		*dotpos   = '.';

		memmove(msg, itemstr, itemlen - 3);
	} else {
		memmove(msg, itemstr, itemlen);

		if (item->flags & MSG_WIDTH_FILL && itemlen < width) {
			unsigned char *fillpos = msg + itemlen;
			int fillwidth = width - itemlen;

			while (fillwidth-- > 0)
				*fillpos++ = ' ';

			itemlen = width;
		}
	}

	return itemlen;
}

static int
add_number_item(struct message_item *item, unsigned char *msg, int len,
		int number, int padding)
{
	int width = (item->width > 0 && item->width < len) ? item->width : len;
	int itemlen = item->width;

	log("add number: [%d] width [%d]", number, width);
	if (longcat(msg, NULL, number, width, padding) != 0)
		return 0;

	/* FIXME Could longcat be made to give length information through slen arg? */
	if (!itemlen) {
		itemlen = 1;

		while (number > 9) {
			itemlen++;
			number /= 10;
		}
	}

	return itemlen;
}

unsigned char *
get_message_line(struct message_line *line, int width)
{
	struct message_item *item;
	unsigned char *message = mem_alloc(width + 1);
	unsigned char *pos;
	int len = width;

	if (!message)
		return NULL;

	pos = message;
	message[width] = '\0';

	foreach (item, line->items) {
		struct message_item_info *info = item->info;
		int itemlen;

		if (info->type == MSG_STRING) {
			unsigned char *str;
			unsigned char trim = (item->flags & MSG_TRIM_BASE);

			if (trim)
				trim = (unsigned char) info->special;

			str = msginfo_string_funcs[info->subtype](item);
			if (!str)
				internal("string null!");

			log("string is [%s]", str);

			itemlen = add_string_item(item, pos, len, str, trim);
		} else if (info->type == MSG_NUMBER) {
			unsigned char pad = (item->flags & MSG_NUMBER_PAD);
			int number;

			if (pad)
				pad = '0';

			number = msginfo_number_funcs[info->subtype](item);
			if (number < 0)
				internal("number negative!");

			log("number is [%d]", number);
			itemlen = add_number_item(item, pos, len, number, pad);
		} else {
			internal("Unknown message info type");
			return NULL;
		}

		len -= itemlen;
		pos += itemlen;
	}

	if (len < 0)
		internal("Oops [%s:%d]", __FILE__, __LINE__);

	if (len > 0)
		while (len-- > 0)
			*pos++ = ' ';

	*pos = '\0';
	return message;
}
