/* HTML forms parser */

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

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "elinks.h"

#include "bfu/listmenu.h"
#include "bfu/menu.h"
#include "document/html/parser/forms.h"
#include "document/html/parser/link.h"
#include "document/html/parser/stack.h"
#include "document/html/parser/parse.h"
#include "document/html/parser.h"
#include "document/html/renderer.h"
#include "document/forms.h"
#include "intl/charsets.h"
#include "protocol/protocol.h"
#include "protocol/uri.h"
#include "util/conv.h"
#include "util/error.h"
#include "util/memdebug.h"
#include "util/memlist.h"
#include "util/memory.h"
#include "util/string.h"

/* Unsafe macros */
#include "document/html/internal.h"



void
html_form(struct html_context *html_context, unsigned char *a,
          unsigned char *xxx3, unsigned char *xxx4, unsigned char **xxx5)
{
	unsigned char *al;
	struct form *form;

	html_context->was_br = 1;

	form = init_form();
	if (!form) return;

	form->method = FORM_METHOD_GET;
	form->form_num = a - html_context->startf;

	al = get_attr_val(a, "method", html_context->options);
	if (al) {
		if (!c_strcasecmp(al, "post")) {
			unsigned char *enctype;

			enctype  = get_attr_val(a, "enctype",
			                        html_context->options);

			form->method = FORM_METHOD_POST;
			if (enctype) {
				if (!c_strcasecmp(enctype, "multipart/form-data"))
					form->method = FORM_METHOD_POST_MP;
				else if (!c_strcasecmp(enctype, "text/plain"))
					form->method = FORM_METHOD_POST_TEXT_PLAIN;
				mem_free(enctype);
			}
		}
		mem_free(al);
	}

	al = get_attr_val(a, "name", html_context->options);
	if (al) form->name = al;

	al = get_attr_val(a, "action", html_context->options);
	/* The HTML specification at
	 * http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.3 states
	 * that the behavior of an empty action attribute should be undefined.
	 * Mozilla handles action="" as action="<current-URI>" which seems
	 * reasonable. (bug 615) */
	if (al && *al) {
		form->action = join_urls(html_context->base_href, trim_chars(al, ' ', NULL));
		mem_free(al);

	} else {
		enum uri_component components = URI_ORIGINAL;

		mem_free_if(al);

		/* We have to do following for GET method, because we would end
		 * up with two '?' otherwise. */
		if (form->method == FORM_METHOD_GET)
			components = URI_FORM_GET;

		form->action = get_uri_string(html_context->base_href, components);

		/* No action URI should contain post data */
		assert(!form->action || !strchr(form->action, POST_CHAR));

		/* GET method URIs should not have '?'. */
		assert(!form->action
			|| form->method != FORM_METHOD_GET
			|| !strchr(form->action, '?'));
	}

	al = get_target(html_context->options, a);
	form->target = al ? al : stracpy(html_context->base_target);

	html_context->special_f(html_context, SP_FORM, form);
}


static int
get_form_mode(struct html_context *html_context, unsigned char *attr)
{
	if (has_attr(attr, "disabled", html_context->options))
		return FORM_MODE_DISABLED;

	if (has_attr(attr, "readonly", html_context->options))
		return FORM_MODE_READONLY;

	return FORM_MODE_NORMAL;
}

static struct form_control *
init_form_control(enum form_type type, unsigned char *attr,
                  struct html_context *html_context)
{
	struct form_control *fc;

	fc = mem_calloc(1, sizeof(*fc));
	if (!fc) return NULL;

	fc->type = type;
	fc->position = attr - html_context->startf;
	fc->mode = get_form_mode(html_context, attr);

	return fc;
}

void
html_button(struct html_context *html_context, unsigned char *a,
            unsigned char *xxx3, unsigned char *xxx4, unsigned char **xxx5)
{
	unsigned char *al;
	struct form_control *fc;
	enum form_type type = FC_SUBMIT;

	html_focusable(html_context, a);

	al = get_attr_val(a, "type", html_context->options);
	if (!al) goto no_type_attr;

	if (!c_strcasecmp(al, "button")) {
		type = FC_BUTTON;
	} else if (!c_strcasecmp(al, "reset")) {
		type = FC_RESET;
	} else if (c_strcasecmp(al, "submit")) {
		/* unknown type */
		mem_free(al);
		return;
	}
	mem_free(al);

no_type_attr:
	fc = init_form_control(type, a, html_context);
	if (!fc) return;

	fc->name = get_attr_val(a, "name", html_context->options);
	fc->default_value = get_attr_val(a, "value", html_context->options);
	if (!fc->default_value) {
		if (fc->type == FC_SUBMIT)
			fc->default_value = stracpy("Submit");
		else if (fc->type == FC_RESET)
			fc->default_value = stracpy("Reset");
		else if (fc->type == FC_BUTTON)
			fc->default_value = stracpy("Button");
	}
	if (!fc->default_value)
		fc->default_value = stracpy("");

	html_context->special_f(html_context, SP_CONTROL, fc);
	format.form = fc;
	format.style.attr |= AT_BOLD;
}

