package lwjgui.scene.control;

import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.lwjgl.glfw.GLFW;
import org.lwjgl.nanovg.NVGGlyphPosition;
import org.lwjgl.nanovg.NanoVG;
import org.lwjgl.system.MemoryStack;

import lwjgui.LWJGUIUtil;
import lwjgui.collections.ObservableList;
import lwjgui.collections.StateStack;
import lwjgui.event.Event;
import lwjgui.event.EventHandler;
import lwjgui.event.EventHelper;
import lwjgui.event.KeyEvent;
import lwjgui.event.TypeEvent;
import lwjgui.font.Font;
import lwjgui.font.FontMetaData;
import lwjgui.font.FontStyle;
import lwjgui.geometry.Insets;
import lwjgui.geometry.Pos;
import lwjgui.glfw.input.MouseHandler;
import lwjgui.paint.Color;
import lwjgui.scene.Context;
import lwjgui.scene.Cursor;
import lwjgui.scene.Node;
import lwjgui.scene.WindowManager;
import lwjgui.scene.layout.Pane;
import lwjgui.style.Background;
import lwjgui.style.BackgroundSolid;
import lwjgui.style.BlockPaneRenderer;
import lwjgui.style.BorderStyle;
import lwjgui.style.BoxShadow;
import lwjgui.theme.Theme;

/**
 * This class acts as the core controller for text input classes.
 */
public abstract class TextInputControl extends Control implements BlockPaneRenderer {
	ArrayList<String> lines;
	ArrayList<ArrayList<GlyphData>> glyphData;
	ArrayList<String> linesDraw;
	private String source = "";
	int caretPosition;
	protected boolean editing = false;
	protected boolean editable = true;
	
	private Background background;
	private Color borderColor;
	private float[] borderRadii;
	private BorderStyle borderStyle;
	private ObservableList<Background> backgrounds = new ObservableList<>();
	private ObservableList<BoxShadow> boxShadows = new ObservableList<>();
	
	private boolean wordWrap;
	
	int selectionStartPosition;
	int selectionEndPosition;
	
	protected TextInputScrollPane internalScrollPane;
	protected TextInputContentRenderer internalRenderingPane;
	
	private StateStack<TextState> undoStack;
	private boolean forceSaveState;

	private EventHandler<Event> onSelectEvent;
	private EventHandler<Event> onDeselectEvent;
	private EventHandler<Event> onTextChange;
	
	private String prompt = null;
	
	private TextParser textParser;

	private static final int MAX_LINES = Integer.MAX_VALUE;
	
	/*
	 * Visual customization
	 */
	protected int fontSize = 16;
	protected Font font = Font.SANS;
	protected Color fontFill = Theme.current().getText();
	protected FontStyle style = FontStyle.REGULAR;
	
	private boolean selectionOutlineEnabled = true;
	
	Color caretFill = Theme.current().getText();
	boolean caretFading = false;
	
	private Color selectionFill = Theme.current().getSelection();
	private Color selectionPassiveFill = Theme.current().getSelectionPassive();
	Color selectionAltFill = Theme.current().getSelectionAlt();
	private Color promptFill = Theme.current().getText().alpha(0.4f);
	
	protected TextInputControlShortcuts shortcuts;
	
	public TextInputControl() {
		this.setBorderStyle(BorderStyle.SOLID);
		this.setBorderRadii(3);
		this.setBackground(new BackgroundSolid(Theme.current().getBackground()));
		
		/*
		 * Input setup
		 */
		this.internalRenderingPane = new TextInputContentRenderer(this);
		
		/*
		 * Scroll Pane setup
		 */
		
		internalScrollPane = new TextInputControl.TextInputScrollPane();
		internalScrollPane.setContent(internalRenderingPane);
		children.add(internalScrollPane);
		
		undoStack = new StateStack<TextState>();
		setText("");
		saveState();
		this.flag_clip = false;
		
		setOnTextInputInternal( new EventHandler<TypeEvent>() {
			@Override
			public void handle(TypeEvent event) {
				if (!editing) return;
				
				if ( forceSaveState ) {
					saveState();
					forceSaveState = false;
				}
				deleteSelection();
				insertText(caretPosition, event.getCharacterString());
				setCaretPosition(caretPosition+1);
				deselect();
				
				// If you press space then Save history
				if (event.character == ' ') {
					saveState();
				}
			}
		});
		
		shortcuts = new TextInputControlShortcuts();
		setOnKeyPressedAndRepeatInternal( event -> {
			if ( !this.isEditing() )
				return;
			shortcuts.process(this, event);
		});
	}
	
	protected void saveState() {
		undoStack.Push(new TextState(getText(),caretPosition));
	}
	
	public void setWordWrap(boolean wrap) {
		wordWrap = wrap;
	}
	
	public boolean isWordWrap() {
		return this.wordWrap;
	}

	public void clear() {
		saveState();
		setText("");
		deselect();
	}
	
	public void deselect() {
		this.selectionStartPosition = caretPosition;
		this.selectionEndPosition = caretPosition;
	}
	
	public void setPrompt(String string) {
		this.prompt = string;
	}
	
	public void setText(String text) {
		if (text == null)
			return;
		
		boolean changed = true;
		if ( this.source != null && this.source.equals(text) )
			changed = false;
		
		int oldCaret = caretPosition;
		
		if ( lines == null ) {
			this.lines = new ArrayList<String>();
			this.linesDraw = new ArrayList<String>();
			this.glyphData = new ArrayList<ArrayList<GlyphData>>();
		} else {
			this.lines.clear();
			this.linesDraw.clear();
			this.glyphData.clear();
		}
		this.caretPosition = 0;
		String trail = "[!$*]T!R@A#I$L%I^N&G[!$*]"; // Naive fix to allow trailing blank lines to still be parsed
		text = text.replace("\r", "");
		this.source = text;

		String temp = text + trail; // Add tail
		String[] split = temp.split("\n");
		for (int i = 0; i < split.length; i++) {
			String tt = split[i];
			tt = tt.replace(trail, ""); // Remove tail
			if (i < split.length - 1) {
				tt += "\n";
			}
			addRow(tt);
		}
		setCaretPosition(oldCaret);
		
		// Fire on text change event
		if ( onTextChange != null && changed ) {
			EventHelper.fireEvent(onTextChange, new Event());
		}
	}
	
