#include "multilinetextbox.h"
#include "fontbase.h"
#include "document.h"
#include "graphicsport.h"
#include "woopsifuncs.h"
#include "stringiterator.h"
#include "woopsitimer.h"
#include "woopsikey.h"
#include "woopsi.h"

using namespace WoopsiUI;

MultiLineTextBox::MultiLineTextBox(s16 x, s16 y, u16 width, u16 height, const WoopsiString& text, u32 flags, s16 maxRows, GadgetStyle* style) : ScrollingPanel(x, y, width, height, flags, style) {

	_hAlignment = TEXT_ALIGNMENT_HORIZ_CENTRE;
	_vAlignment = TEXT_ALIGNMENT_VERT_CENTRE;
	_topRow = 0;
	_opensKeyboard = true;

	_borderSize.top = 3;
	_borderSize.right = 3;
	_borderSize.bottom = 3;
	_borderSize.left = 3;

	Rect rect;
	getClientRect(rect);
	_document = new Document(getFont(), "", rect.width);
	_canvasWidth = rect.width;

	_flags.draggable = true;
	_flags.doubleClickable = true;
	_maxRows = maxRows;

	calculateVisibleRows();

	// Set maximum rows if value not set
	if (_maxRows == 0) {
		_maxRows = _visibleRows + 1;
	}

	_cursorPos = 0;
	_showCursor = false;

	setText(text);
}

void MultiLineTextBox::drawText(GraphicsPort* port) {

	// Early exit if there is no text to display
	if (_document->getLineCount() == 0) return;

	// Determine the top and bottom rows within the graphicsport's clip rect.
	// We only draw these rows in order to increase the speed of the routine.
	Rect rect;\
	port->getClipRect(rect);

	s32 regionY = -_canvasY + rect.y;						// Y co-ord of the visible region of this canvas
	s32 topRow = getRowContainingCoordinate(regionY);
	s32 bottomRow = getRowContainingCoordinate(regionY + rect.height);

	// Early exit checks
	if ((topRow < 0) && (bottomRow < 0)) return;
	if ((bottomRow >= _document->getLineCount()) && (topRow >= _document->getLineCount())) return;

	// Prevent overflows
	if (topRow < 0) topRow = 0;
	if (bottomRow >= _document->getLineCount()) bottomRow = _document->getLineCount() - 1;

	// Draw lines of text
	s32 currentRow = topRow;

	// Draw all rows in this region
	while (currentRow <= bottomRow) {
		drawRow(port, currentRow);
		currentRow++;
	}
}

void MultiLineTextBox::drawRow(GraphicsPort* port, s32 row) {

	u8 rowLength = _document->getLineTrimmedLength(row);
	s16 textX = getRowX(row) + _canvasX;
	s16 textY = getRowY(row) + _canvasY;
	
	if (isEnabled()) {
		port->drawText(textX, textY, _document->getFont(), _document->getText(), _document->getLineStartIndex(row), rowLength);
	} else {
		port->drawText(textX, textY, _document->getFont(), _document->getText(), _document->getLineStartIndex(row), rowLength, getDarkColour());
	}
}

void MultiLineTextBox::drawContents(GraphicsPort* port) {

	drawText(port);

	// Draw the cursor
	drawCursor(port);
}

void MultiLineTextBox::drawBorder(GraphicsPort* port) {

	port->drawFilledRect(0, 0, getWidth(), getHeight(), getBackColour());

	// Stop drawing if the gadget indicates it should not have an outline
	if (isBorderless()) return;

	port->drawBevelledRect(0, 0, getWidth(), getHeight(), getShadowColour(), getShineColour());
}