static void
html_input_format(struct html_context *html_context, unsigned char *a,
	   	  struct form_control *fc)
{
	put_chrs(html_context, " ", 1);
	html_stack_dup(html_context, ELEMENT_KILLABLE);
	html_focusable(html_context, a);
	format.form = fc;
	if (format.title) mem_free(format.title);
	format.title = get_attr_val(a, "title", html_context->options);
	switch (fc->type) {
		case FC_TEXT:
		case FC_PASSWORD:
		case FC_FILE:
		{
			int i;

			format.style.attr |= AT_BOLD;
			for (i = 0; i < fc->size; i++)
				put_chrs(html_context, "_", 1);
			break;
		}
		case FC_CHECKBOX:
			format.style.attr |= AT_BOLD;
			put_chrs(html_context, "[&nbsp;]", 8);
			break;
		case FC_RADIO:
			format.style.attr |= AT_BOLD;
			put_chrs(html_context, "(&nbsp;)", 8);
			break;
		case FC_IMAGE:
		{
			unsigned char *al;

			mem_free_set(&format.image, NULL);
			al = get_url_val(a, "src", html_context->options);
			if (!al)
				al = get_url_val(a, "dynsrc",
				                 html_context->options);
			if (al) {
				format.image = join_urls(html_context->base_href, al);
				mem_free(al);
			}
			format.style.attr |= AT_BOLD;
			put_chrs(html_context, "[&nbsp;", 7);
			if (fc->alt)
				put_chrs(html_context, fc->alt, strlen(fc->alt));
			else if (fc->name)
				put_chrs(html_context, fc->name, strlen(fc->name));
			else
				put_chrs(html_context, "Submit", 6);

			put_chrs(html_context, "&nbsp;]", 7);
			break;
		}
		case FC_SUBMIT:
		case FC_RESET:
		case FC_BUTTON:
			format.style.attr |= AT_BOLD;
			put_chrs(html_context, "[&nbsp;", 7);
			if (fc->default_value)
				put_chrs(html_context, fc->default_value, strlen(fc->default_value));
			put_chrs(html_context, "&nbsp;]", 7);
			break;
		case FC_TEXTAREA:
		case FC_SELECT:
		case FC_HIDDEN:
			INTERNAL("bad control type");
	}
	kill_html_stack_item(html_context, &html_top);
	put_chrs(html_context, " ", 1);
}

void
html_input(struct html_context *html_context, unsigned char *a,
           unsigned char *xxx3, unsigned char *xxx4, unsigned char **xxx5)
{
	unsigned char *al;
	struct form_control *fc;

	fc = init_form_control(FC_TEXT, a, html_context);
	if (!fc) return;

	al = get_attr_val(a, "type", html_context->options);
	if (al) {
		if (!c_strcasecmp(al, "text")) fc->type = FC_TEXT;
		else if (!c_strcasecmp(al, "hidden")) fc->type = FC_HIDDEN;
		else if (!c_strcasecmp(al, "button")) fc->type = FC_BUTTON;
		else if (!c_strcasecmp(al, "checkbox")) fc->type = FC_CHECKBOX;
		else if (!c_strcasecmp(al, "radio")) fc->type = FC_RADIO;
		else if (!c_strcasecmp(al, "password")) fc->type = FC_PASSWORD;
		else if (!c_strcasecmp(al, "submit")) fc->type = FC_SUBMIT;
		else if (!c_strcasecmp(al, "reset")) fc->type = FC_RESET;
		else if (!c_strcasecmp(al, "file")) fc->type = FC_FILE;
		else if (!c_strcasecmp(al, "image")) fc->type = FC_IMAGE;
		/* else unknown type, let it default to FC_TEXT. */
		mem_free(al);
	}

	if (fc->type != FC_FILE)
		fc->default_value = get_attr_val(a, "value",
		                                 html_context->options);
	if (!fc->default_value) {
		if (fc->type == FC_CHECKBOX)
			fc->default_value = stracpy("on");
		else if (fc->type == FC_SUBMIT)
			fc->default_value = stracpy("Submit");
		else if (fc->type == FC_RESET)
			fc->default_value = stracpy("Reset");
		else if (fc->type == FC_BUTTON)
			fc->default_value = stracpy("Button");
	}
	if (!fc->default_value)
		fc->default_value = stracpy("");

	fc->name = get_attr_val(a, "name", html_context->options);

	fc->size = get_num(a, "size", html_context->options);
	if (fc->size == -1)
		fc->size = html_context->options->default_form_input_size;
	fc->size++;
	if (fc->size > html_context->options->box.width)
		fc->size = html_context->options->box.width;
	fc->maxlength = get_num(a, "maxlength", html_context->options);
	if (fc->maxlength == -1) fc->maxlength = INT_MAX;
	if (fc->type == FC_CHECKBOX || fc->type == FC_RADIO)
		fc->default_state = has_attr(a, "checked",
		                             html_context->options);
	if (fc->type == FC_IMAGE)
		fc->alt = get_attr_val(a, "alt", html_context->options);

	if (fc->type != FC_HIDDEN) {
		html_input_format(html_context, a, fc);
	}

	html_context->special_f(html_context, SP_CONTROL, fc);
}

