/*
 * Copyright 2014-2017 See AUTHORS file.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kotcrab.vis.ui.widget;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Cursor.SystemCursor;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.graphics.g2d.GlyphLayout.GlyphRun;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.*;
import com.badlogic.gdx.scenes.scene2d.ui.TextField;
import com.badlogic.gdx.scenes.scene2d.ui.TextField.DefaultOnscreenKeyboard;
import com.badlogic.gdx.scenes.scene2d.ui.TextField.OnscreenKeyboard;
import com.badlogic.gdx.scenes.scene2d.ui.TextField.TextFieldStyle;
import com.badlogic.gdx.scenes.scene2d.ui.Widget;
import com.badlogic.gdx.scenes.scene2d.ui.Window;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.scenes.scene2d.utils.Disableable;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.scenes.scene2d.utils.UIUtils;
import com.badlogic.gdx.utils.*;
import com.badlogic.gdx.utils.Timer.Task;
import com.kotcrab.vis.ui.FocusManager;
import com.kotcrab.vis.ui.Focusable;
import com.kotcrab.vis.ui.VisUI;
import com.kotcrab.vis.ui.util.BorderOwner;
import com.kotcrab.vis.ui.util.CursorManager;

import java.lang.StringBuilder;

/**
 * Extends functionality of standard {@link TextField}. Style supports over, and focus border. Improved text input.
 * Due to scope of changes made this widget is not compatible with {@link TextField}.
 * @author mzechner
 * @author Nathan Sweet
 * @author Kotcrab
 * @see TextField
 */
public class VisTextField extends Widget implements Disableable, Focusable, BorderOwner {
	static private final char BACKSPACE = 8;
	static protected final char ENTER_DESKTOP = '\r';
	static protected final char ENTER_ANDROID = '\n';
	static private final char TAB = '\t';
	static private final char DELETE = 127;
	static private final char BULLET = 8226;

	static private final Vector2 tmp1 = new Vector2();
	static private final Vector2 tmp2 = new Vector2();
	static private final Vector2 tmp3 = new Vector2();

	static public float keyRepeatInitialTime = 0.4f;
	/** Repeat times for keys handled by {@link InputListener#keyDown(InputEvent, int)} such as navigation arrows */
	static public float keyRepeatTime = 0.04f;

	protected String text;
	protected int cursor, selectionStart;
	protected boolean hasSelection;
	protected boolean writeEnters;
	protected final GlyphLayout layout = new GlyphLayout();
	protected final FloatArray glyphPositions = new FloatArray();

	private String messageText;
	protected CharSequence displayText;
	Clipboard clipboard;
	InputListener inputListener;
	TextFieldListener listener;
	TextFieldFilter filter;
	OnscreenKeyboard keyboard = new DefaultOnscreenKeyboard();
	boolean focusTraversal = true, onlyFontChars = true, disabled;
	boolean enterKeyFocusTraversal = false;
	private int textHAlign = Align.left;
	private float selectionX, selectionWidth;

	String undoText = "";
	int undoCursorPos = 0;
	long lastChangeTime;

	boolean passwordMode;
	private StringBuilder passwordBuffer;
	private char passwordCharacter = BULLET;

	protected float fontOffset, textHeight, textOffset;
	float renderOffset;
	private int visibleTextStart, visibleTextEnd;
	private int maxLength = 0;

	private float blinkTime = 0.45f;
	boolean cursorOn = true;
	long lastBlink;

	KeyRepeatTask keyRepeatTask = new KeyRepeatTask();
	boolean programmaticChangeEvents;

	// vis fields
	VisTextFieldStyle style;
	private ClickListener clickListener;
	private boolean drawBorder;
	private boolean focusBorderEnabled = true;
	private boolean inputValid = true;
	private boolean ignoreEqualsTextChange = true;
	private boolean readOnly = false;
	private float cursorPercentHeight = 0.8f;

	public VisTextField () {
		this("", VisUI.getSkin().get(VisTextFieldStyle.class));
	}

	public VisTextField (String text) {
		this(text, VisUI.getSkin().get(VisTextFieldStyle.class));
	}

	public VisTextField (String text, String styleName) {
		this(text, VisUI.getSkin().get(styleName, VisTextFieldStyle.class));
	}

	public VisTextField (String text, VisTextFieldStyle style) {
		setStyle(style);
		clipboard = Gdx.app.getClipboard();
		initialize();
		setText(text);
		setSize(getPrefWidth(), getPrefHeight());
	}

	protected void initialize () {
		addListener(inputListener = createInputListener());
		addListener(clickListener = new ClickListener() {
			@Override
			public void enter (InputEvent event, float x, float y, int pointer, Actor fromActor) {
				super.enter(event, x, y, pointer, fromActor);
				if (pointer == -1 && isDisabled() == false) {
					Gdx.graphics.setSystemCursor(SystemCursor.Ibeam);
				}
			}

			@Override
			public void exit (InputEvent event, float x, float y, int pointer, Actor toActor) {
				super.exit(event, x, y, pointer, toActor);
				if (pointer == -1) {
					CursorManager.restoreDefaultCursor();
				}
			}
		});
	}