void MultiLineTextBox::getCursorCoordinates(s16& x, s16& y) const {

	u32 cursorRow = 0;

	x = 0;
	y = 0;

	// Only calculate the cursor position if the cursor isn't at the start of the text
	if (_cursorPos > 0) {

		// Calculate the row in which the cursor appears
		cursorRow = _document->getLineContainingCharIndex(_cursorPos);

		// Cursor line offset gives us the distance of the cursor from the start of the line
		u8 cursorLineOffset = _cursorPos - _document->getLineStartIndex(cursorRow);
			
		StringIterator* iterator = _document->getText().newStringIterator();
		iterator->moveTo(_document->getLineStartIndex(cursorRow));
			
		// Sum the width of each char in the row to find the x co-ord
		for (s32 i = 0; i < cursorLineOffset; ++i) {
			x += getFont()->getCharWidth(iterator->getCodePoint());
			iterator->moveToNext();
		}
			
		delete iterator;
	}

	// Add offset of row to calculated value
	x += getRowX(cursorRow);

	// Calculate y co-ord of the cursor
	y = getRowY(cursorRow);
}

void MultiLineTextBox::drawCursor(GraphicsPort* port) {

	// Get the cursor co-ords
	if (_showCursor) {

		s16 cursorX = 0;
		s16 cursorY = 0;

		getCursorCoordinates(cursorX, cursorY);

		// Adjust for canvas offsets
		cursorX += _canvasX;
		cursorY += _canvasY;

		// Draw cursor
		port->drawFilledXORRect(cursorX, cursorY, _document->getFont()->getCharWidth(getCursorCodePoint()), _document->getFont()->getHeight());
	}
}

u32 MultiLineTextBox::getCursorCodePoint() const {
	if (_cursorPos < _document->getText().getLength()) {
		return _document->getText().getCharAt(_cursorPos);
	} else {
		return ' ';
	}
}

// Calculate values for centralised text
u8 MultiLineTextBox::getRowX(s32 row) const {

	Rect rect;
	getClientRect(rect);

	u8 rowLength = _document->getLineTrimmedLength(row);
	u8 rowPixelWidth = _document->getFont()->getStringWidth(_document->getText(), _document->getLineStartIndex(row), rowLength);

	// Calculate horizontal position
	switch (_hAlignment) {
		case TEXT_ALIGNMENT_HORIZ_CENTRE:
			return (rect.width - rowPixelWidth) >> 1;
		case TEXT_ALIGNMENT_HORIZ_LEFT:
			return 0;
		case TEXT_ALIGNMENT_HORIZ_RIGHT:
			return rect.width - rowPixelWidth;
	}

	// Will never be reached
	return 0;
}

s16 MultiLineTextBox::getRowY(s32 row) const {

	// If the amount of text exceeds the size of the gadget, force
	// the text to be top-aligned
	if (_visibleRows <= _document->getLineCount()) {
		return row * _document->getLineHeight();
	}

	// All text falls within the textbox, so obey the alignment
	// options

	s16 textY = 0;
	s16 startPos = 0;

    s32 canvasRows = 0;
    s32 textRows = 0;

	Rect rect;
	getClientRect(rect);

	// Calculate vertical position
	switch (_vAlignment) {
		case TEXT_ALIGNMENT_VERT_CENTRE:

			// Calculate the maximum number of rows
			canvasRows = _canvasHeight / _document->getLineHeight();
			textY = row * _document->getLineHeight();

			// Get the number of rows of text
			textRows = _document->getLineCount();

			// Ensure there's always one row
			if (textRows == 0) textRows = 1;

            // Calculate the start position of the block of text
            startPos = ((canvasRows - textRows) * _document->getLineHeight()) >> 1;

            // Calculate the row Y co-ordinate
			textY = startPos + textY;
			break;
		case TEXT_ALIGNMENT_VERT_TOP:
			textY = row * _document->getLineHeight();
			break;
		case TEXT_ALIGNMENT_VERT_BOTTOM:
			textY = rect.height - (((_document->getLineCount() - row) * _document->getLineHeight()));
			break;
	}

	return textY;
}

void MultiLineTextBox::calculateVisibleRows() {

	Rect rect;
	getClientRect(rect);

	_visibleRows = rect.height / _document->getLineHeight();
}

void MultiLineTextBox::setTextAlignmentHoriz(TextAlignmentHoriz alignment) {
	_hAlignment = alignment;
	markRectsDamaged();
}