static struct list_menu lnk_menu;

static void
do_html_select(unsigned char *attr, unsigned char *html,
	       unsigned char *eof, unsigned char **end,
	       struct html_context *html_context)
{
	struct conv_table *ct = html_context->special_f(html_context, SP_TABLE, NULL);
	struct form_control *fc;
	struct string lbl = NULL_STRING, orig_lbl = NULL_STRING;
	unsigned char **values = NULL;
	unsigned char **labels;
	unsigned char *name, *t_attr, *en;
	int namelen;
	int nnmi = 0;
	int order = 0;
	int preselect = -1;
	int group = 0;
	int i, max_width;
	int closing_tag;

	html_focusable(html_context, attr);
	init_menu(&lnk_menu);

se:
        en = html;

see:
        html = en;
	while (html < eof && *html != '<') html++;

	if (html >= eof) {

abort:
		*end = html;
		if (lbl.source) done_string(&lbl);
		if (orig_lbl.source) done_string(&orig_lbl);
		if (values) {
			int j;

			for (j = 0; j < order; j++)
				mem_free_if(values[j]);
			mem_free(values);
		}
		destroy_menu(&lnk_menu);
		*end = en;
		return;
	}

	if (lbl.source) {
		unsigned char *q, *s = en;
		int l = html - en;

		while (l && isspace(s[0])) s++, l--;
		while (l && isspace(s[l-1])) l--;
		q = convert_string(ct, s, l,
		                   html_context->options->cp,
		                   CSM_DEFAULT, NULL, NULL, NULL);
		if (q) add_to_string(&lbl, q), mem_free(q);
		add_bytes_to_string(&orig_lbl, s, l);
	}

	if (html + 2 <= eof && (html[1] == '!' || html[1] == '?')) {
		html = skip_comment(html, eof);
		goto se;
	}

	if (parse_element(html, eof, &name, &namelen, &t_attr, &en)) {
		html++;
		goto se;
	}

	if (!namelen) goto see;

	if (name[0] == '/') {
		namelen--;
		if (!namelen) goto see;
		name++;
		closing_tag = 1;
	} else {
		closing_tag = 0;
	}

	if (closing_tag && !c_strlcasecmp(name, namelen, "SELECT", 6)) {
		add_select_item(&lnk_menu, &lbl, &orig_lbl, values, order, nnmi);
		goto end_parse;
	}

	if (!c_strlcasecmp(name, namelen, "OPTION", 6)) {
		add_select_item(&lnk_menu, &lbl, &orig_lbl, values, order, nnmi);

		if (!closing_tag) {
			unsigned char *value, *label;

			if (has_attr(t_attr, "disabled", html_context->options))
				goto see;
			if (preselect == -1
			    && has_attr(t_attr, "selected", html_context->options))
				preselect = order;
			value = get_attr_val(t_attr, "value", html_context->options);

			if (!mem_align_alloc(&values, order, order + 1, unsigned char *, 0xFF))
				goto abort;

			values[order++] = value;
			label = get_attr_val(t_attr, "label", html_context->options);
			if (label) new_menu_item(&lnk_menu, label, order - 1, 0);
			if (!value || !label) {
				init_string(&lbl);
				init_string(&orig_lbl);
				nnmi = !!label;
			}
		}

		goto see;
	}