	protected InputListener createInputListener () {
		return new TextFieldClickListener();
	}

	protected int letterUnderCursor (float x) {
		x -= textOffset + fontOffset - style.font.getData().cursorX - glyphPositions.get(visibleTextStart);
		int n = this.glyphPositions.size;
		float[] glyphPositions = this.glyphPositions.items;
		for (int i = 1; i < n; i++) {
			if (glyphPositions[i] > x) {
				if (glyphPositions[i] - x <= x - glyphPositions[i - 1]) return i;
				return i - 1;
			}
		}
		return n - 1;
	}

	protected boolean isWordCharacter (char c) {
		return Character.isLetterOrDigit(c);
	}

	protected int[] wordUnderCursor (int at) {
		String text = this.text;
		int start = Math.min(text.length(), at), right = text.length(), left = 0, index = start;
		for (; index < right; index++) {
			if (!isWordCharacter(text.charAt(index))) {
				right = index;
				break;
			}
		}
		for (index = start - 1; index > -1; index--) {
			if (!isWordCharacter(text.charAt(index))) {
				left = index + 1;
				break;
			}
		}
		return new int[]{left, right};
	}

	int[] wordUnderCursor (float x) {
		return wordUnderCursor(letterUnderCursor(x));
	}

	boolean withinMaxLength (int size) {
		return maxLength <= 0 || size < maxLength;
	}

	public int getMaxLength () {
		return this.maxLength;
	}

	public void setMaxLength (int maxLength) {
		this.maxLength = maxLength;
	}

	/**
	 * When false, text set by {@link #setText(String)} may contain characters not in the font, a space will be displayed instead.
	 * When true (the default), characters not in the font are stripped by setText. Characters not in the font are always stripped
	 * when typed or pasted.
	 */
	public void setOnlyFontChars (boolean onlyFontChars) {
		this.onlyFontChars = onlyFontChars;
	}

	/**
	 * Returns the text field's style. Modifying the returned style may not have an effect until
	 * {@link #setStyle(VisTextFieldStyle)} is called.
	 */
	public VisTextFieldStyle getStyle () {
		return style;
	}

	public void setStyle (VisTextFieldStyle style) {
		if (style == null) throw new IllegalArgumentException("style cannot be null.");
		this.style = style;
		textHeight = style.font.getCapHeight() - style.font.getDescent() * 2;
		invalidateHierarchy();
	}

	@Override
	public String toString () {
		return getText();
	}

	protected void calculateOffsets () {
		float visibleWidth = getWidth();
		if (style.background != null)
			visibleWidth -= style.background.getLeftWidth() + style.background.getRightWidth();

		int glyphCount = glyphPositions.size;
		float[] glyphPositions = this.glyphPositions.items;

		// Check if the cursor has gone out the left or right side of the visible area and adjust renderOffset.
		float distance = glyphPositions[Math.max(0, cursor - 1)] + renderOffset;
		if (distance <= 0)
			renderOffset -= distance;
		else {
			int index = Math.min(glyphCount - 1, cursor + 1);
			float minX = glyphPositions[index] - visibleWidth;
			if (-renderOffset < minX) renderOffset = -minX;
		}

		// Prevent renderOffset from starting too close to the end, eg after text was deleted.
		float maxOffset = 0;
		float width = glyphPositions[glyphCount - 1];
		for (int i = glyphCount - 2; i >= 0; i--) {
			float x = glyphPositions[i];
			if (width - x > visibleWidth) break;
			maxOffset = x;
		}
		if (-renderOffset > maxOffset) renderOffset = -maxOffset;

		// calculate first visible char based on render offset
		visibleTextStart = 0;
		float startX = 0;
		for (int i = 0; i < glyphCount; i++) {
			if (glyphPositions[i] >= -renderOffset) {
				visibleTextStart = Math.max(0, i);
				startX = glyphPositions[i];
				break;
			}
		}

		// calculate last visible char based on visible width and render offset
		int length = Math.min(displayText.length(), glyphPositions.length - 1);
		visibleTextEnd = Math.min(length, cursor + 1);
		for (; visibleTextEnd <= length; visibleTextEnd++)
			if (glyphPositions[visibleTextEnd] > startX + visibleWidth) break;
		visibleTextEnd = Math.max(0, visibleTextEnd - 1);

		if ((textHAlign & Align.left) == 0) {
			textOffset = visibleWidth - (glyphPositions[visibleTextEnd] - startX);
			if ((textHAlign & Align.center) != 0) textOffset = Math.round(textOffset * 0.5f);
		} else
			textOffset = startX + renderOffset;

		// calculate selection x position and width
		if (hasSelection) {
			int minIndex = Math.min(cursor, selectionStart);
			int maxIndex = Math.max(cursor, selectionStart);
			float minX = Math.max(glyphPositions[minIndex] - glyphPositions[visibleTextStart], -textOffset);
			float maxX = Math.min(glyphPositions[maxIndex] - glyphPositions[visibleTextStart], visibleWidth - textOffset);
			selectionX = minX;
			selectionWidth = maxX - minX - style.font.getData().cursorX;
		}
	}