void MultiLineTextBox::setTextAlignmentVert(TextAlignmentVert alignment) {
	_vAlignment = alignment;
	markRectsDamaged();
}

bool MultiLineTextBox::cullTopLines() {

	// Ensure that we have the correct number of rows
	if ((_document->getLineCount() > _maxRows) && (_maxRows > -1)) {
		_document->stripTopLines(_document->getLineCount() - _maxRows);
		return true;
	}

	return false;
}

void MultiLineTextBox::limitCanvasHeight() {

	_canvasHeight = _document->getPixelHeight();

	Rect rect;
	getClientRect(rect);
	if (_canvasHeight < rect.height) _canvasHeight = rect.height;
}

void MultiLineTextBox::limitCanvasY() {
	Rect rect;
	getClientRect(rect);

	// Ensure that the visible portion of the canvas is not less than the
	// height of the viewer window
	if (_canvasY + _canvasHeight < rect.height) {
		jumpToTextBottom();
	}
}

void MultiLineTextBox::jumpToTextBottom() {
	Rect rect;
	getClientRect(rect);
	jump(0, -(_canvasHeight - rect.height));
}

void MultiLineTextBox::jumpToCursor() {

	// Get the co-odinates of the cursor
	s16 cursorX;
	s16 cursorY;

	getCursorCoordinates(cursorX, cursorY);

	// Work out which row the cursor falls within
	s32 cursorRow = _document->getLineContainingCharIndex(_cursorPos);
	s16 rowY = getRowY(cursorRow);

	// If the cursor is outside the visible portion of the canvas, jump to it
	Rect rect;
	getClientRect(rect);

	if (rowY + _document->getLineHeight() + _canvasY > rect.height) {

		// Cursor is below the visible portion of the canvas, so
		// jump down so that the cursor's row is the bottom row of
		// text
		jump(0, -(rowY + _document->getLineHeight() - rect.height));
	} else if (rowY + _canvasY < 0) {

		// Cursor is above the visible portion of the canvas, so
		// jump up so that the cursor's row is the top row of text
		jump(0, -cursorY);
	}
}

void MultiLineTextBox::setText(const WoopsiString& text) {

	_document->setText(text);

	cullTopLines();
	limitCanvasHeight();
	jumpToTextBottom();

	markRectsDamaged();

	_gadgetEventHandlers->raiseValueChangeEvent();
}

void MultiLineTextBox::appendText(const WoopsiString& text) {

	_document->append(text);

	cullTopLines();
	limitCanvasHeight();
	jumpToTextBottom();

	markRectsDamaged();

	_gadgetEventHandlers->raiseValueChangeEvent();
}

void MultiLineTextBox::removeText(const u32 startIndex) {
	removeText(startIndex, _document->getText().getLength() - startIndex);
}

void MultiLineTextBox::removeText(const u32 startIndex, const u32 count) {

	_document->remove(startIndex, count);

	limitCanvasHeight();
	limitCanvasY();

	moveCursorToPosition(startIndex);

	markRectsDamaged();

	_gadgetEventHandlers->raiseValueChangeEvent();
}

void MultiLineTextBox::insertText(const WoopsiString& text, const u32 index) {

	_document->insert(text, index);

	cullTopLines();
	limitCanvasHeight();

	moveCursorToPosition(index + text.getLength());

	markRectsDamaged();

	_gadgetEventHandlers->raiseValueChangeEvent();
}

void MultiLineTextBox::setFont(FontBase* font) {

	_style.font = font;
	_document->setFont(font);

	cullTopLines();
	limitCanvasHeight();
	limitCanvasY();

	markRectsDamaged();

	_gadgetEventHandlers->raiseValueChangeEvent();
}

const u16 MultiLineTextBox::getPageCount() const {
	if (_visibleRows > 0) {
		return (_document->getLineCount() / _visibleRows) + 1;
	} else {
		return 1;
	}
}

const u16 MultiLineTextBox::getCurrentPage() const {

	// Calculate the top line of text
	s32 topRow = -_canvasY / _document->getLineHeight();

	// Return the page on which the top row falls
	if (_visibleRows > 0) {
		return topRow / _visibleRows;
	} else {
		return 1;
	}
}