	private void addRow(String originalText) {
		String drawLine = originalText;
		
		ArrayList<GlyphData> glyphEntry = new ArrayList<GlyphData>();
		bindFont();
		
		try (MemoryStack stack = MemoryStack.stackPush()) {
			NVGGlyphPosition.Buffer positions;
			if (drawLine.length() > 0) {
				positions = NVGGlyphPosition.mallocStack(drawLine.length(), stack);
			} else {
				positions = NVGGlyphPosition.mallocStack(1, stack);
			}

			// Create glyph data for each character in the line
			NanoVG.nvgTextGlyphPositions(window.getContext().getNVG(), 0, 0, drawLine, positions);
			int j = 0;
			while (j < drawLine.length()) {
				GlyphData currentGlyph = fixGlyph(positions.get(), drawLine.substring(j, j + 1));
				glyphEntry.add(currentGlyph);
				j++;
			}
			
			// Add blank glyph to end of line
			GlyphData last = glyphEntry.size()== 0 ? new GlyphData(0,0,"") : glyphEntry.get(glyphEntry.size()-1);
			glyphEntry.add(new GlyphData( last.x()+last.width(), 1, "" ));
			
			// Hack to fix spacing of special characters
			for (int i = 0; i < glyphEntry.size()-1; i++) {
				GlyphData currentGlyph = glyphEntry.get(i);
				if ( currentGlyph.SPECIAL ) {
					GlyphData t = glyphEntry.get(i+1);
					float tOff = t.x()-currentGlyph.x();
					float newOff = currentGlyph.width()-tOff;
					
					for (int k = i+1; k < glyphEntry.size(); k++) {
						GlyphData fixGlyph = glyphEntry.get(k);
						fixGlyph.x += newOff;
					}
				}
			}
		}
		
		// Word Wrap not yet properly implemented properly. Will be rewritten.
		/*int vWid = (int) (this.internalScrollPane.getViewport().getWidth() - 20);
		int maxWidth = (int) (wordWrap?vWid:Integer.MAX_VALUE);
		int index = 0;
		while ( index < originalText.length() ) {
			GlyphData entry = glyphEntry.get(index);
			if ( entry.x()+entry.width() >= maxWidth ) {
				addRow(originalText.substring(0, index-1));
				addRow(originalText.substring(index-1,originalText.length()));
				return;
			}
			
			index++;
		}*/
		
		glyphData.add(glyphEntry);
		
		// Get decorated line
		if ( this.textParser != null )
			drawLine = textParser.parseText(drawLine);
		
		// Add line normally
		lines.add(originalText);
		linesDraw.add(drawLine);
		
		while ( lines.size() > MAX_LINES ) {
			lines.remove(0);
			linesDraw.remove(0);
			glyphData.remove(0);
		}
	}
	
	public void appendText(String text) {
		saveState();
		insertText(getLength(), text);
		
		internalScrollPane.scrollToBottom();
	}
	
	public void insertText(int index, String text) {
		String before = getText(0, index);
		String after = getText(index, getLength());
		setText(before + text + after);
		deselect();
	}
	
	public void setSelection(IndexRange range) {
		this.selectionStartPosition = range.getStart();
		this.selectionEndPosition = range.getEnd();
	}
	
	protected boolean deleteSelection() {
		IndexRange selection = getSelection();
		if ( selection.getLength() > 0 ) {
			deleteText(selection);
			caretPosition = selection.getStart();
			selectionStartPosition = caretPosition;
			selectionEndPosition = caretPosition;
			return true;
		}
		return false;
	}
	
	public void deleteText(IndexRange range) {
		range.normalize();
		
		String before = getText(0, range.getStart());
		String after = getText(range.getEnd(), getLength());

		saveState();
		setText(before+after);
		deselect();
	}
	
	public void deleteText(int start, int end) {
		deleteText(new IndexRange(start, end));
	}
	
	public void deletePreviousCharacter() {
		if (!deleteSelection()) {
			this.saveState();
			int old = caretPosition;
			deleteText(caretPosition-1, caretPosition);
			this.setCaretPosition(old-1);
		}
	}
	
	public void deleteNextCharacter() {
		if (!deleteSelection()) {
			this.saveState();
			deleteText(caretPosition, caretPosition+1);
		}
	}
	
	public void setFont(Font font) {
		this.font = font;
	}
	
	public void setFontFill(Color fontFill) {
		this.fontFill = fontFill;
	}
	
	public void setFontSize( int size ) {
		this.fontSize = size;
	}
	
	public void setFontStyle(FontStyle style) {
		this.style = style;
	}

	public Color getSelectionFill() {
		return selectionFill;
	}

	public void setSelectionFill(Color selectionFill) {
		this.selectionFill = selectionFill;
	}

	public Color getSelectionPassiveFill() {
		return selectionPassiveFill;
	}

	public void setSelectionPassiveFill(Color selectionPassiveFill) {
		this.selectionPassiveFill = selectionPassiveFill;
	}

	public Color getSelectionAltFill() {
		return selectionAltFill;
	}

	public void setSelectionAltFill(Color selectionAltFill) {
		this.selectionAltFill = selectionAltFill;
	}

	public void setOnSelected( EventHandler<Event> event ) {
		this.onSelectEvent = event;
	}
	
