/*
 * Copyright 2003 Phil Mellor <monkeyson@users.sourceforge.net>
 * Copyright 2004 James Bursa <bursa@users.sourceforge.net>
 * Copyright 2004 Andrew Timmins <atimmins@blueyonder.co.uk>
 * Copyright 2004 John Tytgat <joty@netsurf-browser.org>
 * Copyright 2005 Adrian Lees <adrianl@users.sourceforge.net>
 *
 * This file is part of NetSurf, http://www.netsurf-browser.org/
 *
 * NetSurf 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; version 2 of the License.
 *
 * NetSurf 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/>.
 */

/** \file
 * HTML form text input handling (implementation)
 */

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

#include "desktop/browser.h"
#include "desktop/gui.h"
#include "desktop/mouse.h"
#include "desktop/scrollbar.h"
#include "desktop/selection.h"
#include "desktop/textinput.h"
#include "render/box.h"
#include "render/font.h"
#include "render/form.h"
#include "render/html_internal.h"
#include "render/layout.h"
#include "render/textinput.h"
#include "utils/log.h"
#include "utils/talloc.h"
#include "utils/utf8.h"
#include "utils/utils.h"

/* Define to enable textinput debug */
#undef TEXTINPUT_DEBUG


static bool textinput_textbox_delete(struct content *c,
		struct box *text_box, unsigned char_offset,
		unsigned utf8_len);

/* Textarea callbacks */
static bool textinput_textarea_callback(struct browser_window *bw,
		uint32_t key, void *p1, void *p2);
static void textinput_textarea_move_caret(struct browser_window *bw,
		void *p1, void *p2);
static bool textinput_textarea_paste_text(struct browser_window *bw,
		const char *utf8, unsigned utf8_len, bool last,
		void *p1, void *p2);

/* Text input callbacks */
static bool textinput_input_callback(struct browser_window *bw,
		uint32_t key, void *p1, void *p2);
static void textinput_input_move_caret(struct browser_window *bw,
		void *p1, void *p2);
static bool textinput_input_paste_text(struct browser_window *bw,
		const char *utf8, unsigned utf8_len, bool last,
		void *p1, void *p2);

#define SPACE_LEN(b) ((b->space == 0) ? 0 : 1)



/**
 * Given the x,y co-ordinates of a point within a textarea, return the
 * TEXT box pointer, and the character and pixel offsets within that
 * box at which the caret should be positioned. (eg. for mouse clicks,
 * drag-and-drop insertions etc)
 *
 * \param  textarea       the textarea being considered
 * \param  x              x ordinate of point
 * \param  y              y ordinate of point
 * \param  pchar_offset   receives the char offset within the TEXT box
 * \param  ppixel_offset  receives the pixel offset within the TEXT box
 * \return pointer to TEXT box
 */

static struct box *textinput_textarea_get_position(struct box *textarea,
		int x, int y, int *pchar_offset, int *ppixel_offset)
{
	/* A textarea is an INLINE_BLOCK containing a single
	 * INLINE_CONTAINER, which contains the text as runs of TEXT
	 * separated by BR. There is at least one TEXT. The first and
	 * last boxes are TEXT. Consecutive BR may not be present. These
	 * constraints are satisfied by using a 0-length TEXT for blank
	 * lines. */

	struct box *inline_container, *text_box;
	plot_font_style_t fstyle;
	size_t char_offset = 0;

	inline_container = textarea->children;

	if (inline_container->y + inline_container->height < y) {
		/* below the bottom of the textarea: place caret at end */
		text_box = inline_container->last;
		assert(text_box->type == BOX_TEXT);
		assert(text_box->text);
		font_plot_style_from_css(text_box->style, &fstyle);
		/** \todo handle errors */
		nsfont.font_position_in_string(&fstyle, text_box->text,
				text_box->length,
				(unsigned int)(x - text_box->x),
				&char_offset, ppixel_offset);
	} else {
		/* find the relevant text box */
		y -= inline_container->y;
		for (text_box = inline_container->children;
				text_box &&
				text_box->y + text_box->height < y;
				text_box = text_box->next)
			;
		for (; text_box && text_box->type != BOX_BR &&
				text_box->y <= y &&
				text_box->x + text_box->width < x;
				text_box = text_box->next)
			;
		if (!text_box) {
			/* past last text box */
			text_box = inline_container->last;
			assert(text_box->type == BOX_TEXT);
			assert(text_box->text);
			font_plot_style_from_css(text_box->style, &fstyle);
			nsfont.font_position_in_string(&fstyle,
					text_box->text,
					text_box->length,
					textarea->width,
					&char_offset,
					ppixel_offset);
		} else {
			/* in a text box */
			if (text_box->type == BOX_BR)
				text_box = text_box->prev;
			else if (y < text_box->y && text_box->prev) {
				if (text_box->prev->type == BOX_BR) {
					assert(text_box->prev->prev);
					text_box = text_box->prev->prev;
				}
				else
					text_box = text_box->prev;
			}
			assert(text_box->type == BOX_TEXT);
			assert(text_box->text);
			font_plot_style_from_css(text_box->style, &fstyle);
			nsfont.font_position_in_string(&fstyle,
					text_box->text,
					text_box->length,
					(unsigned int)(x - text_box->x),
					&char_offset,
					ppixel_offset);
		}
	}

	*pchar_offset = char_offset;

	assert(text_box);
	return text_box;
}


/**
 * Delete some text from a box, or delete the box in its entirety
 *
 * \param  c       html content
 * \param  b       box
 * \param  offset  start offset of text to be deleted (in bytes)
 * \param  length  length of text to be deleted
 * \return true iff successful
 */

static bool textinput_delete_handler(struct content *c, struct box *b,
		int offset, size_t length)
{
	size_t text_length = b->length + SPACE_LEN(b);

	/* only remove if its not the first box */
	if (offset <= 0 && length >= text_length && b->prev != NULL) {
		/* remove the entire box */
		box_unlink_and_free(b);

		return true;
	} else
		return textinput_textbox_delete(c, b, offset,
				min(length, text_length - offset));
}


/**
 * Remove the selected text from a text box and gadget (if applicable)
 *
 * \param  c	The content containing the selection
 * \param  s	The selection to be removed
 */

static void textinput_delete_selection(struct content *c, struct selection *s)
{
	size_t start_offset, end_offset;
	struct box *text_box;
	struct box *end_box;
	struct box *next;
	size_t sel_len;
	int beginning = 0;

	assert(s->defined);

	text_box = selection_get_start(s, &start_offset);
	end_box = selection_get_end(s, &end_offset);
	sel_len = s->end_idx - s->start_idx;

	/* Clear selection so that deletion from textboxes proceeds */
	selection_clear(s, true);

	/* handle first box */
	textinput_delete_handler(c, text_box, start_offset, sel_len);
	if (text_box == end_box)
		return;

	for (text_box = text_box->next; text_box != end_box; text_box = next) {
		next = text_box->next;
		box_unlink_and_free(text_box);
	}

	textinput_delete_handler(c, end_box, beginning, end_offset);
}


/**
 * Insert a number of chars into a text box
 *
 * \param  c            html_content
 * \param  text_box     text box
 * \param  char_offset  offset (bytes) at which to insert text
 * \param  utf8         UTF-8 text to insert
 * \param  utf8_len     length (bytes) of UTF-8 text to insert
 * \return true iff successful
 */