void MultiLineTextBox::onResize(u16 width, u16 height) {

	// Ensure the base class resize method is called
	ScrollingPanel::onResize(width, height);

	// Resize the canvas' width
	Rect rect;
	getClientRect(rect);
	_canvasWidth = rect.width;
	_canvasHeight = rect.height;
	_canvasX = 0;
	_canvasY = 0;

	calculateVisibleRows();

	// Re-wrap the text
	_document->setWidth(getWidth());
	_document->wrap();

	bool raiseEvent = cullTopLines();
	limitCanvasHeight();
	limitCanvasY();

	if (raiseEvent) _gadgetEventHandlers->raiseValueChangeEvent();
}

const u32 MultiLineTextBox::getTextLength() const {
	return _document->getText().getLength();
}

void MultiLineTextBox::showCursor() {
	if (!_showCursor) {
		_showCursor = true;
		markRectsDamaged();
	}
}

void MultiLineTextBox::hideCursor() {
	if (_showCursor) {
		_showCursor = false;
		markRectsDamaged();
	}
}

void MultiLineTextBox::insertTextAtCursor(const WoopsiString& text) {
	insertText(text, getCursorPosition());
	jumpToCursor();
}

void MultiLineTextBox::moveCursorToPosition(const s32 position) {

	GraphicsPort* port = newGraphicsPort(false);

	// Erase existing cursor
	drawCursor(port);

	// Force position to within confines of string
	if (position < 0) {
		_cursorPos = 0;
	} else {
		s32 len = (s32)_document->getText().getLength();
		_cursorPos = len > position ? position : len;
	}

	// Draw cursor in new position
	drawCursor(port);
	
	delete port;
}

void MultiLineTextBox::onClick(s16 x, s16 y) {
	startDragging(x, y);

	// Move cursor to clicked co-ords
	Rect rect;
	getClientRect(rect);

	// Adjust x and y from screen co-ords to canvas co-ords
	s16 canvasRelativeX = x - getX() - rect.x - _canvasX;
	s16 canvasRelativeY = y - getY() - rect.y - _canvasY;

	moveCursorToPosition(getCharIndexAtCoordinates(canvasRelativeX, canvasRelativeY));
}

void MultiLineTextBox::onDoubleClick(s16 x, s16 y) {
	if (_opensKeyboard) woopsiApplication->showKeyboard(this);
}

void MultiLineTextBox::onKeyPress(KeyCode keyCode) {
	processPhysicalKey(keyCode);
}

void MultiLineTextBox::onKeyRepeat(KeyCode keyCode) {
	processPhysicalKey(keyCode);
}

void MultiLineTextBox::moveCursorUp() {
	s16 cursorX = 0;
	s16 cursorY = 0;

	getCursorCoordinates(cursorX, cursorY);

	// Get the midpoint of the cursor.  We use the midpoint to ensure that
	// the cursor does not drift off to the left as it moves up the text, which
	// is a problem when we use the left edge as the reference point when the
	// font is proportional
	cursorX += _document->getFont()->getCharWidth(_document->getText().getCharAt(_cursorPos)) >> 1;

	// Locate the character above the midpoint
	s32 index = getCharIndexAtCoordinates(cursorX, cursorY + _document->getLineHeight());

	moveCursorToPosition(index);
	jumpToCursor();
}

void MultiLineTextBox::moveCursorDown() {
	s16 cursorX = 0;
	s16 cursorY = 0;

	getCursorCoordinates(cursorX, cursorY);

	// Get the midpoint of the cursor.  We use the midpoint to ensure that
	// the cursor does not drift off to the left as it moves up the text, which
	// is a problem when we use the left edge as the reference point when the
	// font is proportional
	cursorX += _document->getFont()->getCharWidth(_document->getText().getCharAt(_cursorPos)) >> 1;

	// Locate the character above the midpoint
	s32 index = getCharIndexAtCoordinates(cursorX, cursorY - _document->getLineHeight());

	moveCursorToPosition(index);
	jumpToCursor();
}