	public void setOnDeselected( EventHandler<Event> event ) {
		this.onDeselectEvent = event;
	}
	
	public void setOnTextChange( EventHandler<Event> event ) {
		this.onTextChange = event;
	}
	
	public void undo() {
		if ( this.undoStack.isCurrent() ) {
			this.saveState();
			this.undoStack.Rewind(); // Extra undo, since we just secretly saved. SHHHH
		}
		TextState state = this.undoStack.Rewind();
		if ( state == null )
			return;
		setText(state.text);
		deselect();
		this.setCaretPosition(state.caretPosition);
		forceSaveState = true;
	}
	
	public void redo() {
		TextState state = this.undoStack.Forward();
		if ( state == null )
			return;
		//this.undoStack.Rewind(); // Go back one more, since setting text will overwrite
		setText(state.text);
		deselect();
		this.setCaretPosition(state.caretPosition);
	}
	
	public void copy() {
		String text = getSelectedText();
		
		WindowManager.runLater(() -> {
			GLFW.glfwSetClipboardString(window.getID(), text);
		});
	}
	
	public void cut() {
		saveState();
		copy();
		deleteSelection();
	}
	
	public void paste() {
		saveState();
		deleteSelection();
		WindowManager.runLater(()-> {
			String str = GLFW.glfwGetClipboardString(window.getID());
			insertText(caretPosition, str);
			caretPosition += str.length();
		});
	}
	
	public void home() {
		caretPosition = this.getCaretFromRowLine(getRowFromCaret(caretPosition), 0);
	}
	
	public void end() {
		int line = getRowFromCaret(caretPosition);
		String str = lines.get(line);
		caretPosition = this.getCaretFromRowLine(line, str.length());
		
		if ( str.length() > 0 && str.charAt(str.length()-1) == '\n' )
			caretPosition--;
	}
	
	public void tab() {
		deleteSelection();
		insertText(caretPosition,"\t");
		setCaretPosition(caretPosition+1);
		saveState();
	}
	
	public void selectAll() {
		selectionStartPosition = 0;
		selectionEndPosition = getLength();
		caretPosition = selectionEndPosition;
	}
	
	public void backward() {
		caretPosition--;
		if ( caretPosition < 0 ) {
			caretPosition = 0;
		}
	}
	
	public void forward() {
		caretPosition++;
		int offset = getIndexFromCaret(caretPosition);
		if ( offset == -1 ) {
			caretPosition--;
		}
	}
	
	public int getCaretPosition() {
		return this.caretPosition;
	}
	
	public void setCaretPosition(int pos) {
		this.renderCaret = (float) (Math.PI*(3/2f));
		
		this.caretPosition = pos;
		if ( this.caretPosition < 0 )
			this.caretPosition = 0;
		if ( this.caretPosition > getLength() )
			this.caretPosition = getLength();
	}

	public Color getCaretFill() {
		return caretFill;
	}

	public void setCaretFill(Color caretFill) {
		this.caretFill = caretFill;
	}

	public boolean isCaretFading() {
		return caretFading;
	}

	/**
	 * If set to true, the caret will fade in and out instead of blink in and out.
	 * 
	 * @param caretFading
	 */
	public void setCaretFading(boolean caretFading) {
		this.caretFading = caretFading;
	}

	public String getSelectedText() {
		return getText(getSelection());
	}
	
	private String getText(IndexRange selection) {
		selection.normalize();
		
		if ( selection.getLength() == 0 )
			return "";
		
		return source.substring(
				Math.max(0, selection.getStart()),
				Math.min(selection.getEnd(),source.length())
		);
		
		/*
		int startLine = getRowFromCaret(selection.getStart());
		int endLine = getRowFromCaret(selection.getEnd());
		int t = startLine;
		String text = "";
		int a = getIndexFromCaret(selection.getStart());
		int b = getIndexFromCaret(selection.getEnd());
		while ( t <= endLine ) {
			String curLine = lines.get(t);
			if ( t == startLine && t != endLine ) {
				text += curLine.substring(a);
			} else if ( t != startLine && t == endLine ) {
				text += curLine.substring(0, b);
			} else if ( t == startLine && t == endLine ) {
				text += curLine.substring(a, b);
			} else {
				text += curLine;
			}
			t++;
		}
		
		return text;*/
	}
	
	public String getText() {
		/*String text = "";
		for (int i = 0; i < lines.size(); i++) {
			text += lines.get(i);
			if ( i < lines.size()-1 ) {
				//text += "\n";
			}
		}
		
		return text;*/
		return source;
	}
	
	public String getText(int start, int end) {
		return getText(new IndexRange(start,end));
	}
	
	/**
	 * @return the total number of lines in this text area.
	 */
	public int getNumLines() {
		return this.lines.size();
	}

	public IndexRange getSelection() {
		return new IndexRange(selectionStartPosition, selectionEndPosition);
	}

	/**
	 * Returns the amount of characters in this text area.
	 * @return
	 */
	public int getLength() {
		/*int len = 0;
		for (int i = 0; i < lines.size(); i++) {
			len += lines.get(i).length();
		}
		return len;*/
		return source.length();
	}
	
	/**
	 * Returns the caret offset at the specific line the caret position is on.
	 * @param pos
	 * @return
	 */
	protected int getIndexFromCaret(int pos) {
		int line = getRowFromCaret(pos);
		int a = 0;
		for (int i = 0; i < line; i++) {
			a += lines.get(i).length();
		}
		return pos-a;
	}

	/**
	 * Returns the row that this caret position is on.
	 * @param caret
	 * @return
	 */
	protected int getRowFromCaret(int caret) {
		int line = -1;
		int a = 0;
		
		while ( a <= caret && line < lines.size()-1 ) {
			line++;
			String t = lines.get(line);
			a += t.length();
		}
		return line;
	}
	