static bool textinput_textbox_insert(struct content *c,
		struct box *text_box, unsigned char_offset, const char *utf8,
		unsigned utf8_len)
{
	html_content *html = (html_content *)c;
	char *text;
	struct box *input = text_box->parent->parent;
	bool hide;

	if (html->bw && html->sel.defined)
		textinput_delete_selection(c, &html->sel);

	/* insert into form gadget (text and password inputs only) */
	if (input->gadget && (input->gadget->type == GADGET_TEXTBOX ||
			input->gadget->type == GADGET_PASSWORD) &&
			input->gadget->value) {
		size_t form_offset = input->gadget->caret_form_offset;
		char *value = realloc(input->gadget->value,
				input->gadget->length + utf8_len + 1);
		if (!value) {
			warn_user("NoMemory", 0);
			return true;
		}
		input->gadget->value = value;

		memmove(input->gadget->value + form_offset + utf8_len,
				input->gadget->value + form_offset,
				input->gadget->length - form_offset);
		memcpy(input->gadget->value + form_offset, utf8, utf8_len);
		input->gadget->length += utf8_len;
		input->gadget->value[input->gadget->length] = 0;
	}

	hide = (input->gadget && input->gadget->type == GADGET_PASSWORD);
	if (hide) {
		/* determine the number of '*'s to be inserted */
		const char *eutf8 = utf8 + utf8_len;
		utf8_len = 0;
		while (utf8 < eutf8) {
			utf8 += utf8_next(utf8, eutf8 - utf8, 0);
			utf8_len++;
		}
	}

	/* insert in text box */
	text = talloc_realloc(c, text_box->text,
			char,
			text_box->length + SPACE_LEN(text_box) + utf8_len + 1);
	if (!text) {
		warn_user("NoMemory", 0);
		return false;
	}
	text_box->text = text;

	if (text_box->space != 0 &&
			char_offset == text_box->length + SPACE_LEN(text_box)) {
		if (hide)
			text_box->space = 0;
		else {
			unsigned int last_off = utf8_prev(utf8, utf8_len);
			if (utf8[last_off] != ' ')
				text_box->space = 0;
			else
				utf8_len = last_off;
		}
		text_box->text[text_box->length++] = ' ';
	} else {
		memmove(text_box->text + char_offset + utf8_len,
				text_box->text + char_offset,
				text_box->length - char_offset);
	}

	if (hide)
		memset(text_box->text + char_offset, '*', utf8_len);
	else
		memcpy(text_box->text + char_offset, utf8, utf8_len);
	text_box->length += utf8_len;

	/* nothing should assume that the text is terminated,
	 * but just in case */
	text_box->text[text_box->length] = 0;

	text_box->width = UNKNOWN_WIDTH;

	return true;
}

/**
 * Calculates the form_offset from the box_offset
 *
 * \param input		The root box containing both the textbox and gadget
 * \param text_box   	The textbox containing the caret
 * \param char_offset   The caret offset within text_box
 * \return the translated form_offset
 */

static size_t textinput_get_form_offset(struct box* input, struct box* text_box,
		size_t char_offset)
{
	int uchars;
	unsigned int offset;

	for (uchars = 0, offset = 0; offset < char_offset; uchars++) {
		if ((text_box->text[offset] & 0x80) == 0x00) {
			offset++;
			continue;
		}
		assert((text_box->text[offset] & 0xC0) == 0xC0);
		for (++offset; offset < char_offset &&
				(text_box->text[offset] & 0xC0) == 0x80;
				offset++)
			/* do nothing */;
	}
	/* uchars is the number of real Unicode characters at the left
	 * side of the caret.
	 */
	for (offset = 0; uchars > 0 && offset < input->gadget->length;
			uchars--) {
		if ((input->gadget->value[offset] & 0x80) == 0x00) {
			offset++;
			continue;
		}
		assert((input->gadget->value[offset] & 0xC0) == 0xC0);
		for (++offset; offset < input->gadget->length &&
			(input->gadget->value[offset] & 0xC0) == 0x80;
				offset++)
			/* do nothing */;
	}
	assert(uchars == 0);
	return offset;
}


/**
 * Delete a number of chars from a text box
 *
 * \param  c            html content
 * \param  text_box     text box
 * \param  char_offset  offset within text box (bytes) of first char to delete
 * \param  utf8_len     length (bytes) of chars to be deleted
 * \return true on success, false otherwise
 *
 * ::char_offset and ::utf8_len are only considered when there is no selection.
 * If there is a selection, the entire selected area is deleted.
 */

bool textinput_textbox_delete(struct content *c, struct box *text_box,
		unsigned char_offset, unsigned utf8_len)
{
	html_content *html = (html_content *)c;
	unsigned next_offset = char_offset + utf8_len;
	struct box *form = text_box->parent->parent;

	if (html->bw && html->sel.defined) {
		textinput_delete_selection(c, &html->sel);
		return true;
	}

	/* delete from form gadget (text and password inputs only) */
	if (form->gadget && (form->gadget->type == GADGET_TEXTBOX ||
			form->gadget->type == GADGET_PASSWORD) &&
			form->gadget->value) {
		size_t form_offset = textinput_get_form_offset(form, text_box,
						char_offset);
		size_t next_offset = textinput_get_form_offset(form, text_box,
						char_offset + utf8_len);

		memmove(form->gadget->value + form_offset,
				form->gadget->value + next_offset,
				form->gadget->length - next_offset);
		form->gadget->length -= (next_offset - form_offset);
		form->gadget->value[form->gadget->length] = 0;
	}

	/* delete from visible textbox */
	if (next_offset <= text_box->length + SPACE_LEN(text_box)) {
		/* handle removal of trailing space */
		if (text_box->space != 0 && next_offset > text_box->length) {
			if (char_offset > 0) {
				/* is the trailing character still a space? */
				int tmp = utf8_prev(text_box->text, char_offset);
				if (isspace(text_box->text[tmp]))
					char_offset = tmp;
				else
					text_box->space = 0;
			} else {
				text_box->space = 0;
			}

			text_box->length = char_offset;
		} else {
			memmove(text_box->text + char_offset,
					text_box->text + next_offset,
					text_box->length - next_offset);
			text_box->length -= utf8_len;
		}

		/* nothing should assume that the text is terminated,
		 * but just in case */
		text_box->text[text_box->length] = 0;

		text_box->width = UNKNOWN_WIDTH;

		return true;
	}

	return false;
}

/**
 * Locate the first inline box at the start of this line
 *
 * \param  text_box  text box from which to start searching
 */

static struct box *textinput_line_start(struct box *text_box)
{
	while (text_box->prev && text_box->prev->type == BOX_TEXT)
		text_box = text_box->prev;
	return text_box;
}


/**
 * Locate the last inline box in this line
 *
 * \param  text_box  text box from which to start searching
 */

static struct box *textinput_line_end(struct box *text_box)
{
	while (text_box->next && text_box->next->type == BOX_TEXT)
		text_box = text_box->next;
	return text_box;
}


/**
 * Backtrack to the start of the previous line, if there is one.
 */

static struct box *textinput_line_above(struct box *text_box)
{
	struct box *prev;

	text_box = textinput_line_start(text_box);

	prev = text_box->prev;
	while (prev && prev->type == BOX_BR)
		prev = prev->prev;

	return prev ? textinput_line_start(prev) : text_box;
}


/**
 * Advance to the start of the next line, if there is one.
 */

static struct box *textinput_line_below(struct box *text_box)
{
	struct box *next;

	text_box = textinput_line_end(text_box);

	next = text_box->next;
	while (next && next->type == BOX_BR)
		next = next->next;