	if (!c_strlcasecmp(name, namelen, "OPTGROUP", 8)) {
		add_select_item(&lnk_menu, &lbl, &orig_lbl, values, order, nnmi);

		if (group) new_menu_item(&lnk_menu, NULL, -1, 0), group = 0;

		if (!closing_tag) {
			unsigned char *label;

			label = get_attr_val(t_attr, "label", html_context->options);

			if (!label) {
				label = stracpy("");
				if (!label) goto see;
			}
			new_menu_item(&lnk_menu, label, -1, 0);
			group = 1;
		}
	}

	goto see;


end_parse:
	*end = en;
	if (!order) goto abort;

	labels = mem_calloc(order, sizeof(unsigned char *));
	if (!labels) goto abort;

	fc = init_form_control(FC_SELECT, attr, html_context);
	if (!fc) {
		mem_free(labels);
		goto abort;
	}

	fc->name = get_attr_val(attr, "name", html_context->options);
	fc->default_state = preselect < 0 ? 0 : preselect;
	fc->default_value = order ? stracpy(values[fc->default_state]) : stracpy("");
	fc->nvalues = order;
	fc->values = values;
	fc->menu = detach_menu(&lnk_menu);
	fc->labels = labels;

	menu_labels(fc->menu, "", labels);
	put_chrs(html_context, "[", 1);
	html_stack_dup(html_context, ELEMENT_KILLABLE);
	format.form = fc;
	format.style.attr |= AT_BOLD;

	max_width = 0;
	for (i = 0; i < order; i++) {
		if (!labels[i]) continue;
		int_lower_bound(&max_width, strlen(labels[i]));
	}

	for (i = 0; i < max_width; i++)
		put_chrs(html_context, "_", 1);

	kill_html_stack_item(html_context, &html_top);
	put_chrs(html_context, "]", 1);
	html_context->special_f(html_context, SP_CONTROL, fc);
}


static void
do_html_select_multiple(struct html_context *html_context, unsigned char *a,
                        unsigned char *html, unsigned char *eof,
                        unsigned char **end)
{
	unsigned char *al = get_attr_val(a, "name", html_context->options);

	if (!al) return;
	html_focusable(html_context, a);
	html_top.type = ELEMENT_DONT_KILL;
	mem_free_set(&format.select, al);
	format.select_disabled = has_attr(a, "disabled", html_context->options)
	                         ? FORM_MODE_DISABLED
	                         : FORM_MODE_NORMAL;
}

void
html_select(struct html_context *html_context, unsigned char *a,
            unsigned char *html, unsigned char *eof, unsigned char **end)
{
	if (has_attr(a, "multiple", html_context->options))
		do_html_select_multiple(html_context, a, html, eof, end);
	else
		do_html_select(a, html, eof, end, html_context);

}

void
html_option(struct html_context *html_context, unsigned char *a,
            unsigned char *xxx3, unsigned char *xxx4, unsigned char **xxx5)
{
	struct form_control *fc;
	unsigned char *val;

	if (!format.select) return;

	val = get_attr_val(a, "value", html_context->options);
	if (!val) {
		struct string str;
		unsigned char *p, *r;
		unsigned char *name;
		int namelen;

		for (p = a - 1; *p != '<'; p--);

		if (!init_string(&str)) goto end_parse;
		if (parse_element(p, html_context->eoff, NULL, NULL, NULL, &p)) {
			INTERNAL("parse element failed");
			val = str.source;
			goto end_parse;
		}

se:
		while (p < html_context->eoff && isspace(*p)) p++;
		while (p < html_context->eoff && !isspace(*p) && *p != '<') {

sp:
			add_char_to_string(&str, *p ? *p : ' '), p++;
		}

		r = p;
		val = str.source; /* Has to be before the possible 'goto end_parse' */

		while (r < html_context->eoff && isspace(*r)) r++;
		if (r >= html_context->eoff) goto end_parse;
		if (r - 2 <= html_context->eoff && (r[1] == '!' || r[1] == '?')) {
			p = skip_comment(r, html_context->eoff);
			goto se;
		}
		if (parse_element(r, html_context->eoff, &name, &namelen, NULL, &p)) goto sp;
		
		if (namelen < 6) goto se;
		if (name[0] == '/') name++, namelen--;
		
		if (c_strlcasecmp(name, namelen, "OPTION", 6)
		    && c_strlcasecmp(name, namelen, "SELECT", 6)
		    && c_strlcasecmp(name, namelen, "OPTGROUP", 8))
			goto se;
	}

end_parse:
	fc = init_form_control(FC_CHECKBOX, a, html_context);
	if (!fc) {
		mem_free_if(val);
		return;
	}

	fc->name = null_or_stracpy(format.select);
	fc->default_value = val;
	fc->default_state = has_attr(a, "selected", html_context->options);
	fc->mode = has_attr(a, "disabled", html_context->options)
	           ? FORM_MODE_DISABLED
	           : format.select_disabled;

	put_chrs(html_context, " ", 1);
	html_stack_dup(html_context, ELEMENT_KILLABLE);
	format.form = fc;
	format.style.attr |= AT_BOLD;
	put_chrs(html_context, "[ ]", 3);
	kill_html_stack_item(html_context, &html_top);
	put_chrs(html_context, " ", 1);
	html_context->special_f(html_context, SP_CONTROL, fc);
}

