/* 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 "viewcolumns_parser.h"

#include <ctype.h> /* isdigit() */
#include <stddef.h> /* NULL */
#include <stdio.h> /* snprintf() */
#include <stdlib.h> /* free() */
#include <string.h> /* strchr() strdup() strtok_r() */

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

static column_info_t * parse_all(map_name_cb cn, const char str[], size_t *len,
		void *arg);
static void free_info_list(column_info_t *list, size_t list_len);
static void free_info(column_info_t *info);
static int parse(map_name_cb cn, const char str[], column_info_t *info,
		void *arg);
static void load_defaults(column_info_t *info);
static const char * parse_align(const char str[], column_info_t *info);
static const char * parse_width(const char str[], column_info_t *info,
		int *present);
static const char * parse_name(map_name_cb cn, const char str[],
		column_info_t *info, void *arg);
static const char * parse_cropping(const char str[], column_info_t *info,
		int *present);
static int extend_column_list(column_info_t **list, size_t *len);
static void add_all(columns_t *columns, add_column_cb ac,
		const column_info_t *list, size_t len);

int
parse_columns(columns_t *columns, add_column_cb ac, map_name_cb cn,
		const char str[], void *arg)
{
	column_info_t *list;
	size_t list_len;
	if((list = parse_all(cn, str, &list_len, arg)) != NULL)
	{
		add_all(columns, ac, list, list_len);
		free_info_list(list, list_len);
		return 0;
	}
	return 1;
}

/* Parses format string.  Returns list of size *len or NULL on error. */
static column_info_t *
parse_all(map_name_cb cn, const char str[], size_t *len, void *arg)
{
	char *saveptr;
	char *str_copy;
	char *token;
	column_info_t *list = NULL;
	size_t list_len = 0;
	size_t percents = 0;

	str_copy = strdup(str);
	if(str_copy == NULL)
	{
		return NULL;
	}

	saveptr = NULL;
	for(token = str_copy; (token = strtok_r(token, ",", &saveptr)); token = NULL)
	{
		column_info_t info;
		if(parse(cn, token, &info, arg) != 0)
		{
			break;
		}

		if(info.sizing == ST_PERCENT && (percents += info.full_width) > 100)
		{
			free_info(&info);
			break;
		}

		if(extend_column_list(&list, &list_len) == 0)
		{
			list[list_len - 1] = info;
		}
		else
		{
			free_info(&info);
		}
	}
	free(str_copy);

	if(token != NULL)
	{
		free_info_list(list, list_len);
		return NULL;
	}
	else
	{
		*len = list_len;
		return list;
	}
}

/* Frees list of column info structures. */
static void
free_info_list(column_info_t *list, size_t list_len)
{
	if(list == NULL)
	{
		return;
	}

	size_t i;
	for(i = 0U; i < list_len; ++i)
	{
		free_info(&list[i]);
	}
	free(list);
}

/* Frees data of a single column info structure.  The parameter can be NULL. */
static void
free_info(column_info_t *info)
{
	if(info != NULL)
	{
		update_string(&info->literal, NULL);
	}
}

/* Parses single column description.  Returns zero on successful parsing. */
static int
parse(map_name_cb cn, const char str[], column_info_t *info, void *arg)
{
	load_defaults(info);

	int width_present = 0, cropping_present = 0;
	if((str = parse_align(str, info)) != NULL)
	{
		if((str = parse_width(str, info, &width_present)) != NULL)
		{
			if((str = parse_name(cn, str, info, arg)) != NULL)
			{
				str = parse_cropping(str, info, &cropping_present);
			}
		}
	}

	if(info->literal != NULL)
	{
		if(!width_present)
		{
			info->sizing = ST_ABSOLUTE;
			info->text_width = utf8_strsw(info->literal);
			info->full_width = info->text_width;
		}
		if(!cropping_present)
		{
			info->cropping = CT_TRUNCATE;
		}
	}

	/* Either something has failed or we didn't parse format until the end. */
	if(str == NULL || *str != '\0')
	{
		free_info(info);
		return 1;
	}

	return 0;
}

/* Initializes info structure with default values. */
static void
load_defaults(column_info_t *info)
{
	info->literal = NULL;
	info->column_id = FILL_COLUMN_ID;
	info->full_width = -1;
	info->text_width = -1;
	info->align = AT_RIGHT;
	info->sizing = ST_AUTO;
	info->cropping = CT_NONE;
}

/* Parses alignment type part of format string. Returns pointer to next char to
 * parse or NULL on error. */
static const char *
parse_align(const char str[], column_info_t *info)
{
	if(*str == '-')
	{
		info->align = AT_LEFT;
		++str;
	}
	else if(*str == '*')
	{
		info->align = AT_DYN;
		++str;
	}
	else if(*str == '^')
	{
		info->align = AT_MIDDLE;
		++str;
	}
	return str;
}

/* Parses width part of format string.  Always sets *present to indicate whether
 * width field was present.  Returns pointer to next char to parse or NULL on
 * error. */
static const char *
parse_width(const char str[], column_info_t *info, int *present)
{
	const char *orig = str;
	*present = 0;

	if(isdigit(*str))
	{
		char *end;
		info->full_width = strtol(str, &end, 10);
		info->text_width = info->full_width;
		str = end;
		info->sizing = ST_ABSOLUTE;

		if(*str == '%')
		{
			str++;
			if(info->full_width > 100)
			{
				return NULL;
			}
			info->sizing = ST_PERCENT;
		}
		else if(*str == '.')
		{
			char *end;
			if(!isdigit(*++str))
			{
				return NULL;
			}

			info->text_width = strtol(str, &end, 10);
			str = end;
		}
	}

	if(info->full_width < info->text_width ||
			info->full_width*info->text_width == 0)
	{
		return NULL;
	}

	*present = (str != orig);
	return str;
}

/* Parses name part of format string. Returns pointer to next char to parse or
 * NULL on error. */
static const char *
parse_name(map_name_cb cn, const char str[], column_info_t *info, void *arg)
{
	if(*str == '{')
	{
		const char *closing_brace = strchr(str + 1, '}');
		if(closing_brace != NULL)
		{
			char name[16];
			const size_t len = MIN(sizeof(name),
					(size_t)(closing_brace - (str + 1) + 1));
			(void)copy_str(name, len, str + 1);

			if(name[0] == '#')
			{
				info->literal = strdup(name + 1);
				return closing_brace + 1;
			}

			if((info->column_id = cn(name, arg)) >= 0)
			{
				return closing_brace + 1;
			}
		}
	}
	return NULL;
}

/* Parses cropping part of format string.  Always sets *present to indicate
 * whether cropping field was present.  RReturns pointer to next char to parse
 * or NULL on error. */
static const char *
parse_cropping(const char str[], column_info_t *info, int *present)
{
	int dot_count = strspn(str, ".");
	*present = (dot_count > 0);

	switch(dot_count)
	{
		case 1:
			info->cropping = CT_TRUNCATE;
			break;
		case 2:
			info->cropping = CT_ELLIPSIS;
			break;
		case 0:
		case 3:
			info->cropping = CT_NONE;
			break;

		default:
			return NULL;
	}
	return str + dot_count;
}

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

/* Adds all columns by calling ac callback for each column. */
static void
add_all(columns_t *columns, add_column_cb ac, const column_info_t *list,
		size_t len)
{
	size_t i;
	for(i = 0U; i < len; ++i)
	{
		ac(columns, list[i]);
	}
}

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