	return next ? next : text_box;
}


/**
 * Cut a range of text from a text box,
 * possibly placing it on the global clipboard.
 *
 * \param  c          html content
 * \param  start_box  text box at start of range
 * \param  start_idx  index (bytes) within start box
 * \param  end_box    text box at end of range
 * \param  end_idx    index (bytes) within end box
 * \param  clipboard  whether to place text on the clipboard
 * \return true iff successful
 */

static bool textinput_textarea_cut(struct content *c,
		struct box *start_box, unsigned start_idx,
		struct box *end_box, unsigned end_idx,
		bool clipboard)
{
	struct box *box = start_box;
	bool success = true;
	bool del = false;	/* caller expects start_box to persist */

	if (clipboard && !gui_empty_clipboard())
		return false;

	while (box && box != end_box) {
		/* read before deletion, in case the whole box goes */
		struct box *next = box->next;

		if (box->type == BOX_BR) {
			if (clipboard &&
					!gui_add_to_clipboard("\n", 1, false)) {
				gui_commit_clipboard();
				return false;
			}
			box_unlink_and_free(box);
		} else {
			/* append box text to clipboard and then delete it */
			if (clipboard &&
				!gui_add_to_clipboard(box->text + start_idx,
					box->length - start_idx,
					SPACE_LEN(box))) {
				gui_commit_clipboard();
				return false;
			}

			if (del) {
				if (!textinput_delete_handler(c, box,
						start_idx,
						(box->length + SPACE_LEN(box)) -
						start_idx) && clipboard) {
					gui_commit_clipboard();
					return false;
				}
			} else {
				textinput_textbox_delete(c, box, start_idx,
					(box->length + SPACE_LEN(box)) -
					start_idx);
			}
		}

		del = true;
		start_idx = 0;
		box = next;
	}

	/* and the last box */
	if (box) {
		if (clipboard && !gui_add_to_clipboard(box->text + start_idx,
				end_idx - start_idx, end_idx > box->length)) {
			success = false;
		} else {
			if (del) {
				if (!textinput_delete_handler(c, box,
						start_idx, end_idx - start_idx))
					success = false;
			} else {
				textinput_textbox_delete(c, box, start_idx,
						end_idx - start_idx);
			}
		}
	}

	if (clipboard && !gui_commit_clipboard())
		success = false;

	return success;
}


/**
 * Break a text box into two
 *
 * \param  c            html content
 * \param  text_box     text box to be split
 * \param  char_offset  offset (in bytes) at which text box is to be split
 */

static struct box *textinput_textarea_insert_break(struct content *c,
		struct box *text_box, size_t char_offset)
{
	struct box *new_br, *new_text;
	char *text;

	text = talloc_array(c, char, text_box->length + 1);
	if (!text) {
		warn_user("NoMemory", 0);
		return NULL;
	}

	new_br = box_create(NULL, text_box->style, false, 0, 0, text_box->title,
			0, c);
	new_text = talloc(c, struct box);
	if (!new_text) {
		warn_user("NoMemory", 0);
		return NULL;
	}

	new_br->type = BOX_BR;
	box_insert_sibling(text_box, new_br);

	memcpy(new_text, text_box, sizeof (struct box));
	new_text->flags |= CLONE;
	new_text->text = text;
	memcpy(new_text->text, text_box->text + char_offset,
			text_box->length - char_offset);
	new_text->length = text_box->length - char_offset;
	text_box->length = char_offset;
	text_box->width = new_text->width = UNKNOWN_WIDTH;
	box_insert_sibling(new_br, new_text);

	return new_text;
}


/**
 * Reflow textarea preserving width and height
 *
 * \param  c                 html content
 * \param  textarea          text area box
 * \param  inline_container  container holding text box
 */

static void textinput_textarea_reflow(struct content *c,
		struct box *textarea, struct box *inline_container)
{
	int width = textarea->width;
	int height = textarea->height;

	assert(c != NULL);

	if (!layout_inline_container(inline_container, width,
			textarea, 0, 0, (struct html_content *) c))
		warn_user("NoMemory", 0);
	textarea->width = width;
	textarea->height = height;
	layout_calculate_descendant_bboxes(textarea);
	box_handle_scrollbars(c, textarea,
			box_hscrollbar_present(textarea),
			box_vscrollbar_present(textarea));
}


/**
 * Move to the start of the word containing the given character position,
 * or the start of the preceding word if already at the start of this one.
 *
 * \param  text     UTF-8 text string
 * \param  poffset  offset of caret within string (updated on exit)
 * \param  pchars   receives the number of characters skipped
 * \return true iff the start of a word was found before/at the string start
 */

static bool textinput_word_left(const char *text,
		size_t *poffset, size_t *pchars)
{
	size_t offset = *poffset;
	bool success = false;
	size_t nchars = 0;

	/* Skip any spaces immediately prior to the offset */
	while (offset > 0) {
		offset = utf8_prev(text, offset);
		nchars++;
		if (!isspace(text[offset])) break;
	}

	/* Now skip all non-space characters */
	while (offset > 0) {
		size_t prev = utf8_prev(text, offset);
		success = true;
		if (isspace(text[prev]))
			break;
		offset = prev;
		nchars++;
	}

	*poffset = offset;
	if (pchars) *pchars = nchars;

	return success;
}


/**
 * Move to the start of the first word following the given character position.
 *
 * \param  text     UTF-8 text string
 * \param  len      length of string in bytes
 * \param  poffset  offset of caret within string (updated on exit)
 * \param  pchars   receives the number of characters skipped
 * \return true iff the start of a word was found before the string end
 */

static bool textinput_word_right(const char *text, size_t len,
		size_t *poffset, size_t *pchars)
{
	size_t offset = *poffset;
	bool success = false;
	size_t nchars = 0;

	/* Skip all non-space characters after the offset */
	while (offset < len) {
		if (isspace(text[offset])) break;
		offset = utf8_next(text, len, offset);
		nchars++;
	}

	/* Now skip all space characters */
	while (offset < len) {
		offset = utf8_next(text, len, offset);
		nchars++;
		if (offset < len && !isspace(text[offset])) {
			success = true;
			break;
		}
	}

	*poffset = offset;
	if (pchars) *pchars = nchars;

	return success;
}

/**
 * Adjust scroll offsets so that the caret is visible
 *
 * \param c         html content where click ocurred
 * \param textarea  textarea box
 * \return true if a change in scroll offsets has occurred
*/

static bool textinput_ensure_caret_visible(struct content *c,
		struct box *textarea)
{
	html_content *html = (html_content *)c;
	int cx, cy;
	int scrollx, scrolly;

	assert(textarea->gadget);

	scrollx = scrollbar_get_offset(textarea->scroll_x);
	scrolly = scrollbar_get_offset(textarea->scroll_y);

	/* Calculate the caret coordinates */
	cx = textarea->gadget->caret_pixel_offset +
			textarea->gadget->caret_text_box->x;
	cy = textarea->gadget->caret_text_box->y;

	/* Ensure they are visible */
	if (textarea->scroll_x == NULL) {
		scrollx = 0;
	} else if (cx - scrollbar_get_offset(textarea->scroll_x) < 0) {
		scrollx = cx;
	} else if (cx > scrollbar_get_offset(textarea->scroll_x) +
			textarea->width) {
		scrollx = cx - textarea->width;
	}

