/*
 * Copyright 2008-2013 Various Authors
 * Copyright 2004 Timo Hirvonen
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

#include "format_print.h"
#include "expr.h"
#include "glob.h"
#include "utils.h"
#include "options.h"
#include "uchar.h"
#include "xmalloc.h"
#include "debug.h"

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

static int width;
static int align_left;
static int pad;
static bool width_is_exact;

static GBUF(cond_buffer);
static GBUF(l_str);
static GBUF(m_str);
static GBUF(r_str);
static struct fp_len str_len = {0, 0, 0};
static int *len = &str_len.llen;
static struct gbuf* str = &l_str;

static void stack_print(char *stack, int stack_len)
{
	int i = 0;

	gbuf_grow(str, width ? width : stack_len);
	char* buf = str->buffer + str->len;

	if (width) {
		if (align_left) {
			while (i < width && stack_len)
				buf[i++] = stack[--stack_len];
			while (i < width)
				buf[i++] = pad;
		} else {
			int pad_len;

			if (stack_len > width)
				stack_len = width;
			pad_len = width - stack_len;
			while (i < pad_len)
				buf[i++] = pad;
			while (i < width)
				buf[i++] = stack[--stack_len];
		}
	} else {
		while (stack_len)
			buf[i++] = stack[--stack_len];
	}
	gbuf_used(str, i);
	*len += i;
}

static void print_num(int num)
{
	char stack[20];
	int i, p;

	if (num < 0) {
		if (width == 0)
			width = 1;
		for (i = 0; i < width; i++)
			gbuf_add_ch(str, '?');
		*len += width;
		return;
	}
	p = 0;
	do {
		stack[p++] = num % 10 + '0';
		num /= 10;
	} while (num);

	stack_print(stack, p);
}

#define DBL_MAX_LEN (20)

static void print_double(double num)
{
	char stack[DBL_MAX_LEN], b[DBL_MAX_LEN];
	int i, p = 0;

	i = snprintf(b, DBL_MAX_LEN, "%g", num) - 1;
	while (i >= 0) {
		stack[p++] = b[i];
		i--;
	}
	stack_print(stack, p);
}

/* print '{,-}{h:,}mm:ss' */
static void print_time(int t)
{
	int h, m, s;
	char stack[32];
	int neg = 0;
	int p = 0;

	if (t < 0) {
		neg = 1;
		t *= -1;
	}
	h = t / 3600;
	t = t % 3600;
	m = t / 60;
	s = t % 60;

	/* put all chars to stack in reverse order ;) */
	stack[p++] = s % 10 + '0';
	stack[p++] = s / 10 + '0';
	stack[p++] = ':';
	stack[p++] = m % 10 + '0';
	if (m / 10 || h || time_show_leading_zero)
		stack[p++] = m / 10 + '0';
	if (h) {
		stack[p++] = ':';
		do {
			stack[p++] = h % 10 + '0';
			h /= 10;
		} while (h);
	}
	if (neg)
		stack[p++] = '-';

	stack_print(stack, p);
}

static void print_str(const char *src)
{
	int str_width = u_str_width(src);

	if (width && width_is_exact) {
		*len += width;
		if (align_left) {
			gbuf_add_ustr(str, src, &width);
			gbuf_set(str, ' ', width);
		} else {
			int s = 0;
			int ws_len = width - str_width;
			if (ws_len < 0) {
				int skip = -ws_len;
				int clipped_mark_len = min_u(u_str_width(clipped_text_internal), width);
				skip += clipped_mark_len;
				gbuf_add_ustr(str, clipped_text_internal, &clipped_mark_len);
				s = u_skip_chars(src, &skip, true);
				/* pad if a wide character caused us to skip too much */
				ws_len = -skip;

			}
			gbuf_set(str, ' ', ws_len);
			gbuf_add_ustr(str, src + s, &width);
		}
	} else {
		if (!width)
			width = str_width;
		*len += width;
		gbuf_add_ustr(str, src, &width);
		*len -= width;
	}
}

static inline int strnequal(const char *a, const char *b, size_t b_len)
{
	return a && (strlen(a) == b_len) && (memcmp(a, b, b_len) == 0);
}

static const struct format_option *find_fopt(const struct format_option *fopts, const char *key)
{
	const struct format_option *fo;
	char ch = strlen(key) == 1 ? *key : 0;
	for (fo = fopts; fo->type != 0; fo++) {
		if ((ch != 0 && fo->ch == ch) || strnequal(fo->str, key, strlen(key))) {
			return fo;
		}
	}
	return NULL;
}