	/**
	 * Returns the text of the line at the specified caret position.
	 * @param caretPosition
	 * @return
	 */
	public String getLine(int caretPosition) {
		return lines.get(this.getRowFromCaret(caretPosition));
	}
	
	public void setTextParser(TextParser parser) {
		this.textParser = parser;
	}
	
	public void setEditable(boolean editable) {
		this.editable = editable;
	}
	
	public boolean isEditing() {
		return this.editing;
	}
	
	public double getViewportWidth() {
		return this.internalScrollPane.getViewportWidth();
	}
	
	public double getViewportHeight() {
		return this.internalScrollPane.getViewportHeight();
	}
	
	@Override
	protected void position(Node parent) {
		defaultStyle();
		super.position(parent);
		
		//this.internalScrollPane.position(this);
		//this.internalScrollPane.setAbsolutePosition(getX()+this.getInnerBounds().getX(), getY()+this.getInnerBounds().getY());
		//this.internalScrollPane.setPrefSize(this.getInnerBounds().getWidth(), this.getInnerBounds().getHeight());
		//this.internalScrollPane.updateChildren();
	}
	
	private void defaultStyle() {
		// SETUP OUTLINE
		this.setBorderStyle(BorderStyle.SOLID);
		this.setBorderWidth(1);
		Color outlineColor = (this.isDescendentSelected()&&!this.isDisabled())? selectionFill : Theme.current().getControlOutline();
		this.setBorderColor(outlineColor);
		
		// SETUP SELECTION GRAPHIC
		this.getBoxShadowList().clear();
		this.getBoxShadowList().add(new BoxShadow( 0, 2, 8, -3, Theme.current().getShadow(), true ));
		if (isDescendentSelected() && !this.isDisabled()) {
			Color sel = Theme.current().getSelection();
			if ( isDisabled() )
				sel = Theme.current().getSelectionPassive();

			this.getBoxShadowList().add(new BoxShadow(0, 0, 4, 0, sel.alpha(0.8f)));
			this.getBoxShadowList().add(new BoxShadow(0, 0, 1.5f, 2, sel.alpha(0.2f), true));
		}
	}
	
	@Override
	protected void resize() {
		this.setAlignment(Pos.TOP_LEFT);
		this.internalScrollPane.setPrefSize(this.getInnerBounds().getWidth(), this.getInnerBounds().getHeight());

		int width = getMaxTextWidth();
		this.internalRenderingPane.setMinSize(width, lines.size()*fontSize);
		
		super.resize();
		
		// Force select this if rendering pane or scroll pane is selected
		if ( this.internalRenderingPane.isSelected() || this.internalScrollPane.isSelected() || this.internalScrollPane.getViewport().isSelected() ) {
			window.getContext().setSelected(this);
		}
		
		//TODO: Move htis into an actual input callback
		if ( (this.isSelected() || this.isDescendentSelected()) && editable && !this.isDisabled() ) {
			if ( !editing && onSelectEvent != null ) {
				EventHelper.fireEvent(onSelectEvent, new Event());
			}
			editing = true;
		} else {
			if ( editing ) {
				if ( onDeselectEvent != null ) {
					EventHelper.fireEvent(onDeselectEvent, new Event());
				}
				this.deselect();
			}
			this.editing = false;
		}
		
		if ( caretPosition < 0 )
			caretPosition = 0;
	}
	
	private int getMaxTextWidth() {
		float width = 0;
		for (int i = 0; i < linesDraw.size(); i++) {
			String str = linesDraw.get(i);
			float len = 0;
			if ( glyphData.size() > 0 ) {
				for (int j = 0; j < str.length(); j++) {
					GlyphData d = glyphData.get(i).get(j);
					len += d.width();
				}
			}
			/*float[] bounds = new float[4];
			
			
			NanoVG.nvgFontSize(cached_context.getNVG(), fontSize);
			NanoVG.nvgFontFace(cached_context.getNVG(), font.getFont(style));
			NanoVG.nvgTextAlign(cached_context.getNVG(),NanoVG.NVG_ALIGN_LEFT|NanoVG.NVG_ALIGN_TOP);
			NanoVG.nvgTextBounds(cached_context.getNVG(), 0, 0, str, bounds);
			
			int len = (int) (bounds[2]-bounds[0]);*/
			if ( len > width ) {
				width = len;
			}
		}
		
		return (int) width;
	}
	
	protected void bindFont() {
		long vg = window.getContext().getNVG();
		NanoVG.nvgFontSize(vg, fontSize);
		NanoVG.nvgFontFace(vg, font.getFont(style));
		NanoVG.nvgTextAlign(vg,NanoVG.NVG_ALIGN_LEFT|NanoVG.NVG_ALIGN_TOP);
	}
	
	protected void bindFont(FontMetaData data) {
		double fs = data.getSize()==null?fontSize:data.getSize().doubleValue();
		Font f = data.getFont()==null?font:data.getFont();
		FontStyle fst = data.getStyle()==null?style:data.getStyle();
		
		long vg =  window.getContext().getNVG();
		NanoVG.nvgFontSize(vg, (float)fs);
		NanoVG.nvgFontFace(vg, f.getFont(fst));
		NanoVG.nvgTextAlign(vg,NanoVG.NVG_ALIGN_LEFT|NanoVG.NVG_ALIGN_TOP);
	}
	
	protected GlyphData fixGlyph(NVGGlyphPosition glyph, String originalCharacter) {
		if ( originalCharacter.equals("\t") )
			return new GlyphData( glyph.x(), 32, originalCharacter, true );
		
		if ( originalCharacter.equals("\r") ) {
			new GlyphData( glyph.x(), 0, originalCharacter );
		}
		
		return new GlyphData( glyph.x(), glyph.maxx()-glyph.x(), originalCharacter );
	}
	