void
html_textarea(struct html_context *html_context, unsigned char *attr,
              unsigned char *html, unsigned char *eof, unsigned char **end)
{
	struct form_control *fc;
	unsigned char *p, *t_name, *wrap_attr;
	int t_namelen;
	int cols, rows;
	int i;

	html_focusable(html_context, attr);
	while (html < eof && (*html == '\n' || *html == '\r')) html++;
	p = html;
	while (p < eof && *p != '<') {

pp:
		p++;
	}
	if (p >= eof) {
		*end = eof;
		return;
	}
	if (parse_element(p, eof, &t_name, &t_namelen, NULL, end)) goto pp;
	if (c_strlcasecmp(t_name, t_namelen, "/TEXTAREA", 9)) goto pp;

	fc = init_form_control(FC_TEXTAREA, attr, html_context);
	if (!fc) return;

	fc->name = get_attr_val(attr, "name", html_context->options);
	fc->default_value = memacpy(html, p - html);
	for (p = fc->default_value; p && p[0]; p++) {
		/* FIXME: We don't cope well with entities here. Bugzilla uses
		 * &#13; inside of textarea and we fail miserably upon that
		 * one.  --pasky */
		if (p[0] == '\r') {
			if (p[1] == '\n'
			    || (p > fc->default_value && p[-1] == '\n')) {
				memmove(p, p + 1, strlen(p));
				p--;
			} else {
				p[0] = '\n';
			}
		}
	}

	cols = get_num(attr, "cols", html_context->options);
	if (cols <= 0)
		cols = html_context->options->default_form_input_size;
	cols++; /* Add 1 column, other browsers may have different
		   behavior here (mozilla adds 2) --Zas */
	if (cols > html_context->options->box.width)
		cols = html_context->options->box.width;
	fc->cols = cols;

	rows = get_num(attr, "rows", html_context->options);
	if (rows <= 0) rows = 1;
	if (rows > html_context->options->box.height)
		rows = html_context->options->box.height;
	fc->rows = rows;
	html_context->options->needs_height = 1;

	wrap_attr = get_attr_val(attr, "wrap", html_context->options);
	if (wrap_attr) {
		if (!c_strcasecmp(wrap_attr, "hard")
		    || !c_strcasecmp(wrap_attr, "physical")) {
			fc->wrap = FORM_WRAP_HARD;
		} else if (!c_strcasecmp(wrap_attr, "soft")
			   || !c_strcasecmp(wrap_attr, "virtual")) {
			fc->wrap = FORM_WRAP_SOFT;
		} else if (!c_strcasecmp(wrap_attr, "none")
			   || !c_strcasecmp(wrap_attr, "off")) {
			fc->wrap = FORM_WRAP_NONE;
		}
		mem_free(wrap_attr);

	} else if (has_attr(attr, "nowrap", html_context->options)) {
		fc->wrap = FORM_WRAP_NONE;

	} else {
		fc->wrap = FORM_WRAP_SOFT;
	}

	fc->maxlength = get_num(attr, "maxlength", html_context->options);
	if (fc->maxlength == -1) fc->maxlength = INT_MAX;

	if (rows > 1) ln_break(html_context, 1);
	else put_chrs(html_context, " ", 1);

	html_stack_dup(html_context, ELEMENT_KILLABLE);
	format.form = fc;
	format.style.attr |= AT_BOLD;

	for (i = 0; i < rows; i++) {
		int j;

		for (j = 0; j < cols; j++)
			put_chrs(html_context, "_", 1);
		if (i < rows - 1)
			ln_break(html_context, 1);
	}

	kill_html_stack_item(html_context, &html_top);
	if (rows > 1)
		ln_break(html_context, 1);
	else
		put_chrs(html_context, " ", 1);
	html_context->special_f(html_context, SP_CONTROL, fc);
}