	if (textarea->scroll_y == NULL) {
		scrolly = 0;
	} else if (cy - scrollbar_get_offset(textarea->scroll_y) < 0) {
		scrolly = cy;
	} else if (cy + textarea->gadget->caret_text_box->height >
			scrollbar_get_offset(textarea->scroll_y) +
			textarea->height) {
		scrolly = (cy + textarea->gadget->caret_text_box->height) -
				textarea->height;
	}

	if ((scrollx == scrollbar_get_offset(textarea->scroll_x)) &&
			(scrolly == scrollbar_get_offset(textarea->scroll_y)))
		return false;

	if (textarea->scroll_x != NULL)	{
		html->scrollbar = textarea->scroll_x;
		scrollbar_set(textarea->scroll_x, scrollx, false);
		html->scrollbar = NULL;
	}
	if (textarea->scroll_y != NULL) {
		html->scrollbar = textarea->scroll_x;
		scrollbar_set(textarea->scroll_y, scrolly, false);
		html->scrollbar = NULL;
	}

	return true;
}


/**
 * Paste a block of text into a textarea at the
 * current caret position.
 *
 * \param  bw        browser window
 * \param  utf8      pointer to block of text
 * \param  utf8_len  length (bytes) of text block
 * \param  last      true iff this is the last chunk (update screen too)
 * \param  p1        pointer to textarea
 * \param  p2        html content with the text area box
 * \return true iff successful
 */

bool textinput_textarea_paste_text(struct browser_window *bw,
		const char *utf8, unsigned utf8_len, bool last,
		void *p1, void *p2)
{
	struct box *textarea = p1;
	struct content *c = p2;
	struct box *inline_container =
			textarea->gadget->caret_inline_container;
	struct box *text_box = textarea->gadget->caret_text_box;
	size_t char_offset = textarea->gadget->caret_box_offset;
	int pixel_offset = textarea->gadget->caret_pixel_offset;
	const char *ep = utf8 + utf8_len;
	const char *p = utf8;
	bool success = true;
	bool update = last;

	while (p < ep) {
		struct box *new_text;
		unsigned utf8_len;

		while (p < ep) {
			if (*p == '\n' || *p == '\r') break;
			p++;
		}

		utf8_len = p - utf8;
		if (!textinput_textbox_insert(c, text_box, char_offset,
				utf8, utf8_len))
			return false;

		char_offset += utf8_len;
		if (p == ep)
			break;

		new_text = textinput_textarea_insert_break(c, text_box,
				char_offset);
		if (!new_text) {
			/* we still need to update the screen */
			update = true;
			success = false;
			break;
		}

		/* place caret at start of new text box */
		text_box = new_text;
		char_offset = 0;

		/* handle CR/LF and LF/CR terminations */
		if ((*p == '\n' && p[1] == '\r') ||
				(*p == '\r' && p[1] == '\n'))
			 p++;
		utf8 = ++p;
	}

//	textarea->gadget->caret_inline_container = inline_container;
	textarea->gadget->caret_text_box = text_box;
	textarea->gadget->caret_box_offset = char_offset;

	if (update) {
		int box_x, box_y;
		plot_font_style_t fstyle;

		/* reflow textarea preserving width and height */
		textinput_textarea_reflow(c, textarea, inline_container);
		/* reflowing may have broken our caret offset
		 * this bit should hopefully continue to work if
		 * textarea_reflow is fixed to update the caret itself */
		char_offset = textarea->gadget->caret_box_offset;
		text_box = textarea->gadget->caret_text_box;

		while ((char_offset > text_box->length + SPACE_LEN(text_box)) &&
				(text_box->next) &&
				(text_box->next->type == BOX_TEXT)) {
#ifdef TEXTINPUT_DEBUG
			LOG(("Caret out of range: Was %d in boxlen %d "
					"space %d", char_offset,
					text_box->length, SPACE_LEN(text_box)));
#endif
			char_offset -= text_box->length + SPACE_LEN(text_box);
			text_box = text_box->next;
		}

		/* not sure if this will happen or not...
		 * but won't stick an assert here as we can recover from it */
		if (char_offset > text_box->length) {
#ifdef TEXTINPUT_DEBUG
			LOG(("Caret moved beyond end of line: "
				"Was %d in boxlen %d", char_offset,
				text_box->length));
#endif
			char_offset = text_box->length;
		}

		textarea->gadget->caret_text_box = text_box;
		textarea->gadget->caret_box_offset = char_offset;

		font_plot_style_from_css(text_box->style, &fstyle);

		nsfont.font_width(&fstyle, text_box->text,
				char_offset, &pixel_offset);

		textarea->gadget->caret_pixel_offset = pixel_offset;

		box_coords(textarea, &box_x, &box_y);
		box_x += scrollbar_get_offset(textarea->scroll_x);
		box_y += scrollbar_get_offset(textarea->scroll_y);
		textinput_ensure_caret_visible(c, textarea);
		box_x -= scrollbar_get_offset(textarea->scroll_x);
		box_y -= scrollbar_get_offset(textarea->scroll_y);

		browser_window_place_caret(bw,
				box_x + inline_container->x + text_box->x +
				pixel_offset,
				box_y + inline_container->y + text_box->y,
				text_box->height,
				textinput_textarea_callback,
				textinput_textarea_paste_text,
				textinput_textarea_move_caret,
				textarea, c);

		html__redraw_a_box(c, textarea);
	}

	return success;
}


/**
 * Move caret to new position after reformatting
 *
 * \param  bw   browser window
 * \param  p1   pointer textarea box
 * \param  p2   html content with the text area box
 * \return none
 */

void textinput_textarea_move_caret(struct browser_window *bw,
		void *p1, void *p2)
{
	struct box *textarea = p1;
	struct content *c = p2;
	struct box *inline_container = textarea->gadget->caret_inline_container;
	struct box *text_box = textarea->gadget->caret_text_box;
	size_t char_offset = textarea->gadget->caret_box_offset;
	int pixel_offset;
	int box_x, box_y;
	plot_font_style_t fstyle;

	font_plot_style_from_css(text_box->style, &fstyle);

	box_coords(textarea, &box_x, &box_y);
	box_x -= scrollbar_get_offset(textarea->scroll_x);
	box_y -= scrollbar_get_offset(textarea->scroll_y);

	nsfont.font_width(&fstyle, text_box->text,
			char_offset, &pixel_offset);

	browser_window_place_caret(bw,
			box_x + inline_container->x + text_box->x +
			pixel_offset,
			box_y + inline_container->y + text_box->y,
			text_box->height,
			textinput_textarea_callback,
			textinput_textarea_paste_text,
			textinput_textarea_move_caret,
			textarea, c);
}


/**
 * Update display to reflect modified input field
 *
 * \param  bw           browser window
 * \param  input        input field
 * \param  form_offset
 * \param  box_offset   offset of caret within text box
 * \param  to_textarea  caret is to be moved to a textarea
 * \param  redraw       force redraw even if field hasn't scrolled
 */