	protected int getCaretFromRowLine(int row, int index) {
		int c = 0;
		for (int i = 0; i < row; i++) {
			c += lines.get(i).length();
		}
		c += index;
		return c;
	}
	
	int getPixelOffsetFromCaret( int caret ) {
		int row = getRowFromCaret( caret );
		int offset = getIndexFromCaret( caret );
		
		float temp = 0;
		for (int i = 0; i < offset; i++) {
			GlyphData g = glyphData.get(row).get(i);
			temp += g.width();
		}
		
		return (int) temp;
	}
	
	int getCaretFromPixelOffset( int row, int pixelX ) {
		if (linesDraw.size() == 0)
			return 0;
		String line = linesDraw.get(row);
		
		if ( line.length() == 0 )
			return getCaretFromRowLine(row,0);
		
		// Find first character clicked in row
		int index = 0;
		ArrayList<GlyphData> glyphLine = glyphData.get(row);
		for (int i = 0; i < glyphLine.size(); i++) {
			GlyphData dat = glyphLine.get(i);
			if ( dat.character().equals("\n"))
				break;
			if ( dat.x()+dat.width()/2 > pixelX )
				break;
			index++;
		}
		
		// Limit
		if ( index > line.length() )
			index = line.length();
		if ( index < 0 )
			index = 0;
		
		// Return caret position
		return getCaretFromRowLine(row,index);
	}
	
	int getCaretAtMouse() {
		MouseHandler mh = window.getMouseHandler();
		double mx = mh.getX()-(internalScrollPane.getContent().getX());
		double my = mh.getY()-(internalScrollPane.getContent().getY());
		
		// Find row clicked
		int row = (int) (my / (float)fontSize);
		if ( row > lines.size()-1 )
			row = lines.size()-1;
		if ( row < 0 )
			row = 0;
		return getCaretFromPixelOffset(row, (int) mx);
	}

	public boolean isSelectionOutlineEnabled() {
		return selectionOutlineEnabled;
	}

	public void setSelectionOutlineEnabled(boolean selectionOutlineEnabled) {
		this.selectionOutlineEnabled = selectionOutlineEnabled;
	}

	public TextInputScrollPane getInternalScrollPane() {
		return internalScrollPane;
	}
	
	/**
	 * Set the background color of this node.
	 * <br>
	 * If set to null, then no background will draw.
	 * @param color
	 */	
	public void setBackground(Background color) {
		this.background = color;
	}
	
	/**
	 * Get the current background color of this node.
	 * @return
	 */
	public Background getBackground() {
		return this.background;
	}

	/**
	 * Get list of backgrounds.
	 */
	@Deprecated
	public ObservableList<Background> getBackgrounds() {
		return this.backgrounds;
	}
	
	@Override
	public void setBorderStyle(BorderStyle style) {
		this.borderStyle = style;
	}

	@Override
	public BorderStyle getBorderStyle() {
		return this.borderStyle;
	}

	@Override
	public float[] getBorderRadii() {
		return borderRadii;
	}

	@Override
	public void setBorderRadii(float radius) {
		this.setBorderRadii(radius, radius, radius, radius);
	}

	@Override
	public void setBorderRadii(float[] radius) {
		this.setBorderRadii(radius[0], radius[1], radius[2], radius[3]);
	}

	@Override
	public void setBorderRadii(float cornerTopLeft, float cornerTopRight, float cornerBottomRight, float cornerBottomLeft) {
		this.borderRadii = new float[] {cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft};
	}

	@Override
	public void setBorderColor(Color color) {
		this.borderColor = color;
	}

	@Override
	public Color getBorderColor() {
		return this.borderColor;
	}

	@Override
	public ObservableList<BoxShadow> getBoxShadowList() {
		return this.boxShadows;
	}
	
	public void setHighlighting(int startIndex, int endIndex, FontMetaData metaData) {
		highlighting.add(new TextHighlighter( startIndex, endIndex, metaData) );
	}
	
	public void resetHighlighting() {
		highlighting.clear();
	}
	
	private ArrayList<TextHighlighter> highlighting = new ArrayList<TextHighlighter>();
	
	protected TextHighlighter getHighlighting( int position ) {
		for (int i = 0; i < highlighting.size(); i++) {
			TextHighlighter t = highlighting.get(i);
			if ( t.contains(position) )
				return t;
		}
		
		return null;
	}
	
	static class TextHighlighter {
		private int startIndex;
		private int endIndex;
		private FontMetaData metaData;
		
		public TextHighlighter(int startIndex, int endIndex, FontMetaData metaData) {
			this.startIndex = startIndex;
			this.endIndex = endIndex;
			this.metaData = metaData;
		}

		public boolean contains(int position) {
			return position >= startIndex && position <= endIndex;
		}

		public FontMetaData getMetaData() {
			return this.metaData;
		}
	}

	@Override
	public void render(Context context) {
		if ( !isVisible() )
			return;

		this.clip(context,8);
		
		// Apply CSS
		this.stylePush();
		{
			// Render standard pane
			BlockPaneRenderer.render(context, this);
			
			// Draw sub nodes
			this.internalScrollPane.setBackground(null);
			this.internalScrollPane.setBorderStyle(BorderStyle.NONE);
			super.render(context);
		}
		this.stylePop();
		
		if ( context == null )
			return;
		
		long vg = context.getNVG();
		// Draw Prompt
		if ( getLength() == 0 && prompt != null && prompt.length() > 0 ) {
			int xx = (int) (this.internalRenderingPane.getX());
			int yy = (int) (this.internalRenderingPane.getY());
			
			// Setup font
			NanoVG.nvgFontSize(vg, fontSize);
			NanoVG.nvgFontFace(vg, Font.SANS.getFont(FontStyle.REGULAR));
			NanoVG.nvgTextAlign(vg,NanoVG.NVG_ALIGN_LEFT|NanoVG.NVG_ALIGN_TOP);

			// Draw
			NanoVG.nvgBeginPath(vg);
			NanoVG.nvgFontBlur(vg,0);
			NanoVG.nvgFillColor(vg, promptFill.getNVG());
			NanoVG.nvgText(vg, xx, yy, prompt);
		}
	}
	
