// ---------------------------------------------------------------------------
// 
//   Lily Engine alpha
// 
//   Copyright (c) 2010 Clifford Jolly
//     http://cliffjolly.com
//     cliff@cliffjolly.com
// 
// ---------------------------------------------------------------------------
// 
//   This program is free software; you can redistribute it and/or modify
//   it under the terms of the GNU General Public License as published by
//   the Free Software Foundation; either version 2 of the License, or
//   (at your option) any later version.
//  
//   This program is distributed in the hope that it will be useful, but
//   WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//   General Public License for more details.
// 
//   You should have received a copy of the GNU General Public License
//   along with this program; if not, write to the Free Software
//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
//   USA
// -------------------------- END HEADER -------------------------------------

#include "../hacks/winhacks.h"

#include <sstream>
#include <iostream>
#include <string>
using namespace std;

#include <GL/gl.h>

#include "../malfont/fontmanager.h"
#include "../malfont/malfont.h"

#include "../3rdparty/tinyxml/tinyxml.h"
#include "widgettext.h"

static inline bool isWhiteSpace(char c) {
	return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}

WidgetText::WidgetText(void) {
	wordWrap = false;
	scale = 1;
	
	fontName = widgetGetDefaultStr("WidgetText.fontName");
	if(!fontName.size()) {
		fontName = "defaultfont.tga";
	}
	
	editable = false;
	cursorPos = 0;
	needsNewDisplayList = true;
	
	lastDrawnHeight = -1;
	lastDrawnWidth  = -1;
	
	displayList = 0;
	widgetAddDisplayListSentinel(&displayList);
	
	markStart = 0;
	markEnd = 0;
	marking = false;
	
	// Marks default to blue.
	markColor.r = markColor.g = 0;
	markColor.b = markColor.a = 1;
	
	// Cursor defaults to white.
	cursorColor.r = cursorColor.g = cursorColor.b = cursorColor.a = 1;
	
	numberOnly = false;
}

WidgetText::WidgetText(TiXmlElement *xml) : Widget(xml) {
	int preformatted = 0;
	xml->QueryIntAttribute("preformatted", &preformatted);	

	int tmp = 0;
	wordWrap = false;
	xml->QueryIntAttribute("wordWrap", &tmp);
	if(tmp) {
		wordWrap = true;
	}
	
	tmp = 0;
	editable = false;
	xml->QueryIntAttribute("editable", &tmp);
	if(tmp) {
		editable = true;
	}
	
	scale = 1;
	xml->QueryFloatAttribute("scale", &scale);

	fontName = widgetGetDefaultStr("WidgetText.fontName");
	if(!fontName.size()) {
		fontName = "defaultfont.tga";
	}
	
	const char *xmlFontName = xml->Attribute("font");
	if(xmlFontName) {
		fontName = xmlFontName;
	}

	TiXmlText *textElement;
	TiXmlNode *child;
	child = xml->FirstChild();
	
	// Go through all the children, looking for a text element. Though,
	// we really should just pick the first one and explode with an
	// error if it's not a text element.
	while(child) {
		textElement = child->ToText();
		
		if(textElement) {
			// Found a text element.
			
			textElement->SetCDATA(false);
			const char *contents = textElement->Value();
			
			if(contents) {
				
				if(preformatted) {
					
					// Preformatted text means we can just pull the
					// character data right out.
					text = string(contents);
					
				} else {
					
					// Non-preformatted (like normal HTML) means
					// that we have to skip over all the whitespace.
					
					int contentsSize = strlen(contents) + 1;
					
					// Temporary buffer that's as big as the
					// contents, just in case we don't actually hit
					// any extra whitespace.
					char *unformatted = new char[contentsSize];
					
					int i = 0; // Source index.
					int n = 0; // Destination index.
					
					// Skip to the first non-whitespace character.
					while(isWhiteSpace(contents[i])) {
						i++;
					}
					
					// Start copying non-whitespace.
					while(i < contentsSize) {
						
						if(isWhiteSpace(contents[i])) {
							// If we did hit whitespace, insert a
							// space (which is what all whitespace
							// will get reduced to), then skip
							// until the next word or whatever.
							unformatted[n++] = ' ';
							while(isWhiteSpace(contents[i])) {
								i++;
							}
						}
						
						// Copy a character and advance.
						unformatted[n++] = contents[i];
						i++;
					}
					
					// Copy it over and free our temp buffer.
					text = string(unformatted);
					delete[] unformatted;
					
				}
				
				// We want whatever is the first text element. Any
				// extras are illegal anyway.
				break;
			}
		}
		child = child->NextSibling();
	}
	
	cursorPos = 0;
	needsNewDisplayList = true;
	
	lastDrawnHeight = -1;
	lastDrawnWidth  = -1;
	
	displayList = 0;
	widgetAddDisplayListSentinel(&displayList);

	markStart = 0;
	markEnd = 0;
	marking = false;
	
}