	@Override
	public void draw (Batch batch, float parentAlpha) {
		Stage stage = getStage();
		boolean focused = stage != null && stage.getKeyboardFocus() == this;
		if (!focused) keyRepeatTask.cancel();

		final BitmapFont font = style.font;
		final Color fontColor = (disabled && style.disabledFontColor != null) ? style.disabledFontColor
				: ((focused && style.focusedFontColor != null) ? style.focusedFontColor : style.fontColor);
		final Drawable selection = style.selection;
		final Drawable cursorPatch = style.cursor;
		Drawable background = (disabled && style.disabledBackground != null) ? style.disabledBackground
				: ((focused && style.focusedBackground != null) ? style.focusedBackground : style.background);

		// vis
		if (!disabled && style.backgroundOver != null && (clickListener.isOver() || focused)) {
			background = style.backgroundOver;
		}

		Color color = getColor();
		float x = getX();
		float y = getY();
		float width = getWidth();
		float height = getHeight();

		batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);
		float bgLeftWidth = 0, bgRightWidth = 0;
		if (background != null) {
			background.draw(batch, x, y, width, height);
			bgLeftWidth = background.getLeftWidth();
			bgRightWidth = background.getRightWidth();
		}

		float textY = getTextY(font, background);
		calculateOffsets();

		if (focused && hasSelection && selection != null) {
			drawSelection(selection, batch, font, x + bgLeftWidth, y + textY);
		}

		float yOffset = font.isFlipped() ? -textHeight : 0;
		if (displayText.length() == 0) {
			if (!focused && messageText != null) {
				if (style.messageFontColor != null) {
					font.setColor(style.messageFontColor.r, style.messageFontColor.g, style.messageFontColor.b,
							style.messageFontColor.a * color.a * parentAlpha);
				} else
					font.setColor(0.7f, 0.7f, 0.7f, color.a * parentAlpha);
				BitmapFont messageFont = style.messageFont != null ? style.messageFont : font;
				messageFont.draw(batch, messageText, x + bgLeftWidth, y + textY + yOffset, 0, messageText.length(),
						width - bgLeftWidth - bgRightWidth, textHAlign, false, "...");
			}
		} else {
			font.setColor(fontColor.r, fontColor.g, fontColor.b, fontColor.a * color.a * parentAlpha);
			drawText(batch, font, x + bgLeftWidth, y + textY + yOffset);
		}
		if (drawBorder && focused && !disabled) {
			blink();
			if (cursorOn && cursorPatch != null) {
				drawCursor(cursorPatch, batch, font, x + bgLeftWidth, y + textY);
			}
		}