static void textinput_input_update_display(struct content *c, struct box *input,
		unsigned box_offset, bool to_textarea, bool redraw)
{
	struct box *text_box = input->children->children;
	unsigned pixel_offset;
	int box_x, box_y;
	int dx;
	plot_font_style_t fstyle;
	html_content *html = (html_content *)c;

	font_plot_style_from_css(text_box->style, &fstyle);

	if (redraw)
		nsfont.font_width(&fstyle, text_box->text, text_box->length,
			&text_box->width);

	box_coords(input, &box_x, &box_y);

	nsfont.font_width(&fstyle, text_box->text, box_offset,
			(int *) &pixel_offset);

	/* Shift text box horizontally, so caret is visible */
	dx = text_box->x;
	text_box->x = 0;
	if (input->width < text_box->width &&
			input->width / 2 < (int) pixel_offset) {
		/* Make caret appear in centre of text input */
		text_box->x = input->width / 2 - pixel_offset;
		/* Clamp if we've shifted too far left */
		if (text_box->x < input->width - text_box->width)
			text_box->x = input->width - text_box->width;
	}
	dx -= text_box->x;
	input->gadget->caret_pixel_offset = pixel_offset;

	if (to_textarea) {
		/* moving to textarea so need to set these up */
		input->gadget->caret_inline_container = input->children;
		input->gadget->caret_text_box = text_box;
	}

	input->gadget->caret_box_offset = box_offset;

	browser_window_place_caret(html->bw,
			box_x + input->children->x +
					text_box->x + pixel_offset,
			box_y + input->children->y + text_box->y,
			text_box->height,
			/* use the appropriate callback */
			to_textarea ? textinput_textarea_callback
					: textinput_input_callback,
			to_textarea ? textinput_textarea_paste_text
					: textinput_input_paste_text,
			to_textarea ? textinput_textarea_move_caret
					: textinput_input_move_caret,
			input, c);

	if (dx || redraw)
		html__redraw_a_box(c, input);
}


/**
 * Key press callback for text areas.
 *
 * \param bw   The browser window containing the text area
 * \param key  The ucs4 character codepoint
 * \param p1   The text area box
 * \param p2   The html content with the text area box
 * \return     true if the keypress is dealt with, false otherwise.  It can
 *             return true even if it ran out of memory; this just means that
 *             it would have claimed it if it could.
 */