WidgetText::~WidgetText(void) {
	
	widgetRemoveDisplayListSentinel(&displayList);
	
	if(displayList) {
		glDeleteLists(displayList, 1);
	}
}


bool WidgetText::getWordWrap(void) {
	return wordWrap;
}

void WidgetText::setWordWrap(bool b) {
	if(b != wordWrap) {
		needsNewDisplayList = true;
	}
	wordWrap = b;
}

const std::string &WidgetText::getText(void) {
	return text;
}

void WidgetText::setText(const std::string &str) {
	needsNewDisplayList = true;
	text = str;

	if(numberOnly) {
		enforceNumberOnly();
	}

	fixCursorPos();
	
}

const std::string &WidgetText::getFontName(void) {
	return fontName;
}

void WidgetText::setFontName(const std::string &newFont) {
	needsNewDisplayList = true;
	fontName = newFont;
}

void WidgetText::setEditable(bool editable) {
	
	// We'll need a new display list to either show or hide the cursor
	// when this gets changed.
	if(this->editable != editable) {
		needsNewDisplayList = true;
	}
	
	this->editable = editable;
}

bool WidgetText::getEditable(void) {
	return editable;
}

void WidgetText::setCursorPos(int newPos) {
	cursorPos = newPos;
	fixCursorPos();
}

int WidgetText::getCursorPos(void) {
	return cursorPos;
}

void WidgetText::draw(void) {

	Widget::draw();
	
	// FIXME: Is the height really needed?
	if(lastDrawnWidth != width || lastDrawnHeight != height) {
		lastDrawnWidth = width;
		lastDrawnHeight = height;
		needsNewDisplayList = true;
	}
	
	FontManager *fontManager = FontManager::get();
	MalFont *font = fontManager->getFont(fontName);

	glColor4f(1, 1, 1, 1);
	float wantedWidth = 0;
	float wantedHeight = 0;
	
	font->bindFontTexture();
	
	//font->drawText(0, 0, text.c_str(), width, height, scale, &wantedWidth, &wantedHeight, wordWrap, (editable && hasFocus) ? cursorPos : -1, true);

	if(needsNewDisplayList || !displayList) {
		
		// Generate a display list if we don't have one already.
		if(!displayList) {
			displayList = glGenLists(1);
		}
		
		// Rebuild the display list.
		glNewList(displayList, GL_COMPILE);
		
		float markColorArray[4] = {markColor.r, markColor.g, markColor.b, markColor.a};
		float cursorColorArray[4] = {cursorColor.r, cursorColor.g, cursorColor.b, cursorColor.a};
		
		font->drawText(
			0, 0, text.c_str(),
			width, height,
			scale,
			&wantedWidth, &wantedHeight,
			wordWrap,
			(editable && hasFocus) ? cursorPos : -1,
			markStart, markEnd,
			true,
			cursorColorArray, markColorArray);
		
		glEndList();
		
		// Now that we've been through the drawing process, we know what
		// size we actually want to be.
		if(!wordWrap) {
			setMinWidth(wantedWidth);
			if(wantedWidth != width) {
				markSizeDirty();
				setWidth(wantedWidth);
			}
		}
		
		setMinHeight(wantedHeight);
		
		if(wantedHeight != height) {
			markSizeDirty();
			setHeight(wantedHeight);
		}
		
	}
	
	glCallList(displayList);
	
	needsNewDisplayList = false;

}

// Quick little function to swap markStart and markEnd if they're out
// of order. Don't do this while selecting with the mouse, because the
// start and end correspond to the start and end of dragging, instead
// of order in the text.
static void makeMarkSane(int &markStart, int &markEnd) {
	if(markStart < 0) markStart = 0;
	if(markEnd < 0) markEnd = 0;
	if(markStart > markEnd) {
		int tmp = markStart;
		markStart = markEnd;
		markEnd = tmp;
	}
}