		// vis
		if (isDisabled() == false && inputValid == false && style.errorBorder != null)
			style.errorBorder.draw(batch, getX(), getY(), getWidth(), getHeight());
		else if (focusBorderEnabled && drawBorder && style.focusBorder != null)
			style.focusBorder.draw(batch, getX(), getY(), getWidth(), getHeight());

	}

	protected float getTextY (BitmapFont font, Drawable background) {
		float height = getHeight();
		float textY = textHeight / 2 + font.getDescent();
		if (background != null) {
			float bottom = background.getBottomHeight();
			textY = textY + (height - background.getTopHeight() - bottom) / 2 + bottom;
		} else {
			textY = textY + height / 2;
		}
		if (font.usesIntegerPositions()) textY = (int) textY;
		return textY;
	}

	/** Draws selection rectangle **/
	protected void drawSelection (Drawable selection, Batch batch, BitmapFont font, float x, float y) {
		selection.draw(batch, x + selectionX + textOffset + fontOffset, y - textHeight - font.getDescent(), selectionWidth,
				textHeight);
	}

	protected void drawText (Batch batch, BitmapFont font, float x, float y) {
		font.draw(batch, displayText, x + textOffset, y, visibleTextStart, visibleTextEnd, 0, Align.left, false);
	}

	protected void drawCursor (Drawable cursorPatch, Batch batch, BitmapFont font, float x, float y) {
		float cursorHeight = textHeight * cursorPercentHeight;
		float cursorYPadding = (textHeight - cursorHeight) / 2;
		cursorPatch.draw(batch,
				x + textOffset + glyphPositions.get(cursor) - glyphPositions.get(visibleTextStart) + fontOffset + font.getData().cursorX,
				y - textHeight - font.getDescent() + cursorYPadding, cursorPatch.getMinWidth(), cursorHeight);
	}

	void updateDisplayText () {
		BitmapFont font = style.font;
		BitmapFontData data = font.getData();
		String text = this.text;
		int textLength = text.length();

		StringBuilder buffer = new StringBuilder();
		for (int i = 0; i < textLength; i++) {
			char c = text.charAt(i);
			buffer.append(data.hasGlyph(c) ? c : ' ');
		}
		String newDisplayText = buffer.toString();

		if (passwordMode && data.hasGlyph(passwordCharacter)) {
			if (passwordBuffer == null) passwordBuffer = new StringBuilder(newDisplayText.length());
			if (passwordBuffer.length() > textLength)
				passwordBuffer.setLength(textLength);
			else {
				for (int i = passwordBuffer.length(); i < textLength; i++)
					passwordBuffer.append(passwordCharacter);
			}
			displayText = passwordBuffer;
		} else
			displayText = newDisplayText;

		layout.setText(font, displayText);
		glyphPositions.clear();
		float x = 0;
		if (layout.runs.size > 0) {
			GlyphRun run = layout.runs.first();
			fontOffset = run.xAdvances.first();

			for (GlyphRun glyphRun : layout.runs) {
				FloatArray xAdvances = glyphRun.xAdvances;
				for (int i = 1, n = xAdvances.size; i < n; i++) {
					glyphPositions.add(x);
					x += xAdvances.get(i);
				}
				glyphPositions.add(x);
			}
		} else {
			fontOffset = 0;
		}
		glyphPositions.add(x);

		if (selectionStart > newDisplayText.length()) selectionStart = textLength;
	}

	private void blink () {
		if (!Gdx.graphics.isContinuousRendering()) {
			cursorOn = true;
			return;
		}
		long time = TimeUtils.nanoTime();
		if ((time - lastBlink) / 1000000000.0f > blinkTime) {
			cursorOn = !cursorOn;
			lastBlink = time;
		}
	}

	/** Copies the contents of this TextField to the {@link Clipboard} implementation set on this TextField. */
	public void copy () {
		if (hasSelection && !passwordMode) {
			int beginIndex = Math.min(cursor, selectionStart);
			int endIndex = Math.max(cursor, selectionStart);
			clipboard.setContents(text.substring(Math.max(0, beginIndex), Math.min(text.length(), endIndex)));
		}
	}

	/**
	 * Copies the selected contents of this TextField to the {@link Clipboard} implementation set on this TextField, then removes
	 * it.
	 */
	public void cut () {
		cut(programmaticChangeEvents);
	}

	void cut (boolean fireChangeEvent) {
		if (hasSelection && !passwordMode) {
			copy();
			cursor = delete(fireChangeEvent);
			updateDisplayText();
		}
	}

	void paste (String content, boolean fireChangeEvent) {
		if (content == null) return;
		StringBuilder buffer = new StringBuilder();
		int textLength = text.length();
		if (hasSelection) textLength -= Math.abs(cursor - selectionStart);
		BitmapFontData data = style.font.getData();
		for (int i = 0, n = content.length(); i < n; i++) {
			if (!withinMaxLength(textLength + buffer.length())) break;
			char c = content.charAt(i);
			if (!(writeEnters && (c == ENTER_ANDROID || c == ENTER_DESKTOP))) {
				if (c == '\r' || c == '\n') continue;
				if (onlyFontChars && !data.hasGlyph(c)) continue;
				if (filter != null && !filter.acceptChar(this, c)) continue;
			}
			buffer.append(c);
		}
		content = buffer.toString();

		if (hasSelection) cursor = delete(fireChangeEvent);
		if (fireChangeEvent)
			changeText(text, insert(cursor, content, text));
		else
			text = insert(cursor, content, text);
		updateDisplayText();
		cursor += content.length();
	}

	String insert (int position, CharSequence text, String to) {
		if (to.length() == 0) return text.toString();
		return to.substring(0, position) + text + to.substring(position, to.length());
	}

	int delete (boolean fireChangeEvent) {
		int from = selectionStart;
		int to = cursor;
		int minIndex = Math.min(from, to);
		int maxIndex = Math.max(from, to);
		String newText = (minIndex > 0 ? text.substring(0, minIndex) : "")
				+ (maxIndex < text.length() ? text.substring(maxIndex, text.length()) : "");
		if (fireChangeEvent)
			changeText(text, newText);
		else
			text = newText;
		clearSelection();
		return minIndex;
	}

	/**
	 * Focuses the next TextField. If none is found, the keyboard is hidden. Does nothing if the text field is not in a stage.
	 * @param up If true, the TextField with the same or next smallest y coordinate is found, else the next highest.
	 */
	public void next (boolean up) {
		Stage stage = getStage();
		if (stage == null) return;
		getParent().localToStageCoordinates(tmp1.set(getX(), getY()));
		VisTextField textField = findNextTextField(stage.getActors(), null, tmp2, tmp1, up);
		if (textField == null) { // Try to wrap around.
			if (up)
				tmp1.set(Float.MIN_VALUE, Float.MIN_VALUE);
			else
				tmp1.set(Float.MAX_VALUE, Float.MAX_VALUE);
			textField = findNextTextField(getStage().getActors(), null, tmp2, tmp1, up);
		}
		if (textField != null) {
			textField.focusField();
			textField.setCursorPosition(textField.getText().length());
		} else
			keyboard.show(false);
	}

	private VisTextField findNextTextField (Array<Actor> actors, VisTextField best, Vector2 bestCoords, Vector2 currentCoords, boolean up) {
		Window modalWindow = findModalWindow(this);

		for (int i = 0, n = actors.size; i < n; i++) {
			Actor actor = actors.get(i);
			if (actor == this) continue;
			if (actor instanceof VisTextField) {
				VisTextField textField = (VisTextField) actor;

				if (modalWindow != null) {
					Window nextFieldModalWindow = findModalWindow(textField);
					if (nextFieldModalWindow != modalWindow) continue;
				}

				if (textField.isDisabled() || !textField.focusTraversal || isActorVisibleInStage(textField) == false)
					continue;

				Vector2 actorCoords = actor.getParent().localToStageCoordinates(tmp3.set(actor.getX(), actor.getY()));
				if ((actorCoords.y < currentCoords.y || (actorCoords.y == currentCoords.y && actorCoords.x > currentCoords.x)) ^ up) {
					if (best == null
							|| (actorCoords.y > bestCoords.y || (actorCoords.y == bestCoords.y && actorCoords.x < bestCoords.x)) ^ up) {
						best = (VisTextField) actor;
						bestCoords.set(actorCoords);
					}
				}
			} else if (actor instanceof Group)
				best = findNextTextField(((Group) actor).getChildren(), best, bestCoords, currentCoords, up);
		}
		return best;
	}

	/**
	 * Checks if actor is visible in stage acknowledging parent visibility.
	 * If any parent returns false from isVisible then this method return false.
	 * True is returned when this actor and all its parent are visible.
	 */
	private boolean isActorVisibleInStage (Actor actor) {
		if (actor == null) return true;
		if (actor.isVisible() == false) return false;
		return isActorVisibleInStage(actor.getParent());
	}

	private Window findModalWindow (Actor actor) {
		if (actor == null) return null;
		if (actor instanceof Window && ((Window) actor).isModal()) return (Window) actor;
		return findModalWindow(actor.getParent());
	}

	public InputListener getDefaultInputListener () {
		return inputListener;
	}

	/** @param listener May be null. */
	public void setTextFieldListener (TextFieldListener listener) {
		this.listener = listener;
	}

	/** @param filter May be null. */
	public void setTextFieldFilter (TextFieldFilter filter) {
		this.filter = filter;
	}

	public TextFieldFilter getTextFieldFilter () {
		return filter;
	}

	/** If true (the default), tab/shift+tab will move to the next text field. */
	public void setFocusTraversal (boolean focusTraversal) {
		this.focusTraversal = focusTraversal;
	}

	/**
	 * If true, enter will move to the next text field with has focus traversal enabled.
	 * False by default. Note that to enable or disable focus traversal completely you must
	 * use {@link #setFocusTraversal(boolean)}
	 */
	public void setEnterKeyFocusTraversal (boolean enterKeyFocusTraversal) {
		this.enterKeyFocusTraversal = enterKeyFocusTraversal;
	}

	/** @return May be null. */
	public String getMessageText () {
		return messageText;
	}

	/**
	 * Sets the text that will be drawn in the text field if no text has been entered.
	 * @param messageText may be null.
	 */
	public void setMessageText (String messageText) {
		this.messageText = messageText;
	}

	/** @param str If null, "" is used. */
	public void appendText (String str) {
		if (str == null) str = "";

		clearSelection();
		cursor = text.length();
		paste(str, programmaticChangeEvents);
	}

	/** @param str If null, "" is used. */
	public void setText (String str) {
		if (str == null) str = "";
		if (ignoreEqualsTextChange && str.equals(text)) return;

		clearSelection();
		String oldText = text;
		text = "";
		paste(str, false);
		if (programmaticChangeEvents) changeText(oldText, text);
		cursor = 0;
	}

	/** @return Never null, might be an empty string. */
	public String getText () {
		return text;
	}

	/**
	 * @param oldText May be null.
	 * @return True if the text was changed.
	 */
	boolean changeText (String oldText, String newText) {
		if (ignoreEqualsTextChange && newText.equals(oldText)) return false;
		text = newText;
		beforeChangeEventFired();
		ChangeEvent changeEvent = Pools.obtain(ChangeEvent.class);
		boolean cancelled = fire(changeEvent);
		text = cancelled ? oldText : newText;
		Pools.free(changeEvent);
		return !cancelled;
	}

	void beforeChangeEventFired () {

	}

	public boolean getProgrammaticChangeEvents () {
		return programmaticChangeEvents;
	}

	/**
	 * If false, methods that change the text will not fire {@link ChangeEvent}, the event will be fired only when user changes
	 * the text.
	 */
	public void setProgrammaticChangeEvents (boolean programmaticChangeEvents) {
		this.programmaticChangeEvents = programmaticChangeEvents;
	}

	public int getSelectionStart () {
		return selectionStart;
	}

	public String getSelection () {
		return hasSelection ? text.substring(Math.min(selectionStart, cursor), Math.max(selectionStart, cursor)) : "";
	}

	public boolean isTextSelected () {
		return hasSelection;
	}

	/** Sets the selected text. */
	public void setSelection (int selectionStart, int selectionEnd) {
		if (selectionStart < 0) throw new IllegalArgumentException("selectionStart must be >= 0");
		if (selectionEnd < 0) throw new IllegalArgumentException("selectionEnd must be >= 0");
		selectionStart = Math.min(text.length(), selectionStart);
		selectionEnd = Math.min(text.length(), selectionEnd);
		if (selectionEnd == selectionStart) {
			clearSelection();
			return;
		}
		if (selectionEnd < selectionStart) {
			int temp = selectionEnd;
			selectionEnd = selectionStart;
			selectionStart = temp;
		}

		hasSelection = true;
		this.selectionStart = selectionStart;
		cursor = selectionEnd;
	}

	public void selectAll () {
		setSelection(0, text.length());
	}

	public void clearSelection () {
		hasSelection = false;
	}

	/** Clears VisTextField text. If programmatic change events are disabled then this will not fire change event. */
	public void clearText () {
		setText("");
	}

	/** Sets the cursor position and clears any selection. */
	public void setCursorPosition (int cursorPosition) {
		if (cursorPosition < 0) throw new IllegalArgumentException("cursorPosition must be >= 0");
		clearSelection();
		cursor = Math.min(cursorPosition, text.length());
	}

	public int getCursorPosition () {
		return cursor;
	}

	public void setCursorAtTextEnd () {
		setCursorPosition(0);
		calculateOffsets();
		setCursorPosition(getText().length());
	}

	/** @param cursorPercentHeight cursor size, value from 0..1 range */
	public void setCursorPercentHeight (float cursorPercentHeight) {
		if (cursorPercentHeight < 0 || cursorPercentHeight > 1)
			throw new IllegalArgumentException("cursorPercentHeight must be >= 0 and <= 1");
		this.cursorPercentHeight = cursorPercentHeight;
	}

	/** Default is an instance of {@link DefaultOnscreenKeyboard}. */
	public OnscreenKeyboard getOnscreenKeyboard () {
		return keyboard;
	}

	public void setOnscreenKeyboard (OnscreenKeyboard keyboard) {
		this.keyboard = keyboard;
	}

	public void setClipboard (Clipboard clipboard) {
		this.clipboard = clipboard;
	}

	@Override
	public float getPrefWidth () {
		return 150;
	}

	@Override
	public float getPrefHeight () {
		float prefHeight = textHeight;
		if (style.background != null) {
			prefHeight = Math.max(prefHeight + style.background.getBottomHeight() + style.background.getTopHeight(),
					style.background.getMinHeight());
		}
		return prefHeight;
	}

	/**
	 * Sets text horizontal alignment (left, center or right).
	 * @see Align
	 */
	public void setAlignment (int alignment) {
		this.textHAlign = alignment;
	}

	/**
	 * If true, the text in this text field will be shown as bullet characters.
	 * @see #setPasswordCharacter(char)
	 */
	public void setPasswordMode (boolean passwordMode) {
		this.passwordMode = passwordMode;
		updateDisplayText();
	}

	public boolean isPasswordMode () {
		return passwordMode;
	}

	/**
	 * Sets the password character for the text field. The character must be present in the {@link BitmapFont}. Default is 149
	 * (bullet).
	 */
	public void setPasswordCharacter (char passwordCharacter) {
		this.passwordCharacter = passwordCharacter;
		if (passwordMode) updateDisplayText();
	}

	public void setBlinkTime (float blinkTime) {
		this.blinkTime = blinkTime;
	}

	public boolean isDisabled () {
		return disabled;
	}

	@Override
	public void setDisabled (boolean disabled) {
		this.disabled = disabled;
		if (disabled) {
			FocusManager.resetFocus(getStage(), this);
			keyRepeatTask.cancel();
		}
	}

	public boolean isReadOnly () {
		return readOnly;
	}

	public void setReadOnly (boolean readOnly) {
		this.readOnly = readOnly;
	}

	protected void moveCursor (boolean forward, boolean jump) {
		int limit = forward ? text.length() : 0;
		int charOffset = forward ? 0 : -1;
		while ((forward ? ++cursor < limit : --cursor > limit) && jump) {
			if (!continueCursor(cursor, charOffset)) break;
		}
	}

	protected boolean continueCursor (int index, int offset) {
		char c = text.charAt(index + offset);
		return isWordCharacter(c);
	}

	/** Focuses this field, field must be added to stage before this method can be called */
	public void focusField () {
		if (disabled) return;
		Stage stage = getStage();
		FocusManager.switchFocus(stage, VisTextField.this);
		setCursorPosition(0);
		selectionStart = 0;
		//make sure textOffset was updated, prevent issue when there was long text selected and it was changed to short text
		//and field was focused. Without it textOffset would stay at max value and only one last letter will be visible in field
		calculateOffsets();
		if (stage != null) stage.setKeyboardFocus(VisTextField.this);
		keyboard.show(true);
		hasSelection = true;
	}

	@Override
	public void focusLost () {
		drawBorder = false;
	}

	@Override
	public void focusGained () {
		drawBorder = true;
	}

	public boolean isEmpty () {
		return text.length() == 0;
	}

	public boolean isInputValid () {
		return inputValid;
	}

	public void setInputValid (boolean inputValid) {
		this.inputValid = inputValid;
	}

	@Override
	public boolean isFocusBorderEnabled () {
		return focusBorderEnabled;
	}

	@Override
	public void setFocusBorderEnabled (boolean focusBorderEnabled) {
		this.focusBorderEnabled = focusBorderEnabled;
	}

	/** @see #setIgnoreEqualsTextChange(boolean) */
	public boolean isIgnoreEqualsTextChange () {
		return ignoreEqualsTextChange;
	}

	/**
	 * Allows to control whether change event is sent when text field's text is changed to same same as was it before.
	 * Eg. current text field is 'abc' and {@link #setText(String)} is called it with 'abc' again.
	 * @param ignoreEqualsTextChange if true then setting text to the same as it was before will NOT fire change event.
	 * Default is true however it is false default {@link VisValidatableTextField} to prevent form refreshment issues -
	 * see issue VisEditor#165
	 */
	public void setIgnoreEqualsTextChange (boolean ignoreEqualsTextChange) {
		this.ignoreEqualsTextChange = ignoreEqualsTextChange;
	}

	static public class VisTextFieldStyle extends TextFieldStyle {
		public Drawable focusBorder;
		public Drawable errorBorder;
		public Drawable backgroundOver;

		public VisTextFieldStyle () {
		}

		public VisTextFieldStyle (BitmapFont font, Color fontColor, Drawable cursor, Drawable selection, Drawable background) {
			super(font, fontColor, cursor, selection, background);
		}

		public VisTextFieldStyle (VisTextFieldStyle style) {
			super(style);
			this.focusBorder = style.focusBorder;
			this.errorBorder = style.errorBorder;
			this.backgroundOver = style.backgroundOver;
		}
	}

	/**
	 * Interface for listening to typed characters.
	 * @author mzechner
	 */
	static public interface TextFieldListener {
		public void keyTyped (VisTextField textField, char c);
	}

	/**
	 * Interface for filtering characters entered into the text field.
	 * @author mzechner
	 */
	static public interface TextFieldFilter {
		public boolean acceptChar (VisTextField textField, char c);

		static public class DigitsOnlyFilter implements TextFieldFilter {
			@Override
			public boolean acceptChar (VisTextField textField, char c) {
				return Character.isDigit(c);
			}

		}
	}

	class KeyRepeatTask extends Task {
		int keycode;

		@Override
		public void run () {
			inputListener.keyDown(null, keycode);
		}
	}

	/** Basic input listener for the text field */
	public class TextFieldClickListener extends ClickListener {
		@Override
		public void clicked (InputEvent event, float x, float y) {
			int count = getTapCount() % 4;
			if (count == 0) clearSelection();
			if (count == 2) {
				int[] array = wordUnderCursor(x);
				setSelection(array[0], array[1]);
			}
			if (count == 3) selectAll();
		}

		@Override
		public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
			if (!super.touchDown(event, x, y, pointer, button)) return false;
			if (pointer == 0 && button != 0) return false;
			if (disabled) return true;
			Stage stage = getStage();
			FocusManager.switchFocus(stage, VisTextField.this);
			setCursorPosition(x, y);
			selectionStart = cursor;
			if (stage != null) stage.setKeyboardFocus(VisTextField.this);
			if (readOnly == false) keyboard.show(true);
			hasSelection = true;
			return true;
		}

		@Override
		public void touchDragged (InputEvent event, float x, float y, int pointer) {
			super.touchDragged(event, x, y, pointer);
			setCursorPosition(x, y);
		}

		@Override
		public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
			if (selectionStart == cursor) hasSelection = false;
			super.touchUp(event, x, y, pointer, button);
		}

		protected void setCursorPosition (float x, float y) {
			lastBlink = 0;
			cursorOn = false;
			cursor = Math.min(letterUnderCursor(x), text.length());
		}

		protected void goHome (boolean jump) {
			cursor = 0;
		}

		protected void goEnd (boolean jump) {
			cursor = text.length();
		}

		@Override
		public boolean keyDown (InputEvent event, int keycode) {
			if (disabled) return false;

			lastBlink = 0;
			cursorOn = false;

			Stage stage = getStage();
			if (stage == null || stage.getKeyboardFocus() != VisTextField.this) return false;
			if (drawBorder == false) return false;

			boolean repeat = false;
			boolean ctrl = UIUtils.ctrl();
			boolean jump = ctrl && !passwordMode;

			if (ctrl) {
				if (keycode == Keys.V && readOnly == false) {
					paste(clipboard.getContents(), true);
					repeat = true;
				}
				if (keycode == Keys.C || keycode == Keys.INSERT) {
					copy();
					return true;
				}
				if (keycode == Keys.X && readOnly == false) {
					cut(true);
					return true;
				}
				if (keycode == Keys.A) {
					selectAll();
					return true;
				}
				if (keycode == Keys.Z && readOnly == false) {
					String oldText = text;
					int oldCursorPos = getCursorPosition();
					setText(undoText);
					VisTextField.this.setCursorPosition(MathUtils.clamp(cursor, 0, undoText.length()));
					undoText = oldText;
					undoCursorPos = oldCursorPos;
					updateDisplayText();
					return true;
				}
			}

			if (UIUtils.shift()) {
				if (keycode == Keys.INSERT && readOnly == false) paste(clipboard.getContents(), true);
				if (keycode == Keys.FORWARD_DEL && readOnly == false) cut(true);
				selection:
				{
					int temp = cursor;
					keys:
					{
						if (keycode == Keys.LEFT) {
							moveCursor(false, jump);
							repeat = true;
							break keys;
						}
						if (keycode == Keys.RIGHT) {
							moveCursor(true, jump);
							repeat = true;
							break keys;
						}
						if (keycode == Keys.HOME) {
							goHome(jump);
							break keys;
						}
						if (keycode == Keys.END) {
							goEnd(jump);
							break keys;
						}
						break selection;
					}
					if (!hasSelection) {
						selectionStart = temp;
						hasSelection = true;
					}
				}
			} else {
				// Cursor movement or other keys (kills selection).
				if (keycode == Keys.LEFT) {
					moveCursor(false, jump);
					clearSelection();
					repeat = true;
				}
				if (keycode == Keys.RIGHT) {
					moveCursor(true, jump);
					clearSelection();
					repeat = true;
				}
				if (keycode == Keys.HOME) {
					goHome(jump);
					clearSelection();
				}
				if (keycode == Keys.END) {
					goEnd(jump);
					clearSelection();
				}
			}
			cursor = MathUtils.clamp(cursor, 0, text.length());

			if (repeat) {
				scheduleKeyRepeatTask(keycode);
			}
			return true;
		}

		protected void scheduleKeyRepeatTask (int keycode) {
			if (!keyRepeatTask.isScheduled() || keyRepeatTask.keycode != keycode) {
				keyRepeatTask.keycode = keycode;
				keyRepeatTask.cancel();
				if (Gdx.input.isKeyPressed(keyRepeatTask.keycode)) { //issue #179
					Timer.schedule(keyRepeatTask, keyRepeatInitialTime, keyRepeatTime);
				}
			}
		}

		@Override
		public boolean keyUp (InputEvent event, int keycode) {
			if (disabled) return false;
			keyRepeatTask.cancel();
			return true;
		}

		@Override
		public boolean keyTyped (InputEvent event, char character) {
			if (disabled || readOnly) return false;

			// Disallow "typing" most ASCII control characters, which would show up as a space when onlyFontChars is true.
			switch (character) {
				case BACKSPACE:
				case TAB:
				case ENTER_ANDROID:
				case ENTER_DESKTOP:
					break;
				default:
					if (character < 32) return false;
			}

			Stage stage = getStage();
			if (stage == null || stage.getKeyboardFocus() != VisTextField.this) return false;

			if (UIUtils.isMac && Gdx.input.isKeyPressed(Keys.SYM)) return true;

			if (focusTraversal && (character == TAB || (character == ENTER_ANDROID && enterKeyFocusTraversal))) {
				next(UIUtils.shift());
			} else {
				boolean delete = character == DELETE;
				boolean backspace = character == BACKSPACE;
				boolean enter = character == ENTER_DESKTOP || character == ENTER_ANDROID;
				boolean add = enter ? writeEnters : (!onlyFontChars || style.font.getData().hasGlyph(character));
				boolean remove = backspace || delete;
				if (add || remove) {
					String oldText = text;
					int oldCursor = cursor;
					if (hasSelection)
						cursor = delete(false);
					else {
						if (backspace && cursor > 0) {
							text = text.substring(0, cursor - 1) + text.substring(cursor--);
							renderOffset = 0;
						}
						if (delete && cursor < text.length()) {
							text = text.substring(0, cursor) + text.substring(cursor + 1);
						}
					}
					if (add && !remove) {
						// Character may be added to the text.
						if (!enter && filter != null && !filter.acceptChar(VisTextField.this, character)) return true;
						if (!withinMaxLength(text.length())) return true;
						String insertion = enter ? "\n" : String.valueOf(character);
						text = insert(cursor++, insertion, text);
					}
					if (changeText(oldText, text)) {
						long time = System.currentTimeMillis();
						if (time - 750 > lastChangeTime) {
							undoText = oldText;
							undoCursorPos = getCursorPosition() - 1;
						}
						lastChangeTime = time;
					} else
						cursor = oldCursor;
					updateDisplayText();
				}
			}
			if (listener != null) listener.keyTyped(VisTextField.this, character);
			return true;
		}
	}
}