	class TextState {
		protected String text;
		protected int caretPosition;
		
		public TextState(String text, int pos) {
			this.text = text;
			this.caretPosition = pos;
		}
		
		@Override
		public String toString() {
			return caretPosition+":"+text;
		}
	}
	
	class GlyphData { // This class could be avoided if NanoVG author wouldn't ignore me.
		float width;
		float x;
		private String c;
		boolean SPECIAL;
		
		public GlyphData( float x, float width, String car ) {
			this.c = car;
			this.x = x;
			this.width = width;
		}
		
		public GlyphData( float x, float width, String car, boolean special ) {
			this( x, width, car );
			this.SPECIAL = special;
		}
		
		public String character() {
			return c;
		}
		
		public float x() {
			return x;
		}
		
		public float width() {
			return width;
		}
	}
	
	abstract class TextParser {
		public abstract String parseText(String input);
	}
	
	float renderCaret = 0;
	long lastClickTime = 0;
	long lastLastClickTime = 0;
	int DOUBLE_CLICK_SPEED = 225; // Time between clicks, in milliseconds.
	int TRIPLE_CLICK_SPEED = 650; // Time between all clicks, in milliseconds.
	
	class TextInputScrollPane extends ScrollPane {

		public TextInputScrollPane() {
			setFillToParentHeight(true);
			setFillToParentWidth(true);
			setVbarPolicy(ScrollBarPolicy.NEVER);
			setHbarPolicy(ScrollBarPolicy.NEVER);
			
			// Set padding of viewport
			this.internalCanvas.setPadding(new Insets(3,4,4,3));

			// Enter
			getViewport().setOnMouseEntered(event -> {
				getScene().setCursor(Cursor.IBEAM);
			});

			// Leave
			getViewport().setOnMouseExited(event -> {
				getScene().setCursor(Cursor.NORMAL);
			});
			
			// Clicked
			getViewport().setOnMousePressed(event -> {
				long clickTime = System.currentTimeMillis();

				if ( event.getButton() == GLFW.GLFW_MOUSE_BUTTON_LEFT && clickTime - lastClickTime > DOUBLE_CLICK_SPEED ) {
					window.getContext().setSelected(getViewport());
					
					//Sets caret position at mouse
					setCaretPosition(getCaretAtMouse());
					selectionEndPosition = caretPosition;
					
					if ( !window.getKeyboardHandler().isShiftPressed() ) {
						selectionStartPosition = caretPosition;
					}
				} else if ( event.getButton() == GLFW.GLFW_MOUSE_BUTTON_LEFT && clickTime - lastClickTime <= DOUBLE_CLICK_SPEED ) {

					if (clickTime - lastLastClickTime <= TRIPLE_CLICK_SPEED && lastLastClickTime != 0) {
						// Triple clicked.
						String line = lines.get(getRowFromCaret(caretPosition));
						int rowStart = getCaretFromRowLine(getRowFromCaret(caretPosition), 0);

						int lineLength = line.length();

						if (line.charAt(lineLength-1) == '\n' || line.charAt(lineLength-1) == '\r') {
							lineLength--;
						}

						int rowEnd = rowStart + lineLength;

						selectionStartPosition = rowStart;
						selectionEndPosition = rowEnd;

						lastLastClickTime = 0;
					} else {
						lastLastClickTime = lastClickTime;
						// Double clicked.
						setCaretPosition(getCaretAtMouse());
						selectionEndPosition = caretPosition;

						String line = lines.get(getRowFromCaret(caretPosition));

						int caretIndex = getIndexFromCaret(caretPosition);

						Pattern pattern = Pattern.compile("\\w+|\\d+");
						Matcher matcher = pattern.matcher(line);

						while (matcher.find()) {
							if (matcher.start() <= caretIndex && matcher.end() >= caretIndex) {
								selectionStartPosition = caretPosition - (caretIndex - matcher.start());
								selectionEndPosition = caretPosition + (matcher.end() - caretIndex);
								break;
							}
						}
					}

					setCaretPosition(selectionEndPosition);
				} else if ( event.getButton() == GLFW.GLFW_MOUSE_BUTTON_RIGHT ) {
					if ( TextInputControl.this.getContextMenu() != null ) {
						TextInputControl.this.getContextMenu().show(this.getScene(), event.getMouseX(), event.getMouseY());
					}
				}

				lastClickTime = clickTime;
			});
			
			// Drag mouse
			getViewport().setOnMouseDragged(event -> {
				caretPosition = getCaretAtMouse();
				selectionEndPosition = caretPosition;

				lastClickTime = 0;
				lastLastClickTime = 0;
			});
		}
		
		public void scrollToBottom() {
			setVvalue(1.0);
		}

		protected Pane getViewport() {
			return internalCanvas;
		}
	}
	
	/**
	 * Handles rendering the interal context (e.g. text) of TextInputControl.
	 */
	class TextInputContentRenderer extends Pane {
		
		private TextInputControl textInputControl;
		private Color caretFillCopy = null;
		
		public TextInputContentRenderer(TextInputControl textInputControl) {
			this.textInputControl = textInputControl;
			this.setMouseTransparent(true);
			
			this.setAlignment(Pos.TOP_LEFT);
			
			this.flag_clip = false;
		}

		@Override
		public String getElementType() {
			return "textcontentpane";
		}
		