bool textinput_textarea_callback(struct browser_window *bw, uint32_t key,
		void *p1, void *p2)
{
	struct box *textarea = p1;
	struct content *c = p2;
	html_content *html = (html_content *)c;
	struct box *inline_container =
				textarea->gadget->caret_inline_container;
	struct box *text_box = textarea->gadget->caret_text_box;
	struct box *new_text;
	size_t char_offset = textarea->gadget->caret_box_offset;
	int pixel_offset = textarea->gadget->caret_pixel_offset;
	int box_x, box_y;
	char utf8[6];
	unsigned int utf8_len;
	bool scrolled, reflow = false;
	bool selection_exists = html->sel.defined;
	plot_font_style_t fstyle;

	/* box_dump(textarea, 0); */
#ifdef TEXTINPUT_DEBUG
	LOG(("key %i at %i in '%.*s'", key, char_offset,
			(int) text_box->length, text_box->text));
#endif

	box_coords(textarea, &box_x, &box_y);
	box_x -= scrollbar_get_offset(textarea->scroll_x);
	box_y -= scrollbar_get_offset(textarea->scroll_y);

	if (!(key <= 0x001F || (0x007F <= key && key <= 0x009F))) {
		/* normal character insertion */
		utf8_len = utf8_from_ucs4(key, utf8);

		if (!textinput_textbox_insert(c, text_box, char_offset,
				utf8, utf8_len))
			return true;

		char_offset += utf8_len;
		reflow = true;

	} else switch (key) {
	case KEY_DELETE_LEFT:
		if (selection_exists) {
			/* Have a selection; delete it */
			textinput_textbox_delete(c, text_box, 0, 0);
		} else if (char_offset == 0) {
			/* at the start of a text box */
			struct box *prev;

			if (text_box->prev && text_box->prev->type == BOX_BR) {
				/* previous box is BR: remove it */
				box_unlink_and_free(text_box->prev);
			}

			/* This needs to be after the BR removal, as that may
			 * result in no previous box existing */
			if (!text_box->prev)
				/* at very beginning of text area: ignore */
				return true;

			/* delete space by merging with previous text box */
			prev = text_box->prev;
			assert(prev->type == BOX_TEXT);
			assert(prev->text);

			char_offset = prev->length;	/* caret at join */

			if (!textinput_textbox_insert(c, prev, prev->length,
					text_box->text, text_box->length))
				return true;

			box_unlink_and_free(text_box);

			/* place caret at join (see above) */
			text_box = prev;

		} else {
			/* delete a character */
			size_t prev_offset = char_offset;
			size_t new_offset =
					utf8_prev(text_box->text, char_offset);

			if (textinput_textbox_delete(c, text_box, new_offset,
					prev_offset - new_offset))
				char_offset = new_offset;
		}
		reflow = true;
		break;

	case KEY_DELETE_LINE_START:
	{
		struct box *start_box = textinput_line_start(text_box);

		/* Clear the selection, if one exists */
		if (selection_exists)
			selection_clear(&html->sel, false);

		textinput_textarea_cut(c, start_box, 0, text_box,
				char_offset, false);
		text_box = start_box;
		char_offset = 0;
		reflow = true;
	}
		break;

	case KEY_DELETE_LINE_END:
	{
		struct box *end_box = textinput_line_end(text_box);

		/* Clear the selection, if one exists */
		if (selection_exists)
			selection_clear(&html->sel, false);

		if (end_box != text_box ||
			char_offset < text_box->length + SPACE_LEN(text_box)) {
			/* there's something at the end of the line to delete */
			textinput_textarea_cut(c, text_box,
					char_offset, end_box,
					end_box->length + SPACE_LEN(end_box),
					false);
			reflow = true;
			break;
		}
	}
	/* no break */
	case KEY_DELETE_RIGHT:	/* delete to right */
		if (selection_exists) {
			/* Delete selection */
			textinput_textbox_delete(c, text_box, 0, 0);
		} else if (char_offset >= text_box->length) {
			/* at the end of a text box */
			struct box *next;

			if (text_box->next && text_box->next->type == BOX_BR) {
				/* next box is a BR: remove it */
				box_unlink_and_free(text_box->next);
			}

			/* This test is after the BR removal, as that may
			 * result in no subsequent box being present */
			if (!text_box->next)
				/* at very end of text area: ignore */
				return true;

			/* delete space by merging with next text box */

			next = text_box->next;
			assert(next->type == BOX_TEXT);
			assert(next->text);

			if (!textinput_textbox_insert(c, text_box,
					text_box->length,
					next->text, next->length))
				return true;

			box_unlink_and_free(next);

			/* leave caret at join */
		} else {
			/* delete a character */
			size_t next_offset = utf8_next(text_box->text,
					text_box->length, char_offset);

			textinput_textbox_delete(c, text_box, char_offset,
					next_offset - char_offset);
		}
		reflow = true;
		break;

	case KEY_NL:
	case KEY_CR:	/* paragraph break */
		if (selection_exists) {
			/* If we have a selection, then delete it,
			 * so it's replaced by the break */
			textinput_textbox_delete(c, text_box, 0, 0);
		}

		new_text = textinput_textarea_insert_break(c, text_box,
				char_offset);
		if (!new_text)
			return true;

		/* place caret at start of new text box */
		text_box = new_text;
		char_offset = 0;

		reflow = true;
		break;

	case KEY_CUT_LINE:
	{
		struct box *start_box = textinput_line_start(text_box);
		struct box *end_box = textinput_line_end(text_box);

		/* Clear the selection, if one exists */
		if (selection_exists)
			selection_clear(&html->sel, false);

		textinput_textarea_cut(c, start_box, 0, end_box,
				end_box->length, false);

		text_box = start_box;
		char_offset = 0;
		reflow = true;
	}
		break;

	case KEY_PASTE:
		gui_paste_from_clipboard(bw->window,
			box_x + inline_container->x +
					text_box->x + pixel_offset,
			box_y + inline_container->y + text_box->y);

		/* screen updated and caret repositioned already */
		return true;

	case KEY_CUT_SELECTION:
	{
		size_t start_idx, end_idx;
		struct box *start_box =
				selection_get_start(&html->sel, &start_idx);
		struct box *end_box = selection_get_end(&html->sel, &end_idx);

		if (start_box && end_box) {
			selection_clear(&html->sel, false);
			textinput_textarea_cut(c, start_box, start_idx,
					end_box, end_idx, true);
			text_box = start_box;
			char_offset = start_idx;
			reflow = true;
		}
	}
		break;

	case KEY_RIGHT:
		if (selection_exists) {
			/* In selection, move caret to end */
			text_box = selection_get_end(&html->sel, &char_offset);
		} else if (char_offset < text_box->length) {
			/* Within-box movement */
			char_offset = utf8_next(text_box->text,
					text_box->length, char_offset);
		} else {
			/* Between-box movement */
			if (!text_box->next)
				/* at end of text area: ignore */
				return true;

			text_box = text_box->next;
			if (text_box->type == BOX_BR)
				text_box = text_box->next;
			char_offset = 0;
		}
		break;

	case KEY_LEFT:
		if (selection_exists) {
			/* In selection, move caret to start */
			text_box = selection_get_start(&html->sel, &char_offset);
		} else if (char_offset > 0) {
			/* Within-box movement */
			char_offset = utf8_prev(text_box->text, char_offset);
		} else {
			/* Between-box movement */
			if (!text_box->prev)
				/* at start of text area: ignore */
				return true;

			text_box = text_box->prev;
			if (text_box->type == BOX_BR)
				text_box = text_box->prev;
			char_offset = text_box->length;
		}
		break;

	case KEY_UP:
		selection_clear(&html->sel, true);
		textinput_textarea_click(c, BROWSER_MOUSE_CLICK_1,
				textarea,
				box_x, box_y,
				text_box->x + pixel_offset,
				inline_container->y + text_box->y - 1);
		return true;

	case KEY_DOWN:
		selection_clear(&html->sel, true);
		textinput_textarea_click(c, BROWSER_MOUSE_CLICK_1,
				textarea,
				box_x, box_y,
				text_box->x + pixel_offset,
				inline_container->y + text_box->y +
				text_box->height + 1);
		return true;

	case KEY_LINE_START:
		text_box = textinput_line_start(text_box);
		char_offset = 0;
		break;

	case KEY_LINE_END:
		text_box = textinput_line_end(text_box);
		char_offset = text_box->length;
		break;

	case KEY_TEXT_START:
		assert(text_box->parent);

		/* place caret at start of first box */
		text_box = text_box->parent->children;
		char_offset = 0;
		break;

	case KEY_TEXT_END:
		assert(text_box->parent);

		/* place caret at end of last box */
		text_box = text_box->parent->last;
		char_offset = text_box->length;
		break;

	case KEY_WORD_LEFT:
	{
		bool start_of_word;
		/* if there is a selection, caret should stay at beginning */
		if (selection_exists)
			break;

		start_of_word = (char_offset <= 0 ||
				isspace(text_box->text[char_offset - 1]));

		while (!textinput_word_left(text_box->text,
				&char_offset, NULL)) {
			struct box *prev = NULL;

			assert(char_offset == 0);

			if (start_of_word) {
				/* find the preceding non-BR box */
				prev = text_box->prev;
				if (prev && prev->type == BOX_BR)
					prev = prev->prev;
			}

			if (!prev) {
				/* just stay at the start of this box */
				break;
			}

			assert(prev->type == BOX_TEXT);

			text_box = prev;
			char_offset = prev->length;
		}
	}
		break;

	case KEY_WORD_RIGHT:
	{
		bool in_word;
		/* if there is a selection, caret should move to the end */
		if (selection_exists) {
			text_box = selection_get_end(&html->sel, &char_offset);
			break;
		}

		in_word = (char_offset < text_box->length &&
				!isspace(text_box->text[char_offset]));

		while (!textinput_word_right(text_box->text, text_box->length,
				&char_offset, NULL)) {
			struct box *next = text_box->next;

			/* find the next non-BR box */
			if (next && next->type == BOX_BR)
				next = next->next;

			if (!next) {
				/* just stay at the end of this box */
				char_offset = text_box->length;
				break;
			}

			assert(next->type == BOX_TEXT);

			text_box = next;
			char_offset = 0;

			if (in_word && text_box->length > 0 &&
					!isspace(text_box->text[0])) {
				/* just stay at the start of this box */
				break;
			}
		}
	}
		break;

	case KEY_PAGE_UP:
	{
		int nlines = (textarea->height / text_box->height) - 1;

		while (nlines-- > 0)
			text_box = textinput_line_above(text_box);

		if (char_offset > text_box->length)
			char_offset = text_box->length;
	}
		break;

	case KEY_PAGE_DOWN:
	{
		int nlines = (textarea->height / text_box->height) - 1;

		while (nlines-- > 0)
			text_box = textinput_line_below(text_box);

		/* vague attempt to keep the caret at the same horizontal
		 * position, given that the code currently cannot support it
		 * being beyond the end of a line */
		if (char_offset > text_box->length)
			char_offset = text_box->length;
	}
		break;

	default:
		return false;
	}

	/*
	 box_dump(textarea, 0);
	 for (struct box *t = inline_container->children; t; t = t->next) {
		assert(t->type == BOX_TEXT);
		assert(t->text);
		assert(t->parent == inline_container);
		if (t->next) assert(t->next->prev == t);
		if (t->prev) assert(t->prev->next == t);
		if (!t->next) {
			assert(inline_container->last == t);
			break;
		}
		if (t->next->type == BOX_BR) {
			assert(t->next->next);
			t = t->next;
		}
	} */

	if (reflow)
		textinput_textarea_reflow(c, textarea, inline_container);

	if (text_box->length + SPACE_LEN(text_box) <= char_offset) {
		if (text_box->next && text_box->next->type == BOX_TEXT) {
			/* the text box has been split when reflowing and
			   the caret is in the second part */
			char_offset -= (text_box->length + SPACE_LEN(text_box));
			text_box = text_box->next;
			assert(text_box);
			assert(char_offset <= text_box->length);
			/* Scroll back to the left */
			if (textarea->scroll_x != NULL) {
				box_x += scrollbar_get_offset(
						textarea->scroll_x);
				scrollbar_set(textarea->scroll_x, 0, false);
			}
		} else {
			assert(!text_box->next ||
					(text_box->next &&
					text_box->next->type == BOX_BR));

			char_offset = text_box->length + SPACE_LEN(text_box);
		}
	}

	font_plot_style_from_css(text_box->style, &fstyle);

	nsfont.font_width(&fstyle, text_box->text, char_offset, &pixel_offset);

	selection_clear(&html->sel, true);

	textarea->gadget->caret_inline_container = inline_container;
	textarea->gadget->caret_text_box = text_box;
	textarea->gadget->caret_box_offset = char_offset;
	textarea->gadget->caret_pixel_offset = pixel_offset;

	box_x += scrollbar_get_offset(textarea->scroll_x);
	box_y += scrollbar_get_offset(textarea->scroll_y);
	scrolled = textinput_ensure_caret_visible(c, textarea);
	box_x -= scrollbar_get_offset(textarea->scroll_x);
	box_y -= scrollbar_get_offset(textarea->scroll_y);

	browser_window_place_caret(bw,
			box_x + inline_container->x + text_box->x +
			pixel_offset,
			box_y + inline_container->y + text_box->y,
			text_box->height,
			textinput_textarea_callback,
			textinput_textarea_paste_text,
			textinput_textarea_move_caret,
			textarea, c);

	if (scrolled || reflow)
		html__redraw_a_box(c, textarea);

	return true;
}