bool WidgetText::handleEvent(const WidgetEvent *event) {

	if(Widget::handleEvent(event)) {
		// We're going to go ahead and let the normal event handler eat
		// this. Probably means a script overrode something.
		return true;
	}

	if(editable) {
		
		bool textChangedDoCallback = false;

		if((event->type == WIDGETEVENT_FOCUSENTER || event->type == WIDGETEVENT_FOCUSLEAVE) && editable) {
			needsNewDisplayList = true;
			
			if(numberOnly && event->type == WIDGETEVENT_FOCUSLEAVE) {
				// When the user is done editing, set back to a number.
				enforceNumberOnly();
				fixCursorPos();
			}
		}

		if(event->type == WIDGETEVENT_FOCUSACCEPT || event->type == WIDGETEVENT_FOCUSACCEPT_BACK) {
			return true;
		}

		if(((event->type == WIDGETEVENT_MOUSEDOWN || event->type == WIDGETEVENT_MOUSEUP) && event->mouse.button == 0) || (event->type == WIDGETEVENT_MOUSEMOTION && marking)) {
			
			FontManager *fontManager = FontManager::get();
			MalFont *font = fontManager->getFont(fontName);
			int newCurLoc = 0;
			
			if(font) {
				char *buf = font->makeWordWrappedBuffer(
					text.c_str(),
					width,
					scale,
					NULL,
					NULL,
					NULL,
					event->mouse.x,
					event->mouse.y,
					&newCurLoc,
					!wordWrap);
					
				free(buf);
				
				if(cursorPos != newCurLoc) {
					needsNewDisplayList = true;
				}
				
				if(event->type == WIDGETEVENT_MOUSEDOWN) {
					cursorPos = newCurLoc;
				}
			}

			// Marking start.
			if(event->type == WIDGETEVENT_MOUSEDOWN) {
				
				marking = true;
				
				// Set both of these. Otherwise we end up with markEnd
				// still pointing to the end of the last mark.
				markStart = newCurLoc;
				markEnd = newCurLoc;
			}
			
			// Marking end.
			if(event->type == WIDGETEVENT_MOUSEUP) {
				marking = false;
				markEnd = newCurLoc;
			}
			
			if(event->type == WIDGETEVENT_MOUSEMOTION) {
				markEnd = newCurLoc;
				
				// Only moves cursor if marking!
				cursorPos = newCurLoc;
			}
			
			return true;
		}
	
		char ascii = event->keyboard.convertedKey;
	
		// Keyboard events!
		if(event->type == WIDGETEVENT_KEYDOWN || event->type == WIDGETEVENT_KEYREPEAT) {
			
			// This is basically always going to need a redraw, except
			// in few places, in which case it's just redundant.
			needsNewDisplayList = true;
			
			// If it's still true after all the functional/special keys,
			// are checked, then just insert ascii stuff into the text.
			bool tryAscii = true;
			
			// Keep this around in case we need to mark the area we
			// just moved over.
			int oldCursorPos = cursorPos;

			// Figure out if we're at the beginning or end of the selection.
			int *changingMarkPoint = NULL;
			if(markStart == cursorPos) {
				changingMarkPoint = &markStart;
			} else if(markEnd == cursorPos) {
				changingMarkPoint = &markEnd;
			}
			
			// Handle arrow keys and weird crap.
			switch(event->keyboard.keyCode) {
				
				case WIDGET_KEY_LEFT:
				
					if(event->keyboard.ctrl) {
						
						// We're holding ctrl down, so skip words
						// instead of just characters.
						cursorPos--;
						while(cursorPos >= 0 && !isWhiteSpace(text[cursorPos])) {
							cursorPos--;
						}
						
					} else {
						
						// Single character.
						cursorPos--;
						
					}
					
					fixCursorPos();
					tryAscii = false;
					break;
					
				case WIDGET_KEY_RIGHT:
					if(event->keyboard.ctrl) {
						
						// Skip words.
						cursorPos++;
						while(cursorPos < (int)text.size() && !isWhiteSpace(text[cursorPos])) {
							cursorPos++;
						}
						
					} else {
						
						// Single character.
						cursorPos++;
						
					}
					fixCursorPos();
					tryAscii = false;
					break;
					
				// FIXME: This code is a little ugly and I have doubts
				//   about its robustness. I may have missed a bounds
				//   check somewhere. This applies to both up arrow and
				//   down arrow key events.
				
				case WIDGET_KEY_UP: {
					// FIXME: This should be made to treat '\t' as
					// multiple characters.
					
					// Count the number of characters to the start of
					// the line.
					int charsToLineStart = 0;
					int i;
					i = cursorPos;
					
					do {
						charsToLineStart++;
						i--;
					} while(text[i] != '\n' && i >= 0);
					
					// Move onto the previous line.
					i--;
					if(i < 0) {
						
						// We're at the start of the buffer. Everything
						// after this would be redundant.
						i = 0;
						
					} else {
						
						// Find the start of the previous line.
						while(text[i] != '\n' && i >= 0) {
							i--;
						}
						
						// Go forward.
						i++;
						charsToLineStart--;
						while(charsToLineStart > 0 && text[i] != '\n' && i != (int)text.size()) {
							charsToLineStart--;
							i++;
						}
						
					}
					
					// Done!
					cursorPos = i;
					
					fixCursorPos();
					break;
				}
				
				case WIDGET_KEY_DOWN: {
					// FIXME: This should be made to treat '\t' as
					// multiple characters.

					// Count the number of characters to the start of
					// the line.
					int charsToLineStart = 0;
					int i;
					i = cursorPos;
					
					do {
						charsToLineStart++;
						i--;
					} while(text[i] != '\n' && i >= 0);

					// Go to the end of THIS line.
					i = cursorPos;
					while(i < (int)text.size() && text[i] != '\n') {
						i++;
					}
					
					if(i == (int)text.size()) {
						
						// Oh. We've reached the end.
						
					} else {
						
						// Skip the newline we just ran into.
						i++;
						charsToLineStart--;
						
						// Move forward.
						while(charsToLineStart && i < (int)text.size() && text[i] != '\n') {
							charsToLineStart--;
							i++;
						}
						
					}

					cursorPos = i;

					fixCursorPos();
					break;
				}
					
				case WIDGET_KEY_HOME:
					
					// Move us off a newline if we're at the end of a line.
					if(cursorPos < (int)text.size() && text[cursorPos] == '\n') {
						cursorPos--;
					}
					
					while(text[cursorPos] != '\n' && cursorPos > 0) {
						cursorPos--;
						
						if(cursorPos >= 0 && text[cursorPos] == '\n') {
							// We want to actually stay on the same line.
							cursorPos++;
							break;
						}
					}
					fixCursorPos();
					break;
					
				case WIDGET_KEY_END:
					while(text[cursorPos] != '\n' && cursorPos != (int)text.size()) {
						cursorPos++;
					}
					fixCursorPos();
					break;
			}
			
			// Handle marking stuff.
			if(oldCursorPos != cursorPos && event->keyboard.shift) {
				if(changingMarkPoint) {
					*changingMarkPoint = cursorPos;
				} else {
					markStart = oldCursorPos;
					markEnd = cursorPos;
				}
			}
			
			if(oldCursorPos != cursorPos && !event->keyboard.shift) {
				// Cursor moved without shift held. Clear selection.
				markStart = -1;
				markEnd = -1;
			}
			
			// Handle actual converted ascii stuff.
			if(ascii) {
				
				// Another boolean to bail out of inserting ascii at
				// the end. In case we convert an ascii code into
				// something functional, like backspace and delete.
				bool dontAdd = false;
				
				if(event->keyboard.ctrl) {
					dontAdd = true;
				}
				
				switch(ascii) {
					
					case 'a':
						if(event->keyboard.ctrl) {
							// Select all.
							markEnd = text.size();
							markStart = 0;
						}
						break;
					
					case 'x':
						if(event->keyboard.ctrl) {
							// Cut.

							fixCursorPos();

							//if(markEnd > int(text.size()) - 1) markEnd = int(text.size()) - 1;
							//if(markStart > int(text.size()) - 1) markStart = int(text.size()) - 1;
							
							makeMarkSane(markStart, markEnd);
							
							// Copy the current marked area to the clipboard.
							setClipboard(text.substr(markStart, markEnd - markStart));
							
							// Remove the old text.
							if(markStart != markEnd) {
								text = text.substr(0, markStart) + text.substr(markEnd, text.size() - cursorPos);
								cursorPos = markStart;
								markStart = -1;
								markEnd = -1;
							}
							
							textChangedDoCallback = true;
						}
						break;
					
					case 'c':
						if(event->keyboard.ctrl) {
							// Copy.
							
							fixCursorPos();
							
							//if(markEnd > int(text.size()) - 1) markEnd = int(text.size()) - 1;
							//if(markStart > int(text.size()) - 1) markStart = int(text.size()) - 1;
							
							makeMarkSane(markStart, markEnd);
							
							// Copy the current marked area to the clipboard.
							setClipboard(text.substr(markStart, markEnd - markStart));
						}
						break;
						
					case 'v':
						if(event->keyboard.ctrl) {
							// Paste.
							
							fixCursorPos();
							
							// Clobber whatever is in the selected area.
							if(markStart != markEnd) {
								text = text.substr(0, markStart) + text.substr(markEnd, text.size() - cursorPos);
								cursorPos = markStart;
								markStart = -1;
								markEnd = -1;
							}
							
							// Insert the text in the clipboard.
							std::string clipboardText = getClipboard();
							text = text.substr(0, cursorPos) + clipboardText + text.substr(cursorPos, text.size() - cursorPos);
							
							// Move the cursor to the end of the pasted text.
							cursorPos += clipboardText.size();
							
							textChangedDoCallback = true;
							
							if(numberOnly) {
								// After a copy+paste, make sure we're
								// still a number so no newlines creep
								// in.
								enforceNumberOnly();
								fixCursorPos();
							}

						}
						break;
						
					// Convert CR to LF
					case 13:
						ascii = 10;
						break;
					
					// Backspace
					case 8:
						dontAdd = true;
						
						fixCursorPos();
						
						// Don't backspace past the beginning of the
						// buffer. That would be bad.
						if(cursorPos == 0 || text.size() == 0) {
							break;
						}
						
						if(markStart != markEnd) {
							
							makeMarkSane(markStart, markEnd);
							
							text = text.substr(0, markStart) + text.substr(markEnd, text.size() - cursorPos);
							
							cursorPos = markStart;
							
							markStart = -1;
							markEnd = -1;
						
						} else {
							
							text = text.substr(0, cursorPos - 1) + text.substr(cursorPos, text.size() - cursorPos);
							cursorPos--;
							
						}

						textChangedDoCallback = true;

						break;
					
					// Delete
					case 127:
						dontAdd = true;
						
						fixCursorPos();

						if(markStart != markEnd) {
							
							makeMarkSane(markStart, markEnd);
							
							text = text.substr(0, markStart) + text.substr(markEnd, text.size() - cursorPos);
							
							cursorPos = markStart;
							
							markStart = -1;
							markEnd = -1;
						
						} else {
							
							if(cursorPos == (int)text.size() || text.size() == 0) {
								break;
							}
							
							text = text.substr(0, cursorPos) + text.substr(cursorPos + 1, text.size() - cursorPos);
							
						}

						textChangedDoCallback = true;

						break;
				}
				
				if(!dontAdd) {
					
					fixCursorPos();

					// Remove whatever is marked (to be replaced).
					if(markStart != markEnd) {
					
						makeMarkSane(markStart, markEnd);
						
						text = text.substr(0, markStart) + text.substr(markEnd, text.size() - cursorPos);
						
						cursorPos = markStart;
						
						markStart = -1;
						markEnd = -1;
					
					}
					
					fixCursorPos();
					
					// Pretty sure now that it's an ASCII character.
					// Just insert the stupid thing already.
					text = text.substr(0, cursorPos) + ascii + text.substr(cursorPos, text.size() - cursorPos);
					
					// And advance the cursor.
					cursorPos++;
					
					textChangedDoCallback = true;

					if(numberOnly && ascii == 10) {
						// Don't allow newlines if it's a number.
						enforceNumberOnly();
						fixCursorPos();
					}

				}
				
				markSizeDirty();
			}
			
			if(textChangedDoCallback) {
				handleLuaEvent("onTextChanged", event);
			}
			
            makeMarkSane(markStart, markEnd);

			return true;
		}
	}
	
	return false;
}