static const char *str_val(const char *key, const struct format_option *fopts, char *buf)
{
	const struct format_option *fo;
	const struct cmus_opt *opt;
	const char *val = NULL;

	fo = find_fopt(fopts, key);
	if (fo && !fo->empty) {
		if (fo->type == FO_STR)
			val = fo->fo_str;
	} else {
		opt = option_find_silent(key);
		if (opt) {
			opt->get(opt->data, buf, OPTION_MAX_SIZE);
			val = buf;
		}
	}
	return val;
}

static int int_val(const char *key, const struct format_option *fopts, char *buf)
{
	const struct format_option *fo;
	int val = -1;

	fo = find_fopt(fopts, key);
	if (fo && !fo->empty) {
		if (fo->type == FO_INT)
			val = fo->fo_int;
	}
	return val;
}

static int format_eval_cond(struct expr* expr, const struct format_option *fopts)
{
	if (!expr)
		return -1;
	enum expr_type type = expr->type;
	const char *key;
	const struct format_option *fo;
	const struct cmus_opt *opt;
	char buf[OPTION_MAX_SIZE];

	if (expr->left) {
		int left = format_eval_cond(expr->left, fopts);

		if (type == EXPR_AND)
			return left && format_eval_cond(expr->right, fopts);
		if (type == EXPR_OR)
			return left || format_eval_cond(expr->right, fopts);
		/* EXPR_NOT */
		return !left;
	}

	key = expr->key;
	if (type == EXPR_STR) {
		const char *val = str_val(key, fopts, buf);
		int res;

		if (!val)
			val = "";
		res = glob_match(&expr->estr.glob_head, val);
		if (expr->estr.op == SOP_EQ)
			return res;
		return !res;
	} else if (type == EXPR_INT) {
		int val = int_val(key, fopts, buf);
		int res = val - expr->eint.val;
		if (val == -1 || expr->eint.val == -1) {
			switch (expr->eid.op) {
			case KOP_EQ:
				return res == 0;
			case KOP_NE:
				return res != 0;
			default:
				return 0;
			}
		}
		return expr_op_to_bool(res, expr->eint.op);
	} else if (type == EXPR_ID) {
		int a = 0, b = 0;
		const char *sa, *sb;
		int res = 0;
		if ((sa = str_val(key, fopts, buf)) && (sb = str_val(expr->eid.key, fopts, buf))) {
			res = strcmp(sa, sb);
			return expr_op_to_bool(res, expr->eid.op);
		} else {
			a = int_val(key, fopts, buf);
			b = int_val(expr->eid.key, fopts, buf);
			res = a - b;
			if (a == -1 || b == -1) {
				switch (expr->eid.op) {
				case KOP_EQ:
					return res == 0;
				case KOP_NE:
					return res != 0;
				default:
					return 0;
				}
			}
			return expr_op_to_bool(res, expr->eid.op);
		}
		return res;
	}
	if (strcmp(key, "stream") == 0) {
		fo = find_fopt(fopts, "filename");
		return fo && is_http_url(fo->fo_str);
	}
	fo = find_fopt(fopts, key);
	if (fo)
		return !fo->empty;
	opt = option_find_silent(key);
	if (opt) {
		opt->get(opt->data, buf, OPTION_MAX_SIZE);
		if (strcmp(buf, "false") != 0 && strlen(buf) != 0)
			return 1;
	}
	return 0;
}

static struct expr *format_parse_cond(const char* format, int size)
{
	gbuf_clear(&cond_buffer);
	gbuf_add_bytes(&cond_buffer, format, size);
	return expr_parse_i(cond_buffer.buffer, "condition contains control characters", 0);
}

static uchar format_skip_cond_expr(const char *format, int *s)
{
	uchar r = 0;
	while (format[*s]) {
		uchar u = u_get_char(format, s);
		if (u == '}' || u == '?') {
			return u;
		}
		if (u != '%') {
			continue;
		}
		u = u_get_char(format, s);
		if (u == '%' || u == '?' || u == '!' || u == '=') {
			continue;
		}
		if (u == '-') {
			u = u_get_char(format, s);
		}
		if (u == '.')
			u = u_get_char(format, s);
		while (isdigit(u)) {
			u = u_get_char(format, s);
		}
		if (u == '{') {
			unsigned level = 1;
			while (level) {
				u = u_get_char(format, s);
				if (u == 0)
					return 0;
				if (u == '}')
					--level;
				if (u != '%')
					continue;
				u = u_get_char(format, s);
				if (u == '%' || u == '?' || u == '!' || u == '=')
					continue;
				if (u == '-')
					u = u_get_char(format, s);
				if (u == '.')
					u = u_get_char(format, s);
				while (isdigit(u))
					u = u_get_char(format, s);
				if (u == 0)
					return 0;
				if (u == '{')
					++level;
			}
		}
	}
	return r;
}