/**
 * Handle clicks in a text area by placing the caret.
 *
 * \param  c         html content where click occurred
 * \param  mouse     state of mouse buttons and modifier keys
 * \param  textarea  textarea box
 * \param  box_x     position of textarea in global document coordinates
 * \param  box_y     position of textarea in global document coordinates
 * \param  x         coordinate of click relative to textarea
 * \param  y         coordinate of click relative to textarea
 */
void textinput_textarea_click(struct content *c, browser_mouse_state mouse,
		struct box *textarea, int box_x, int box_y, int x, int y)
{
	/* A textarea is an INLINE_BLOCK containing a single
	 * INLINE_CONTAINER, which contains the text as runs of TEXT
	 * separated by BR. There is at least one TEXT. The first and
	 * last boxes are TEXT. Consecutive BR may not be present. These
	 * constraints are satisfied by using a 0-length TEXT for blank
	 * lines. */

	int char_offset = 0, pixel_offset = 0;
	struct box *inline_container = textarea->children;
	struct box *text_box;
	bool scrolled;
	html_content *html = (html_content *)c;

	text_box = textinput_textarea_get_position(textarea, x, y,
			&char_offset, &pixel_offset);

	textarea->gadget->caret_inline_container = inline_container;
	textarea->gadget->caret_text_box = text_box;
	textarea->gadget->caret_box_offset = char_offset;
	textarea->gadget->caret_pixel_offset = pixel_offset;

	box_x += scrollbar_get_offset(textarea->scroll_x);
	box_y += scrollbar_get_offset(textarea->scroll_y);
	scrolled = textinput_ensure_caret_visible(c, textarea);
	box_x -= scrollbar_get_offset(textarea->scroll_x);
	box_y -= scrollbar_get_offset(textarea->scroll_y);

	browser_window_place_caret(html->bw,
			box_x + inline_container->x + text_box->x +
			pixel_offset,
			box_y + inline_container->y + text_box->y,
			text_box->height,
			textinput_textarea_callback,
			textinput_textarea_paste_text,
			textinput_textarea_move_caret,
			textarea, c);

	if (scrolled)
		html__redraw_a_box(c, textarea);
}


/**
 * Paste a block of text into an input field at the caret position.
 *
 * \param  bw        browser window
 * \param  utf8      pointer to block of text
 * \param  utf8_len  length (bytes) of text block
 * \param  last      true iff this is the last chunk (update screen too)
 * \param  p1        pointer to input box
 * \param  p2        html content with the input box
 * \return true iff successful
 */

bool textinput_input_paste_text(struct browser_window *bw,
		const char *utf8, unsigned utf8_len, bool last,
		void *p1, void *p2)
{
	struct box *input = p1;
	struct content *c = p2;
	struct box *text_box = input->children->children;
	size_t box_offset = input->gadget->caret_box_offset;
	unsigned int nchars = utf8_length(input->gadget->value);
	const char *ep = utf8 + utf8_len;
	const char *p = utf8;
	bool success = true;
	bool update = last;

	/* keep adding chars until we've run out or would exceed
		the maximum length of the field (in which we silently
		ignore all others)
	*/
	while (p < ep && nchars < input->gadget->maxlength) {
		char buf[80 + 6];
		int nbytes = 0;

		/* how many more chars can we insert in one go? */
		while (p < ep && nbytes < 80 &&
				nchars < input->gadget->maxlength &&
				*p != '\n' && *p != '\r') {
			unsigned len = utf8_next(p, ep - p, 0);
			if (*p == ' ')
				nbytes += utf8_from_ucs4(160, &buf[nbytes]);
			else {
				memcpy(&buf[nbytes], p, len);
				nbytes += len;
			}

			p += len;
			nchars++;
		}

		if (!textinput_textbox_insert(c, text_box, box_offset,
				buf, nbytes)) {
			/* we still need to update the screen */
			update = true;
			success = false;
			break;
		}
		box_offset += nbytes;
		/* Keep caret_form_offset in sync -- textbox_insert uses this
		 * to determine where to insert into the gadget's value */
		input->gadget->caret_form_offset += nbytes;

		/* handle CR/LF and LF/CR terminations */
		if (*p == '\n') {
			p++;
			if (*p == '\r') p++;
		}
		else if (*p == '\r') {
			p++;
			if (*p == '\n') p++;
		}
	}

	if (update)
		textinput_input_update_display(c, input, box_offset,
				false, true);

	return success;
}


/**
 * Move caret to new position after reformatting
 *
 * \param  bw   browser window
 * \param  p1   pointer to text input box
 * \param  p2   html content with the input box
 * \return none
 */

void textinput_input_move_caret(struct browser_window *bw,
		void *p1, void *p2)
{
	struct box *input = (struct box *)p1;
	struct content *c = p2;
	struct box *text_box = input->children->children;
	unsigned int box_offset = input->gadget->caret_box_offset;
	int pixel_offset;
	int box_x, box_y;
	plot_font_style_t fstyle;

	font_plot_style_from_css(text_box->style, &fstyle);

	box_coords(input, &box_x, &box_y);

	nsfont.font_width(&fstyle, text_box->text, box_offset,
			&pixel_offset);

	browser_window_place_caret(bw,
			box_x + input->children->x +
					text_box->x + pixel_offset,
			box_y + input->children->y + text_box->y,
			text_box->height,
			textinput_input_callback,
			textinput_input_paste_text,
			textinput_input_move_caret,
			input, c);
}

/**
 * Key press callback for text or password input boxes.
 *
 * \param bw   The browser window containing the input box
 * \param key  The UCS4 character codepoint
 * \param p1   The input box
 * \param p2   The html content with the input box
 * \return     true if the keypress is dealt with, false otherwise.  It can
 *             return true even if it ran out of memory; this just means that
 *             it would have claimed it if it could.
 */