void WidgetText::fixCursorPos(void) {
	if(cursorPos < 0) cursorPos = 0;
	
	// Note that text.size() is actually one character past the end of
	// the buffer.
	if(cursorPos > (int)text.size()) cursorPos = (int)text.size();
}

void WidgetText::setScale(float scale) {
	this->scale = scale;
	needsNewDisplayList = true;
}

float WidgetText::getScale(void) {
	return scale;
}

void WidgetText::setCursorColor(float r, float g, float b, float a) {
	cursorColor.r = r;
	cursorColor.g = g;
	cursorColor.b = b;
	cursorColor.a = a;
	needsNewDisplayList = true;
}

void WidgetText::setMarkColor(float r, float g, float b, float a) {
	markColor.r = r;
	markColor.g = g;
	markColor.b = b;
	markColor.a = a;
	needsNewDisplayList = true;
}

void WidgetText::setMark(int start, int end) {
	markStart = start;
	markEnd = end;
	needsNewDisplayList = true;
}

int WidgetText::getMarkStart(void) {
	return markStart;
}

int WidgetText::getMarkEnd(void) {
	return markEnd;
}

void WidgetText::setNumberOnly(bool numberOnly) {
	this->numberOnly = numberOnly;
	
	if(numberOnly) {
		enforceNumberOnly();
	}
}

bool WidgetText::getNumberOnly(void) {
	return numberOnly;
}
	
void WidgetText::enforceNumberOnly(void) {
	if(numberOnly) {
		ostringstream str;
		float currentNum = atof(text.c_str());
		str << currentNum;
		text = str.str();
	}
}