void MultiLineTextBox::moveCursorLeft() {
	if (_cursorPos > 0) {
		moveCursorToPosition(_cursorPos - 1);
	}

	jumpToCursor();
}

void MultiLineTextBox::moveCursorRight() {
	if (_cursorPos < (s32)_document->getText().getLength()) {
		moveCursorToPosition(_cursorPos + 1);
	}

	jumpToCursor();
}

void MultiLineTextBox::processPhysicalKey(KeyCode keyCode) {
	switch (keyCode) {
		case KEY_CODE_LEFT:
			moveCursorLeft();
			break;
		case KEY_CODE_RIGHT:
			moveCursorRight();
			break;
		case KEY_CODE_UP:
			moveCursorDown();
			break;
		case KEY_CODE_DOWN:
			moveCursorUp();
			break;
		default:
			// Not interested in other keys
			break;
	}
}

void MultiLineTextBox::handleKeyboardPressEvent(const KeyboardEventArgs& e) {
	processKey(e.getKey());
}

void MultiLineTextBox::handleKeyboardRepeatEvent(const KeyboardEventArgs& e) {
	processKey(e.getKey());
}

void MultiLineTextBox::processKey(const WoopsiKey* key) {

	if (key->getKeyType() == WoopsiKey::KEY_BACKSPACE) {

		// Delete character in front of cursor
		if (_cursorPos > 0) removeText(_cursorPos - 1, 1);
	} else if (key->getValue() != '\0') {

		// Not modifier; append value
		insertTextAtCursor(key->getValue());
	} 
}

s32 MultiLineTextBox::getRowContainingCoordinate(s16 y) const {

	s32 row = -1;

	// Locate the row containing the character
	for (s32 i = 0; i < _document->getLineCount(); ++i) {

		// Abort search if we've found the row below the y co-ordinate
		if (getRowY(i) > y) {

			if (i == 0) {

				// If the co-ordinate is above the text, we return the top
				// row
				row = 0;
			} else {

				// Row within the text, so return the previous row - this is
				// the row that contains the co-ordinate.
				row = i - 1;
			}

			break;
		}
	}

	// If the co-ordinate is below the text, row will still be -1.
	// We need to set it to the last row
	if (row == -1) row = _document->getLineCount() - 1;

	return row;
}

u32 MultiLineTextBox::getCharIndexAtCoordinate(s16 x, s32 rowIndex) const {

	// Locate the character within the row
	s32 startIndex = _document->getLineStartIndex(rowIndex);
	s32 stopIndex = _document->getLineLength(rowIndex);
	s32 width = getRowX(rowIndex);
	s32 index = -1;

	StringIterator* iterator = _document->getText().newStringIterator();
	iterator->moveTo(startIndex);

	width += _document->getFont()->getCharWidth(iterator->getCodePoint());

	for (s32 i = 0; i < stopIndex; ++i) {
		if (width > x) {

			if (i == 0) {

				// If the co-ordinate is on the left of the text, we add nothing
				// to the index
				index = startIndex;
			} else {

				// Character within the row.
				// This is the character that contains the co-ordinate.
				index = startIndex + i;
			}

			break;
		}

		iterator->moveToNext();

		width += _document->getFont()->getCharWidth(iterator->getCodePoint());
	}

	delete iterator;

	// If the co-ordinate is past the last character, index will still be -1.
	// We need to set it to the last character
	if (index == -1) {
		
		if (rowIndex == _document->getLineCount() - 1) {

			// Index past the end point of the text, so return an index
			// just past the text
			index = startIndex + stopIndex;
		} else {

			// Index at the end of a row, so return the last index of the
			// row
			index = startIndex + stopIndex - 1;
		}

	}

	return index;
}

u32 MultiLineTextBox::getCharIndexAtCoordinates(s16 x, s16 y) const {
	s32 rowIndex = getRowContainingCoordinate(y);
	return getCharIndexAtCoordinate(x, rowIndex);
}