		private long lastTime;
		@Override
		public void render(Context context) {
			if ( !isVisible() )
				return;
			
			double startX = this.getX();// + internalScrollPane.getViewport().getInnerBounds().getX();
			double startY = this.getY();// + internalScrollPane.getViewport().getInnerBounds().getY();
			
			if (textInputControl.glyphData.size() == 0) {
				textInputControl.setText(textInputControl.getText());
			}
			
			this.clip(context, -1);
			this.textInputControl.renderCaret += lastTime-System.currentTimeMillis();
			lastTime = System.currentTimeMillis();
			
			// Render selection
			IndexRange range = this.textInputControl.getSelection();
			range.normalize();
			int len = range.getLength();
			int startLine = this.textInputControl.getRowFromCaret(range.getStart());
			int endLine = this.textInputControl.getRowFromCaret(range.getEnd());
			int a = this.textInputControl.getIndexFromCaret(range.getStart());
			int b = this.textInputControl.getIndexFromCaret(range.getEnd());
			if ( len > 0 ) {
				for (int i = startLine; i <= endLine; i++) {
					String l = this.textInputControl.lines.get(i);
					
					int left = a;
					int right = b;
					
					if ( i != endLine )
						right = l.length()-1;
					if ( i != startLine )
						left = 0;

					int xx = (int)(this.textInputControl.glyphData.get(i).get(left).x());
					int yy = (int)(this.textInputControl.fontSize*i);
					int height = this.textInputControl.fontSize;
					int width = (int) (this.textInputControl.glyphData.get(i).get(right).x()-xx);

					LWJGUIUtil.fillRect(context, startX + xx, startY + yy, width, height, this.textInputControl.selectionAltFill);
				}
			}
			
			// Draw text
			synchronized(this.textInputControl.linesDraw) {
				for (int i = 0; i < this.textInputControl.linesDraw.size(); i++) {
					if ( i >= this.textInputControl.linesDraw.size() )
						continue;
					
					int mx = (int)(startX);
					int my = (int)(startY) + (this.textInputControl.fontSize*i);
					
					// Quick bounds check
					if ( my < this.textInputControl.internalScrollPane.getY()-(this.textInputControl.fontSize*i))
						continue;
					if ( my > this.textInputControl.internalScrollPane.getY()+this.textInputControl.internalScrollPane.getHeight())
						continue;
					
					String text = this.textInputControl.linesDraw.get(i);
					
					// Setup font
					this.textInputControl.bindFont();
					
					// Inefficient Draw. Thanks NanoVG refusing to implement \t Very cool
					if ( this.textInputControl.glyphData.size() > 0 ) {
						ArrayList<GlyphData> dat = this.textInputControl.glyphData.get(i);
						if ( dat.size() > 0 ) {
							
							for (int j = 0; j < text.length(); j++) {
								boolean draw = true;
								String c = text.substring(j, j+1);
								
								// Manual fix for drawing boxes of special characters of certain fonts
								// NanoVG author ALSO refuses to fix this. Cheers.
								if ( c.length() == 1 ) {
									if ( c.charAt(0) < 32 )
										draw = false;
								}
								GlyphData g = dat.get(j);
								
								if ( draw ) {
									final int currentPosition = textInputControl.getCaretFromRowLine(i, j);
									TextHighlighter highlight = textInputControl.getHighlighting(currentPosition);
									Color color = textInputControl.fontFill;
									Color background = null;
									if ( highlight == null ) {
										textInputControl.bindFont();
									} else {
										textInputControl.bindFont(highlight.getMetaData());
										if ( highlight.getMetaData().getColor() != null )
											color = highlight.getMetaData().getColor();
										
										background = highlight.getMetaData().getBackground();
									}
									if ( context == null )
										continue;
									long vg = context.getNVG();
									
									// Fill a background behind the letter if necessary.
									if ( background != null ) {
										float wid = g.width();
										if ( j < text.length() - 1 ) {
											GlyphData nextGlyph = dat.get(j+1);
											wid = nextGlyph.x()-g.x();
										}
										
										// NVG Background
										NanoVG.nvgBeginPath(context.getNVG());
										NanoVG.nvgRect(context.getNVG(), mx+g.x(), my, wid, (int)this.textInputControl.fontSize);
										NanoVG.nvgFillColor(context.getNVG(), background.getNVG());
										NanoVG.nvgFill(context.getNVG());
										NanoVG.nvgClosePath(context.getNVG());
									}
									
									// Draw character
									NanoVG.nvgBeginPath(vg);
									NanoVG.nvgFontBlur(vg,0);
									NanoVG.nvgFillColor(vg, color.getNVG());
									NanoVG.nvgText(vg, mx+g.x(), my, c);
								}
							}
						}
					}
				}
			}
			
			// Draw caret
			if ( this.textInputControl.editing ) {
				int line = this.textInputControl.getRowFromCaret(this.textInputControl.caretPosition);
				int index = this.textInputControl.getIndexFromCaret(this.textInputControl.caretPosition);
				int cx = (int) (startX-1);
				int cy = (int) (startY + (line * this.textInputControl.fontSize));
				if ( this.textInputControl.glyphData.size() > 0 ) {
					
					// Check if caret goes past the line
					boolean addWid = false;
					while ( index >= this.textInputControl.glyphData.get(line).size() ) {
						index--;
						addWid = true;
					}

					// Get current x offset
					float offsetX = this.textInputControl.glyphData.get(line).get(index).x();
					if ( addWid ) {
						offsetX += this.textInputControl.glyphData.get(line).get(index).width();
					}
					
					if (this.textInputControl.caretFading) {
						if (caretFillCopy == null) {
							caretFillCopy = this.textInputControl.caretFill.copy();
						} else if (!caretFillCopy.rgbMatches(this.textInputControl.caretFill)){
							caretFillCopy.set(this.textInputControl.caretFill);
						}

						float alpha = 1.0f-(float) (Math.sin(this.textInputControl.renderCaret * 0.004f)*0.5+0.5);
						LWJGUIUtil.fillRect(context, cx+offsetX, cy, 2, this.textInputControl.fontSize, caretFillCopy.alpha(alpha));
					} else if ( Math.sin(this.textInputControl.renderCaret*1/150f) < 0 ) {
						LWJGUIUtil.fillRect(context, cx+offsetX, cy, 2, this.textInputControl.fontSize, this.textInputControl.caretFill);
					}
				}
			}
			
			super.render(context);
		}
	}
	