static int format_read_cond(const char *format, int *s, int *a, int *b, int *end)
{
	uchar t = format_skip_cond_expr(format, s);
	if (t != '?')
		return 1;
	*a = *s - 1;
	t = format_skip_cond_expr(format, s);
	if (t == 0)
		return 1;
	if (t == '?') {
		*b = *s - 1;
		t = format_skip_cond_expr(format, s);
		if (t != '}')
			return 1;
	}
	*end = *s - 1;
	return 0;
}

static void format_parse(int str_width, const char *format, const struct format_option *fopts, int f_size);

static void format_parse_if(int str_width, const char *format, const struct format_option *fopts, int *s)
{
	int cond_pos = *s, then_pos = -1, else_pos = -1, end_pos = -1, cond_res = -1;
	BUG_ON(format_read_cond(format, s, &then_pos, &else_pos, &end_pos) != 0);

	struct expr *cond = format_parse_cond(format + cond_pos, then_pos - cond_pos);
	cond_res = format_eval_cond(cond, fopts);
	if (cond)
		expr_free(cond);

	BUG_ON(cond_res < 0);
	if (cond_res) {
		format_parse(str_width, format + then_pos + 1, fopts,
				(else_pos > 0 ? else_pos : end_pos) - then_pos - 1);
	} else if (else_pos > 0) {
		format_parse(str_width, format + else_pos + 1, fopts, end_pos - else_pos - 1);
	}

	*s = end_pos + 1;
}

static void format_parse(int str_width, const char *format, const struct format_option *fopts, int f_size)
{
	int s = 0;

	while (s < f_size) {
		const struct format_option *fo;
		int long_len = 0;
		const char *long_begin = NULL;
		uchar u;

		u = u_get_char(format, &s);
		if (u != '%') {
			gbuf_add_uchar(str, u);
			(*len) += u_char_width(u);
			continue;
		}
		u = u_get_char(format, &s);
		if (u == '%' || u == '?') {
			gbuf_add_ch(str, u);
			++(*len);
			continue;
		}
		if (u == '!') {
			/* middle (priority) text starts */
			str = &m_str;
			len = &str_len.mlen;
			continue;
		}
		if (u == '=') {
			/* right aligned text starts */
			str = &r_str;
			len = &str_len.rlen;
			continue;
		}
		align_left = 0;
		if (u == '-') {
			align_left = 1;
			u = u_get_char(format, &s);
		}
		width_is_exact = true;
		if (u == '.') {
			width_is_exact = false;
			u = u_get_char(format, &s);
		}
		pad = ' ';
		if (u == '0') {
			pad = '0';
			u = u_get_char(format, &s);
		}
		width = 0;
		while (isdigit(u)) {
			/* minimum length of this field */
			width *= 10;
			width += u - '0';
			u = u_get_char(format, &s);
		}
		if (u == '%') {
			width = (width * str_width) / 100.0 + 0.5;
			u = u_get_char(format, &s);
		}
		if (u == '{') {
			long_begin = format + s;
			if (*long_begin == '?') {
				++s;
				format_parse_if(str_width, format, fopts, &s);
				BUG_ON(s > f_size);
				continue;
			}
			while (1) {
				BUG_ON(s >= f_size);
				u = u_get_char(format, &s);
				if (u == '}')
					break;
				long_len++;
			}
		}
		for (fo = fopts; ; fo++) {
			BUG_ON(fo->type == 0);
			if (long_len ? strnequal(fo->str, long_begin, long_len)
				     : (fo->ch == u)) {

				int type = fo->type;

				if (fo->empty) {
					gbuf_set(str, ' ', width);
					*len += width;
				} else if (type == FO_STR) {
					print_str(fo->fo_str);
				} else if (type == FO_INT) {
					print_num(fo->fo_int);
				} else if (type == FO_TIME) {
					print_time(fo->fo_time);
				} else if (type == FO_DOUBLE) {
					print_double(fo->fo_double);
				}
				break;
			}
		}
	}
}

static void format_read(int str_width, const char *format, const struct format_option *fopts)
{
	gbuf_clear(&l_str);
	gbuf_clear(&m_str);
	gbuf_clear(&r_str);
	str_len.llen = 0;
	str_len.mlen = 0;
	str_len.rlen = 0;
	str = &l_str;
	len = &str_len.llen;
	format_parse(str_width, format, fopts, strlen(format));
}