bool textinput_input_callback(struct browser_window *bw, uint32_t key,
		void *p1, void *p2)
{
	struct box *input = (struct box *)p1;
	struct content *c = p2;
	html_content *html = (html_content *)c;
	struct box *text_box = input->children->children;
	size_t box_offset = input->gadget->caret_box_offset;
	size_t end_offset;
	int pixel_offset = input->gadget->caret_pixel_offset;
	int box_x, box_y;
	struct form* form = input->gadget->form;
	bool changed = false;
	char utf8[6];
	unsigned int utf8_len;
	bool to_textarea = false;
	bool selection_exists = html->sel.defined;

	input->gadget->caret_form_offset =
			textinput_get_form_offset(input, text_box, box_offset);

	/* update the form offset */
	input->gadget->caret_form_offset =
			textinput_get_form_offset(input, text_box, box_offset);

	selection_get_end(&html->sel, &end_offset);

	box_coords(input, &box_x, &box_y);

	/* normal character insertion */
	if (!(key <= 0x001F || (0x007F <= key && key <= 0x009F))) {
		/* have we exceeded max length of input? */
		utf8_len = utf8_length(input->gadget->value);
		if (utf8_len >= input->gadget->maxlength)
			return true;

		utf8_len = utf8_from_ucs4(key, utf8);

		if (!textinput_textbox_insert(c, text_box, box_offset,
				utf8, utf8_len))
			return true;

		box_offset += utf8_len;

		changed = true;

	} else switch (key) {
	case KEY_DELETE_LEFT:
	{
		int prev_offset, new_offset;

		if (selection_exists) {
			textinput_textbox_delete(c, text_box, 0, 0);
		} else {
			/* Can't delete left from text box start */
			if (box_offset == 0)
				return true;

			prev_offset = box_offset;
			new_offset = utf8_prev(text_box->text, box_offset);

			if (textinput_textbox_delete(c, text_box, new_offset,
					prev_offset - new_offset))
				box_offset = new_offset;
		}

		changed = true;
	}
		break;

	case KEY_DELETE_RIGHT:
	{
		unsigned next_offset;

		if (selection_exists) {
			textinput_textbox_delete(c, text_box, 0, 0);
		} else {
			/* Can't delete right from text box end */
			if (box_offset >= text_box->length)
				return true;

			/* Go to the next valid UTF-8 character */
			next_offset = utf8_next(text_box->text,
					text_box->length, box_offset);

			textinput_textbox_delete(c, text_box, box_offset,
					next_offset - box_offset);
		}

		changed = true;
	}
		break;

	case KEY_TAB:
	{
		struct form_control *next_input;
		/* Find next text entry field that is actually
		 * displayed (i.e. has an associated box) */
		for (next_input = input->gadget->next;
				next_input &&
				((next_input->type != GADGET_TEXTBOX &&
				next_input->type != GADGET_TEXTAREA &&
				next_input->type != GADGET_PASSWORD) ||
				!next_input->box);
				next_input = next_input->next)
			;
		if (!next_input)
			return true;

		input = next_input->box;
		box_offset = 0;
		to_textarea = next_input->type == GADGET_TEXTAREA;
	}
		break;

	case KEY_NL:
	case KEY_CR:	/* Return/Enter hit */
		selection_clear(&html->sel, true);

		if (form)
			form_submit(bw->current_content, bw, form, 0);
		return true;

	case KEY_SHIFT_TAB:
	{
		struct form_control *prev_input;
		/* Find previous text entry field that is actually
		 * displayed (i.e. has an associated box) */
		for (prev_input = input->gadget->prev;
				prev_input &&
				((prev_input->type != GADGET_TEXTBOX &&
				prev_input->type != GADGET_TEXTAREA &&
				prev_input->type != GADGET_PASSWORD) ||
				!prev_input->box);
				prev_input = prev_input->prev)
			;
		if (!prev_input)
			return true;

		input = prev_input->box;
		box_offset = 0;
		to_textarea = prev_input->type == GADGET_TEXTAREA;
	}
		break;

	case KEY_CUT_LINE:
		/* Clear the selection, if one exists */
		if (selection_exists)
			selection_clear(&html->sel, false);

		textinput_textarea_cut(c, text_box, 0, text_box,
				text_box->length, false);
		box_offset = 0;

		changed = true;
		break;

	case KEY_PASTE:
		gui_paste_from_clipboard(bw->window,
			box_x + input->children->x + text_box->x + pixel_offset,
			box_y + input->children->y + text_box->y);

		/* screen updated and caret repositioned already */
		return true;

	case KEY_CUT_SELECTION:
	{
		size_t start_idx, end_idx;
		struct box *start_box =
				selection_get_start(&html->sel, &start_idx);
		struct box *end_box = selection_get_end(&html->sel, &end_idx);

		if (start_box && end_box) {
			selection_clear(&html->sel, false);
			textinput_textarea_cut(c, start_box, start_idx,
					end_box, end_idx, true);

			box_offset = start_idx;
			changed = true;
		}
	}
		break;

	case KEY_RIGHT:
		if (selection_exists) {
			box_offset = end_offset;
			break;
		}

		if (box_offset < text_box->length) {
			/* Go to the next valid UTF-8 character */
			box_offset = utf8_next(text_box->text,
					text_box->length, box_offset);
		}

		break;

	case KEY_LEFT:
		/* If there is a selection, caret should remain at start */
		if (selection_exists)
			break;

		/* Go to the previous valid UTF-8 character */
		box_offset = utf8_prev(text_box->text, box_offset);
		break;

	case KEY_LINE_START:
		box_offset = 0;
		break;

	case KEY_LINE_END:
		box_offset = text_box->length;
		break;

	case KEY_WORD_LEFT:
		/* If there is a selection, caret should remain at start */
		if (selection_exists)
			break;

		if (!textinput_word_left(text_box->text, &box_offset, NULL))
			box_offset = 0;

		break;

	case KEY_WORD_RIGHT:
		if (selection_exists) {
			box_offset = end_offset;
			break;
		}

		if (!textinput_word_right(text_box->text, text_box->length,
				&box_offset, NULL))
			box_offset = text_box->length;

		break;

	case KEY_DELETE_LINE_START:
		if (selection_exists)
			selection_clear(&html->sel, true);

		if (box_offset == 0)
			return true;

		textinput_textarea_cut(c, text_box, 0, text_box,
				box_offset, false);
		box_offset = 0;

		changed = true;
		break;

	case KEY_DELETE_LINE_END:
		if (selection_exists)
			selection_clear(&html->sel, true);

		if (box_offset >= text_box->length)
			return true;

		textinput_textarea_cut(c, text_box, box_offset,
				text_box, text_box->length, false);

		changed = true;
		break;

	default:
		return false;
	}

	selection_clear(&html->sel, true);
	textinput_input_update_display(c, input, box_offset,
			to_textarea, changed);

	return true;
}


/**
 * Handle clicks in a text or password input box by placing the caret.
 *
 * \param  bw     browser window where click occurred
 * \param  input  input box
 * \param  box_x  position of input in global document coordinates
 * \param  box_y  position of input in global document coordinates
 * \param  x      coordinate of click relative to input
 * \param  y      coordinate of click relative to input
 */
void textinput_input_click(struct content *c, struct box *input,
		int box_x, int box_y, int x, int y)
{
	size_t char_offset = 0;
	int pixel_offset = 0, dx = 0;
	struct box *text_box = input->children->children;
	plot_font_style_t fstyle;
	html_content *html = (html_content *)c;

	font_plot_style_from_css(text_box->style, &fstyle);

	nsfont.font_position_in_string(&fstyle, text_box->text,
			text_box->length, x - text_box->x,
			&char_offset, &pixel_offset);
	assert(char_offset <= text_box->length);

	/* Shift the text box horizontally to ensure that the
	 * caret position is visible, and ideally centred */
	text_box->x = 0;
	if ((input->width < text_box->width) &&
			(input->width / 2 < pixel_offset)) {
		dx = text_box->x;
		/* Move left so caret is centred */
		text_box->x = input->width / 2 - pixel_offset;
		/* Clamp, so text box's right hand edge coincides
		 * with the input's right hand edge */
		if (text_box->x < input->width - text_box->width)
			text_box->x = input->width - text_box->width;
		dx -= text_box->x;
	}
	input->gadget->caret_box_offset = char_offset;
	input->gadget->caret_form_offset =
			textinput_get_form_offset(input, text_box, char_offset);
	input->gadget->caret_pixel_offset = pixel_offset;

	browser_window_place_caret(html->bw,
			box_x + input->children->x +
					text_box->x + pixel_offset,
			box_y + input->children->y + text_box->y,
			text_box->height,
			textinput_input_callback,
			textinput_input_paste_text,
			textinput_input_move_caret,
			input, c);

	if (dx)
		html__redraw_a_box(c, input);
}