	/**
	 * Handles the special key shortcuts of TextInputControl (e.g. CTRL-V to paste)
	 */
	public class TextInputControlShortcuts {

		public void process(TextInputControl tic, KeyEvent event) {
			
			// Return if consumed
			if (event.isConsumed())
				return;
			
			// Select All
			if (event.key == GLFW.GLFW_KEY_A && event.isCtrlDown && tic.isDescendentSelected()) {
				tic.selectAll();
				event.consume();
			}
			
			// Copy
			if (event.key == GLFW.GLFW_KEY_C && event.isCtrlDown && tic.isDescendentSelected() ) {
				tic.copy();
				event.consume();
			}
			
			// Home
			if (event.key == GLFW.GLFW_KEY_HOME ) {
				
				if (event.isCtrlDown) {
					tic.caretPosition = 0;
				} else {
					tic.home();
				}

				if (!event.isShiftDown) {
					tic.deselect();
				} else {
					tic.selectionEndPosition = tic.caretPosition;
				}
				
				event.consume();
			}
			
			// End
			if (event.key == GLFW.GLFW_KEY_END ) {
					
				if (event.isCtrlDown) {
					tic.caretPosition = tic.getLength();
				} else {
					tic.end();
				}
				
				if (!event.isShiftDown) {
					tic.deselect();
				} else {
					tic.selectionEndPosition = tic.caretPosition;
				}
				
				event.consume();
			}
			
			// Left
			if (event.key == GLFW.GLFW_KEY_LEFT ) {
				if (!event.isShiftDown && tic.getSelection().getLength() > 0) {
					tic.deselect();
				} else {
					tic.setCaretPosition(tic.caretPosition-1);
					if (event.isShiftDown) {
						tic.selectionEndPosition = tic.caretPosition;
					} else {
						tic.selectionStartPosition = tic.caretPosition;
						tic.selectionEndPosition = tic.caretPosition;
					}
				}
			}
			
			// Right
			if (event.key == GLFW.GLFW_KEY_RIGHT ) {
				if (!event.isShiftDown && tic.getSelection().getLength()>0 ) {
					tic.deselect();
				} else {
					tic.setCaretPosition(tic.caretPosition+1);
					if (event.isShiftDown) {
						tic.selectionEndPosition = tic.caretPosition;
					} else {
						tic.selectionStartPosition = tic.caretPosition;
						tic.selectionEndPosition = tic.caretPosition;
					}
				}
			}
			
			// Up
			if (event.key == GLFW.GLFW_KEY_UP ) {
				if (!event.isShiftDown && tic.getSelection().getLength()>0 ) {
					tic.deselect();
				}
				
				int nextRow = tic.getRowFromCaret(tic.caretPosition)-1;
				if ( nextRow < 0 ) {
					tic.setCaretPosition(0);
				} else {
					int pixelX = (int) tic.getPixelOffsetFromCaret(tic.caretPosition);
					int index = tic.getCaretFromPixelOffset(nextRow, pixelX);
					tic.setCaretPosition(index);
				}
				if (event.isShiftDown) {
					tic.selectionEndPosition = tic.caretPosition;
				} else {
					tic.selectionStartPosition = tic.caretPosition;
					tic.selectionEndPosition = tic.caretPosition;
				}
			}
			
			// Down
			if (event.key == GLFW.GLFW_KEY_DOWN ) {
				if (!event.isShiftDown && tic.getSelection().getLength() > 0) {
					tic.deselect();
				}
				
				int nextRow = tic.getRowFromCaret(tic.caretPosition)+1;
				if ( nextRow >= tic.lines.size() ) {
					tic.setCaretPosition(tic.getLength());
				} else {
					int pixelX = (int) tic.getPixelOffsetFromCaret(tic.caretPosition);
					int index = tic.getCaretFromPixelOffset(nextRow, pixelX);
					tic.setCaretPosition(index);
				}
				if (event.isShiftDown) {
					tic.selectionEndPosition = tic.caretPosition;
				} else {
					tic.selectionStartPosition = tic.caretPosition;
					tic.selectionEndPosition = tic.caretPosition;
				}
			}
			
			
			
			// These require us to be editing...
			if (tic.editing) {
			
				// Backspace
				if (event.key == GLFW.GLFW_KEY_BACKSPACE) {
					tic.deletePreviousCharacter();
					event.consume();
				}
				
				// Delete
				if (event.key == GLFW.GLFW_KEY_DELETE ) {
					tic.deleteNextCharacter();
					event.consume();
				}
				
				// Paste
				if (event.key == GLFW.GLFW_KEY_V && event.isCtrlDown ) {
					tic.paste();
					event.consume();
				}
				
				// Cut
				if (event.key == GLFW.GLFW_KEY_X && event.isCtrlDown ) {
					tic.cut();
					event.consume();
				}
				
				// Undo/Redo
				if (event.key == GLFW.GLFW_KEY_Z && event.isCtrlDown ) {
					if (event.isShiftDown) {
						tic.redo();
					} else {
						tic.undo();
					}
					
					event.consume();
				}
				
				// Normal Redo
				if ( event.key == GLFW.GLFW_KEY_Y && event.isCtrlDown ) {
					tic.redo();
					event.consume();
				}
			}
		}
	}
}