static void format_write(struct gbuf *buf, int str_width)
{
	if (str_width == 0)
		str_width = str_len.llen + str_len.mlen + str_len.rlen + (str_len.rlen > 0);

	/* NOTE: any invalid UTF-8 bytes have already been converted to <xx>
	 *       (ASCII) where x is hex digit
	 */

	if (str_len.llen + str_len.mlen + str_len.rlen <= str_width) {
		/* all fit */
		int ws_len = str_width - (str_len.llen + str_len.mlen + str_len.rlen);

		gbuf_add_bytes(buf, l_str.buffer, l_str.len);
		gbuf_add_bytes(buf, m_str.buffer, m_str.len);
		gbuf_set(buf, ' ', ws_len);
		gbuf_add_bytes(buf, r_str.buffer, r_str.len);
	} else {
		/* keep first character since it's almost always padding */
		int clipped_mark_len = min_u(u_str_width(clipped_text_internal) + 1, str_width);
		int r_space = str_width - clipped_mark_len;
		int r_width = min_i(r_space, str_len.rlen);
		int m_space = r_space - r_width;
		int m_width = min_i(m_space, str_len.mlen);
		int l_space = m_space - m_width;
		int l_width = l_space + clipped_mark_len;
		int r_idx = 0, ws_pad = 0;

		gbuf_add_ustr(buf, l_str.buffer, &l_width);
		ws_pad += l_width;
		gbuf_add_ustr(buf, m_str.buffer, &m_width);
		ws_pad += m_width;

		int r_skip = str_len.rlen - r_width;
		r_idx = u_skip_chars(r_str.buffer, &r_skip, true);
		ws_pad += -r_skip;
		gbuf_set(buf, ' ', ws_pad);
		gbuf_add_bytes(buf, r_str.buffer + r_idx, r_str.len - r_idx);
	}
}

struct fp_len format_print(struct gbuf *buf, int str_width, const char *format, const struct format_option *fopts)
{
	format_read(str_width, format, fopts);

#if DEBUG > 1
	if (str_len.llen > 0) {
		int ul = u_str_width(l_str.buffer);
		if (ul != str_len.llen)
			d_print("L %d != %d: size=%zu '%s'\n", ul, str_len.llen, l_str.len, l_str.buffer);
	}

	if (str_len.rlen > 0) {
		int ul = u_str_width(r_str.buffer);
		if (ul != str_len.rlen)
			d_print("R %d != %d: size=%zu '%s'\n", ul, str_len.rlen, r_str.len, r_str.buffer);
	}
#endif

	format_write(buf, str_width);
	return str_len;
}

static int format_valid_sub(const char *format, const struct format_option *fopts, int f_size);

static int format_valid_if(const char *format, const struct format_option *fopts, int *s)
{
	int cond_pos = *s, then_pos = -1, else_pos = -1, end_pos = -1;
	if (format_read_cond(format, s, &then_pos, &else_pos, &end_pos) != 0)
		return 0;

	struct expr *cond = format_parse_cond(format + cond_pos, then_pos - cond_pos);
	if (cond == NULL)
		return 0;
	expr_free(cond);

	if (!format_valid_sub(format + then_pos + 1, fopts,
				(else_pos > 0 ? else_pos : end_pos) - then_pos - 1))
		return 0;
	if (else_pos > 0)
		if (!format_valid_sub(format + else_pos + 1, fopts, end_pos - else_pos - 1))
			return 0;

	*s = end_pos + 1;
	return 1;
}

static int format_valid_sub(const char *format, const struct format_option *fopts, int f_size)
{
	int s = 0;

	while (s < f_size) {
		uchar u;

		u = u_get_char(format, &s);
		if (u == '%') {
			int pad_zero = 0, long_len = 0;
			const struct format_option *fo;
			const char *long_begin = NULL;

			u = u_get_char(format, &s);
			if (u == '%' || u == '?' || u == '!' || u == '=')
				continue;
			if (u == '-')
				u = u_get_char(format, &s);
			if (u == '.')
				u = u_get_char(format, &s);
			if (u == '0') {
				pad_zero = 1;
				u = u_get_char(format, &s);
			}
			while (isdigit(u))
				u = u_get_char(format, &s);
			if (u == '%')
				u = u_get_char(format, &s);
			if (u == '{') {
				long_begin = format + s;
				if (*long_begin == '?') {
					++s;
					if (!format_valid_if(format, fopts, &s))
						return 0;
					if (s > f_size)
						return 0;
					continue;
				}

				while (1) {
					if (s >= f_size)
						return 0;
					u = u_get_char(format, &s);
					if (u == '}')
						break;
					long_len++;
				}
			}
			for (fo = fopts; fo->type; fo++) {
				if (long_len ? strnequal(fo->str, long_begin, long_len)
					     : (fo->ch == u)) {
					if (pad_zero && !fo->pad_zero)
						return 0;
					break;
				}
			}
			if (! fo->type)
				return 0;
		}
	}
	return 1;
}

int format_valid(const char *format, const struct format_option *fopts)
{
	return format_valid_sub(format, fopts, strlen(format));
}
