//
// TextView.cs: multi-line text editing
//
// Authors:
//   Miguel de Icaza (miguel@gnome.org)
//
// 
// TODO:
// In ReadOnly mode backspace/space behave like pageup/pagedown
// Attributed text on spans
// Replace insertion with Insert method
// String accumulation (Control-k, control-k is not preserving the last new line, see StringToRunes
// Alt-D, Alt-Backspace
// API to set the cursor position
// API to scroll to a particular place
// keybindings to go to top/bottom
// public API to insert, remove ranges
// Add word forward/word backwards commands
// Save buffer API
// Mouse
//
// Desirable:
//   Move all the text manipulation into the TextModel


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NStack;

using Rune = System.Rune;

namespace Terminal.Gui {
	class TextModel {
		List<List<Rune>> lines = new List<List<Rune>> ();

		public bool LoadFile (string file)
		{
			if (file == null)
				throw new ArgumentNullException (nameof (file));
			try {
				FilePath = file;
				var stream = File.OpenRead (file);
			} catch {
				return false;
			}
			LoadStream (File.OpenRead (file));
			return true;
		}

		public bool CloseFile ()
		{
			if (FilePath == null)
				throw new ArgumentNullException (nameof (FilePath));
			try {
				FilePath = null;
				lines = new List<List<Rune>> ();
			} catch {
				return false;
			}
			return true;
		}

		// Turns the ustring into runes, this does not split the 
		// contents on a newline if it is present.
		internal static List<Rune> ToRunes (ustring str)
		{
			List<Rune> runes = new List<Rune> ();
			foreach (var x in str.ToRunes ()) {
				runes.Add (x);
			}
			return runes;
		}

		// Splits a string into a List that contains a List<Rune> for each line
		public static List<List<Rune>> StringToRunes (ustring content)
		{
			var lines = new List<List<Rune>> ();
			int start = 0, i = 0;
			// ASCII code 10 = Line Feed.
			for (; i < content.Length; i++) {
				if (content [i] == 10) {
					if (i - start > 0)
						lines.Add (ToRunes (content [start, i]));
					else
						lines.Add (ToRunes (ustring.Empty));
					start = i + 1;
				}
			}
			if (i - start >= 0)
				lines.Add (ToRunes (content [start, null]));
			return lines;
		}

		void Append (List<byte> line)
		{
			var str = ustring.Make (line.ToArray ());
			lines.Add (ToRunes (str));
		}

		public void LoadStream (Stream input)
		{
			if (input == null)
				throw new ArgumentNullException (nameof (input));

			lines = new List<List<Rune>> ();
			var buff = new BufferedStream (input);
			int v;
			var line = new List<byte> ();
			while ((v = buff.ReadByte ()) != -1) {
				if (v == 10) {
					Append (line);
					line.Clear ();
					continue;
				}
				line.Add ((byte)v);
			}
			if (line.Count > 0)
				Append (line);
		}

		public void LoadString (ustring content)
		{
			lines = StringToRunes (content);
		}

		public override string ToString ()
		{
			var sb = new StringBuilder ();
			for (int i = 0; i < lines.Count; i++) {
				sb.Append (ustring.Make (lines [i]));
				if ((i + 1) < lines.Count) {
					sb.AppendLine ();
				}
			}
			return sb.ToString ();
		}

		public string FilePath { get; set; }

		/// <summary>
		/// The number of text lines in the model
		/// </summary>
		public int Count => lines.Count;

		/// <summary>
		/// Returns the specified line as a List of Rune
		/// </summary>
		/// <returns>The line.</returns>
		/// <param name="line">Line number to retrieve.</param>
		public List<Rune> GetLine (int line)
		{
			if (lines.Count > 0) {
				if (line < Count) {
					return lines [line];
				} else {
					return lines [Count - 1];
				}
			} else {
				lines.Add (new List<Rune> ());
				return lines [0];
			}
		}

		/// <summary>
		/// Adds a line to the model at the specified position.
		/// </summary>
		/// <param name="pos">Line number where the line will be inserted.</param>
		/// <param name="runes">The line of text, as a List of Rune.</param>
		public void AddLine (int pos, List<Rune> runes)
		{
			lines.Insert (pos, runes);
		}

		/// <summary>
		/// Removes the line at the specified position
		/// </summary>
		/// <param name="pos">Position.</param>
		public void RemoveLine (int pos)
		{
			if (lines.Count > 0) {
				if (lines.Count == 1 && lines [0].Count == 0) {
					return;
				}
				lines.RemoveAt (pos);
			}
		}

		/// <summary>
		/// Returns the maximum line length of the visible lines.
		/// </summary>
		/// <param name="first">The first line.</param>
		/// <param name="last">The last line.</param>
		/// <param name="tabWidth">The tab width.</param>
		public int GetMaxVisibleLine (int first, int last, int tabWidth)
		{
			int maxLength = 0;
			last = last < lines.Count ? last : lines.Count;
			for (int i = first; i < last; i++) {
				var line = GetLine (i);
				var tabSum = line.Sum (r => r == '\t' ? tabWidth - 1 : 0);
				var l = line.Count + tabSum;
				if (l > maxLength) {
					maxLength = l;
				}
			}

			return maxLength;
		}

		internal static bool SetCol (ref int col, int width, int cols)
		{
			if (col + cols <= width) {
				col += cols;
				return true;
			}

			return false;
		}

		internal static int GetColFromX (List<Rune> t, int start, int x, int tabWidth = 0)
		{
			if (x < 0) {
				return x;
			}
			int size = start;
			var pX = x + start;
			for (int i = start; i < t.Count; i++) {
				var r = t [i];
				size += Rune.ColumnWidth (r);
				if (r == '\t' && tabWidth > 0) {
					size += tabWidth + 1;
				}
				if (i == pX || (size > pX)) {
					return i - start;
				}
			}
			return t.Count - start;
		}

		// Returns the size and length in a range of the string.
		internal static (int size, int length) DisplaySize (List<Rune> t, int start = -1, int end = -1,
			bool checkNextRune = true, int tabWidth = 0)
		{
			if (t == null || t.Count == 0) {
				return (0, 0);
			}
			int size = 0;
			int len = 0;
			int tcount = end == -1 ? t.Count : end > t.Count ? t.Count : end;
			int i = start == -1 ? 0 : start;
			for (; i < tcount; i++) {
				var rune = t [i];
				size += Rune.ColumnWidth (rune);
				len += Rune.RuneLen (rune);
				if (rune == '\t' && tabWidth > 0) {
					size += tabWidth + 1;
					len += tabWidth - 1;
				}
				if (checkNextRune && i == tcount - 1 && t.Count > tcount
					&& IsWideRune (t [i + 1], tabWidth, out int s, out int l)) {
					size += s;
					len += l;
				}
			}

			bool IsWideRune (Rune r, int tWidth, out int s, out int l)
			{
				s = Rune.ColumnWidth (r);
				l = Rune.RuneLen (r);
				if (r == '\t' && tWidth > 0) {
					s += tWidth + 1;
					l += tWidth - 1;
				}

				return s > 1;
			}

			return (size, len);
		}

		// Returns the left column in a range of the string.
		internal static int CalculateLeftColumn (List<Rune> t, int start, int end, int width, int tabWidth = 0)
		{
			if (t == null || t.Count == 0) {
				return 0;
			}
			int size = 0;
			int tcount = end > t.Count - 1 ? t.Count - 1 : end;
			int col = 0;

			for (int i = tcount; i >= 0; i--) {
				var rune = t [i];
				size += Rune.ColumnWidth (rune);
				if (rune == '\t') {
					size += tabWidth + 1;
				}
				if (size > width) {
					if (end == t.Count) {
						col++;
					}
					break;
				} else if (end < t.Count && col > 0 && start < end && col == start) {
					break;
				}
				col = i;
			}

			return col;
		}

		(Point startPointToFind, Point currentPointToFind, bool found) toFind;

		internal (Point current, bool found) FindNextText (ustring text, out bool gaveFullTurn, bool matchCase = false, bool matchWholeWord = false)
		{
			if (text == null || lines.Count == 0) {
				gaveFullTurn = false;
				return (Point.Empty, false);
			}

			if (toFind.found) {
				toFind.currentPointToFind.X++;
			}
			var foundPos = GetFoundNextTextPoint (text, lines.Count, matchCase, matchWholeWord, toFind.currentPointToFind);
			if (!foundPos.found && toFind.currentPointToFind != toFind.startPointToFind) {
				foundPos = GetFoundNextTextPoint (text, toFind.startPointToFind.Y + 1, matchCase, matchWholeWord, Point.Empty);
			}
			gaveFullTurn = ApplyToFind (foundPos);

			return foundPos;
		}

		internal (Point current, bool found) FindPreviousText (ustring text, out bool gaveFullTurn, bool matchCase = false, bool matchWholeWord = false)
		{
			if (text == null || lines.Count == 0) {
				gaveFullTurn = false;
				return (Point.Empty, false);
			}

			if (toFind.found) {
				toFind.currentPointToFind.X++;
			}
			var foundPos = GetFoundPreviousTextPoint (text, toFind.currentPointToFind.Y, matchCase, matchWholeWord, toFind.currentPointToFind);
			if (!foundPos.found && toFind.currentPointToFind != toFind.startPointToFind) {
				foundPos = GetFoundPreviousTextPoint (text, lines.Count - 1, matchCase, matchWholeWord,
					new Point (lines [lines.Count - 1].Count, lines.Count));
			}
			gaveFullTurn = ApplyToFind (foundPos);

			return foundPos;
		}

		internal (Point current, bool found) ReplaceAllText (ustring text, bool matchCase = false, bool matchWholeWord = false, ustring textToReplace = null)
		{
			bool found = false;
			Point pos = Point.Empty;

			for (int i = 0; i < lines.Count; i++) {
				var x = lines [i];
				var txt = ustring.Make (x).ToString ();
				if (!matchCase) {
					txt = txt.ToUpper ();
				}
				var matchText = !matchCase ? text.ToUpper ().ToString () : text.ToString ();
				var col = txt.IndexOf (matchText);
				if (col > -1 && matchWholeWord && !MatchWholeWord (txt, matchText, col)) {
					continue;
				}
				if (col > -1) {
					if (!found) {
						found = true;
					}
					pos = new Point (col, i);
					lines [i] = ReplaceText (x, textToReplace, matchText, col).ToRuneList ();
					i--;
				}
			}

			return (pos, found);
		}

		ustring ReplaceText (List<Rune> source, ustring textToReplace, string matchText, int col)
		{
			var origTxt = ustring.Make (source);
			(int _, int len) = TextModel.DisplaySize (source, 0, col, false);
			(var _, var len2) = TextModel.DisplaySize (source, col, col + matchText.Length, false);
			(var _, var len3) = TextModel.DisplaySize (source, col + matchText.Length, origTxt.RuneCount, false);

			return origTxt [0, len] +
				textToReplace.ToString () +
				origTxt [len + len2, len + len2 + len3];
		}

		bool ApplyToFind ((Point current, bool found) foundPos)
		{
			bool gaveFullTurn = false;
			if (foundPos.found) {
				toFind.currentPointToFind = foundPos.current;
				if (toFind.found && toFind.currentPointToFind == toFind.startPointToFind) {
					gaveFullTurn = true;
				}
				if (!toFind.found) {
					toFind.startPointToFind = toFind.currentPointToFind = foundPos.current;
					toFind.found = foundPos.found;
				}
			}

			return gaveFullTurn;
		}

		(Point current, bool found) GetFoundNextTextPoint (ustring text, int linesCount, bool matchCase, bool matchWholeWord, Point start)
		{
			for (int i = start.Y; i < linesCount; i++) {
				var x = lines [i];
				var txt = ustring.Make (x).ToString ();
				if (!matchCase) {
					txt = txt.ToUpper ();
				}
				var matchText = !matchCase ? text.ToUpper ().ToString () : text.ToString ();
				var col = txt.IndexOf (matchText, Math.Min (start.X, txt.Length));
				if (col > -1 && matchWholeWord && !MatchWholeWord (txt, matchText, col)) {
					continue;
				}
				if (col > -1 && ((i == start.Y && col >= start.X)
					|| i > start.Y)
					&& txt.Contains (matchText)) {
					return (new Point (col, i), true);
				} else if (col == -1 && start.X > 0) {
					start.X = 0;
				}
			}

			return (Point.Empty, false);
		}

		(Point current, bool found) GetFoundPreviousTextPoint (ustring text, int linesCount, bool matchCase, bool matchWholeWord, Point start)
		{
			for (int i = linesCount; i >= 0; i--) {
				var x = lines [i];
				var txt = ustring.Make (x).ToString ();
				if (!matchCase) {
					txt = txt.ToUpper ();
				}
				if (start.Y != i) {
					start.X = Math.Max (x.Count - 1, 0);
				}
				var matchText = !matchCase ? text.ToUpper ().ToString () : text.ToString ();
				var col = txt.LastIndexOf (matchText, start.X);
				if (col > -1 && matchWholeWord && !MatchWholeWord (txt, matchText, col)) {
					continue;
				}
				if (col > -1 && ((i == linesCount && col <= start.X)
					|| i < start.Y)
					&& txt.Contains (matchText)) {
					return (new Point (col, i), true);
				}
			}

			return (Point.Empty, false);
		}

		bool MatchWholeWord (string source, string matchText, int index = 0)
		{
			if (string.IsNullOrEmpty (source) || string.IsNullOrEmpty (matchText)) {
				return false;
			}

			var txt = matchText.Trim ();
			var start = index > 0 ? index - 1 : 0;
			var end = index + txt.Length;

			if ((start == 0 || Rune.IsWhiteSpace (source [start]))
				&& (end == source.Length || Rune.IsWhiteSpace (source [end]))) {
				return true;
			}

			return false;
		}

		/// <summary>
		/// Redefine column and line tracking.
		/// </summary>
		/// <param name="point">Contains the column and line.</param>
		internal void ResetContinuousFind (Point point)
		{
			toFind.startPointToFind = toFind.currentPointToFind = point;
			toFind.found = false;
		}
	}

	class WordWrapManager {
		class WrappedLine {
			public int ModelLine;
			public int Row;
			public int RowIndex;
			public int ColWidth;
		}

		List<WrappedLine> wrappedModelLines = new List<WrappedLine> ();
		int frameWidth;
		bool isWrapModelRefreshing;

		public TextModel Model { get; private set; }

		public WordWrapManager (TextModel model)
		{
			Model = model;
		}

		public TextModel WrapModel (int width, out int nRow, out int nCol, out int nStartRow, out int nStartCol,
			int row = 0, int col = 0, int startRow = 0, int startCol = 0, int tabWidth = 0)
		{
			frameWidth = width;

			var modelRow = isWrapModelRefreshing ? row : GetModelLineFromWrappedLines (row);
			var modelCol = isWrapModelRefreshing ? col : GetModelColFromWrappedLines (row, col);
			var modelStartRow = isWrapModelRefreshing ? startRow : GetModelLineFromWrappedLines (startRow);
			var modelStartCol = isWrapModelRefreshing ? startCol : GetModelColFromWrappedLines (startRow, startCol);
			var wrappedModel = new TextModel ();
			int lines = 0;
			nRow = 0;
			nCol = 0;
			nStartRow = 0;
			nStartCol = 0;
			bool isRowAndColSetted = row == 0 && col == 0;
			bool isStartRowAndColSetted = startRow == 0 && startCol == 0;
			List<WrappedLine> wModelLines = new List<WrappedLine> ();

			for (int i = 0; i < Model.Count; i++) {
				var line = Model.GetLine (i);
				var wrappedLines = ToListRune (
					TextFormatter.Format (ustring.Make (line), width,
					TextAlignment.Left, true, true, tabWidth));
				int sumColWidth = 0;
				for (int j = 0; j < wrappedLines.Count; j++) {
					var wrapLine = wrappedLines [j];
					if (!isRowAndColSetted && modelRow == i) {
						if (nCol + wrapLine.Count <= modelCol) {
							nCol += wrapLine.Count;
							nRow = lines;
							if (nCol == modelCol) {
								nCol = wrapLine.Count;
								isRowAndColSetted = true;
							} else if (j == wrappedLines.Count - 1) {
								nCol = wrapLine.Count - j + modelCol - nCol;
								isRowAndColSetted = true;
							}
						} else {
							var offset = nCol + wrapLine.Count - modelCol;
							nCol = wrapLine.Count - offset;
							nRow = lines;
							isRowAndColSetted = true;
						}
					}
					if (!isStartRowAndColSetted && modelStartRow == i) {
						if (nStartCol + wrapLine.Count <= modelStartCol) {
							nStartCol += wrapLine.Count;
							nStartRow = lines;
							if (nStartCol == modelStartCol) {
								nStartCol = wrapLine.Count;
								isStartRowAndColSetted = true;
							} else if (j == wrappedLines.Count - 1) {
								nStartCol = wrapLine.Count - j + modelStartCol - nStartCol;
								isStartRowAndColSetted = true;
							}
						} else {
							var offset = nStartCol + wrapLine.Count - modelStartCol;
							nStartCol = wrapLine.Count - offset;
							nStartRow = lines;
							isStartRowAndColSetted = true;
						}
					}
					wrappedModel.AddLine (lines, wrapLine);
					sumColWidth += wrapLine.Count;
					var wrappedLine = new WrappedLine () {
						ModelLine = i,
						Row = lines,
						RowIndex = j,
						ColWidth = wrapLine.Count,
					};
					wModelLines.Add (wrappedLine);
					lines++;
				}
			}
			wrappedModelLines = wModelLines;

			return wrappedModel;
		}

		public List<List<Rune>> ToListRune (List<ustring> textList)
		{
			var runesList = new List<List<Rune>> ();

			foreach (var text in textList) {
				runesList.Add (text.ToRuneList ());
			}

			return runesList;
		}

		public int GetModelLineFromWrappedLines (int line) => wrappedModelLines.Count > 0
			? wrappedModelLines [Math.Min (line, wrappedModelLines.Count - 1)].ModelLine
			: 0;

		public int GetModelColFromWrappedLines (int line, int col)
		{
			if (wrappedModelLines?.Count == 0) {
				return 0;
			}

			var modelLine = GetModelLineFromWrappedLines (line);
			var firstLine = wrappedModelLines.IndexOf (r => r.ModelLine == modelLine);
			int modelCol = 0;

			for (int i = firstLine; i <= line; i++) {
				var wLine = wrappedModelLines [i];

				if (i < line) {
					modelCol += wLine.ColWidth;
				} else {
					modelCol += col;
				}
			}

			return modelCol;
		}

		List<Rune> GetCurrentLine (int row) => Model.GetLine (row);

		public void AddLine (int row, int col)
		{
			var modelRow = GetModelLineFromWrappedLines (row);
			var modelCol = GetModelColFromWrappedLines (row, col);
			var line = GetCurrentLine (modelRow);
			var restCount = line.Count - modelCol;
			var rest = line.GetRange (modelCol, restCount);
			line.RemoveRange (modelCol, restCount);
			Model.AddLine (modelRow + 1, rest);
			isWrapModelRefreshing = true;
			WrapModel (frameWidth, out _, out _, out _, out _, modelRow + 1, 0);
			isWrapModelRefreshing = false;
		}

		public bool Insert (int row, int col, Rune rune)
		{
			var line = GetCurrentLine (GetModelLineFromWrappedLines (row));
			line.Insert (GetModelColFromWrappedLines (row, col), rune);
			if (line.Count > frameWidth) {
				return true;
			} else {
				return false;
			}
		}

		public bool RemoveAt (int row, int col)
		{
			var modelRow = GetModelLineFromWrappedLines (row);
			var line = GetCurrentLine (modelRow);
			var modelCol = GetModelColFromWrappedLines (row, col);

			if (modelCol >= line.Count) {
				Model.RemoveLine (modelRow);
				RemoveAt (row, 0);
				return false;
			}
			line.RemoveAt (modelCol);
			if (line.Count > frameWidth || (row + 1 < wrappedModelLines.Count
				&& wrappedModelLines [row + 1].ModelLine == modelRow)) {
				return true;
			}

			return false;
		}

		public bool RemoveLine (int row, int col, out bool lineRemoved, bool forward = true)
		{
			lineRemoved = false;
			var modelRow = GetModelLineFromWrappedLines (row);
			var line = GetCurrentLine (modelRow);
			var modelCol = GetModelColFromWrappedLines (row, col);

			if (modelCol == 0 && line.Count == 0) {
				Model.RemoveLine (modelRow);
				return false;
			} else if (modelCol < line.Count) {
				if (forward) {
					line.RemoveAt (modelCol);
					return true;
				} else if (modelCol - 1 > -1) {
					line.RemoveAt (modelCol - 1);
					return true;
				}
			}
			lineRemoved = true;
			if (forward) {
				if (modelRow + 1 == Model.Count) {
					return false;
				}

				var nextLine = Model.GetLine (modelRow + 1);
				line.AddRange (nextLine);
				Model.RemoveLine (modelRow + 1);
				if (line.Count > frameWidth) {
					return true;
				}
			} else {
				if (modelRow == 0) {
					return false;
				}

				var prevLine = Model.GetLine (modelRow - 1);
				prevLine.AddRange (line);
				Model.RemoveLine (modelRow);
				if (prevLine.Count > frameWidth) {
					return true;
				}
			}

			return false;
		}

		public bool RemoveRange (int row, int index, int count)
		{
			var modelRow = GetModelLineFromWrappedLines (row);
			var line = GetCurrentLine (modelRow);
			var modelCol = GetModelColFromWrappedLines (row, index);

			try {
				line.RemoveRange (modelCol, count);
			} catch (Exception) {
				return false;
			}

			return true;
		}

		public void UpdateModel (TextModel model, out int nRow, out int nCol, out int nStartRow, out int nStartCol,
			int row, int col, int startRow, int startCol)
		{
			isWrapModelRefreshing = true;
			Model = model;
			WrapModel (frameWidth, out nRow, out nCol, out nStartRow, out nStartCol, row, col, startRow, startCol);
			isWrapModelRefreshing = false;
		}
	}

	/// <summary>
	///   Multi-line text editing <see cref="View"/>
	/// </summary>
	/// <remarks>
	///   <para>
	///     <see cref="TextView"/> provides a multi-line text editor. Users interact
	///     with it with the standard Emacs commands for movement or the arrow
	///     keys. 
	///   </para> 
	///   <list type="table"> 
	///     <listheader>
	///       <term>Shortcut</term>
	///       <description>Action performed</description>
	///     </listheader>
	///     <item>
	///        <term>Left cursor, Control-b</term>
	///        <description>
	///          Moves the editing point left.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Right cursor, Control-f</term>
	///        <description>
	///          Moves the editing point right.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Alt-b</term>
	///        <description>
	///          Moves one word back.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Alt-f</term>
	///        <description>
	///          Moves one word forward.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Up cursor, Control-p</term>
	///        <description>
	///          Moves the editing point one line up.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Down cursor, Control-n</term>
	///        <description>
	///          Moves the editing point one line down
	///        </description>
	///     </item>
	///     <item>
	///        <term>Home key, Control-a</term>
	///        <description>
	///          Moves the cursor to the beginning of the line.
	///        </description>
	///     </item>
	///     <item>
	///        <term>End key, Control-e</term>
	///        <description>
	///          Moves the cursor to the end of the line.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Control-Home</term>
	///        <description>
	///          Scrolls to the first line and moves the cursor there.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Control-End</term>
	///        <description>
	///          Scrolls to the last line and moves the cursor there.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Delete, Control-d</term>
	///        <description>
	///          Deletes the character in front of the cursor.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Backspace</term>
	///        <description>
	///          Deletes the character behind the cursor.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Control-k</term>
	///        <description>
	///          Deletes the text until the end of the line and replaces the kill buffer
	///          with the deleted text.   You can paste this text in a different place by
	///          using Control-y.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Control-y</term>
	///        <description>
	///           Pastes the content of the kill ring into the current position.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Alt-d</term>
	///        <description>
	///           Deletes the word above the cursor and adds it to the kill ring.  You 
	///           can paste the contents of the kill ring with Control-y.
	///        </description>
	///     </item>
	///     <item>
	///        <term>Control-q</term>
	///        <description>
	///          Quotes the next input character, to prevent the normal processing of
	///          key handling to take place.
	///        </description>
	///     </item>
	///   </list>
	/// </remarks>
	public class TextView : View {
		TextModel model = new TextModel ();
		int topRow;
		int leftColumn;
		int currentRow;
		int currentColumn;
		int selectionStartColumn, selectionStartRow;
		bool selecting;
		bool wordWrap;
		WordWrapManager wrapManager;
		bool continuousFind;
		int tabWidth = 4;
		bool allowsTab = true;
		bool allowsReturn = true;
		bool multiline = true;

		/// <summary>
		/// Raised when the <see cref="Text"/> of the <see cref="TextView"/> changes.
		/// </summary>
		public event Action TextChanged;

#if false
		/// <summary>
		///   Changed event, raised when the text has clicked.
		/// </summary>
		/// <remarks>
		///   Client code can hook up to this event, it is
		///   raised when the text in the entry changes.
		/// </remarks>
		public Action Changed;
#endif
		/// <summary>
		///   Initializes a <see cref="TextView"/> on the specified area, with absolute position and size.
		/// </summary>
		/// <remarks>
		/// </remarks>
		public TextView (Rect frame) : base (frame)
		{
			Initialize ();
		}

		/// <summary>
		///   Initializes a <see cref="TextView"/> on the specified area, 
		///   with dimensions controlled with the X, Y, Width and Height properties.
		/// </summary>
		public TextView () : base ()
		{
			Initialize ();
		}

		void Initialize ()
		{
			CanFocus = true;
			Used = true;
		}

		/// <summary>
		/// Tracks whether the text view should be considered "used", that is, that the user has moved in the entry,
		/// so new input should be appended at the cursor position, rather than clearing the entry
		/// </summary>
		public bool Used { get; set; }

		void ResetPosition ()
		{
			topRow = leftColumn = currentRow = currentColumn = 0;
			selecting = false;
			shiftSelecting = false;
			ResetCursorVisibility ();
		}

		/// <summary>
		///   Sets or gets the text in the <see cref="TextView"/>.
		/// </summary>
		/// <remarks>
		/// </remarks>
		public override ustring Text {
			get {
				if (wordWrap) {
					return wrapManager.Model.ToString ();
				} else {
					return model.ToString ();
				}
			}

			set {
				ResetPosition ();
				model.LoadString (value);
				if (wordWrap) {
					wrapManager = new WordWrapManager (model);
					model = wrapManager.WrapModel (Frame.Width - 2, out _, out _, out _, out _);
				}
				TextChanged?.Invoke ();
				SetNeedsDisplay ();
			}
		}

		///<inheritdoc/>
		public override Rect Frame {
			get => base.Frame;
			set {
				base.Frame = value;
				WrapTextModel ();
				Adjust ();
			}
		}

		void WrapTextModel ()
		{
			if (wordWrap && wrapManager != null) {
				model = wrapManager.WrapModel (Frame.Width - 2,
					out int nRow, out int nCol,
					out int nStartRow, out int nStartCol,
					currentRow, currentColumn,
					selectionStartRow, selectionStartColumn,
					tabWidth);
				currentRow = nRow;
				currentColumn = nCol;
				selectionStartRow = nStartRow;
				selectionStartColumn = nStartCol;
				SetNeedsDisplay ();
			}
		}

		/// <summary>
		/// Gets or sets the top row.
		/// </summary>
		public int TopRow { get => topRow; set => topRow = Math.Max (Math.Min (value, Lines - 1), 0); }

		/// <summary>
		/// Gets or sets the left column.
		/// </summary>
		public int LeftColumn { get => leftColumn; set => leftColumn = Math.Max (Math.Min (value, Maxlength - 1), 0); }

		/// <summary>
		/// Gets the maximum visible length line.
		/// </summary>
		public int Maxlength => model.GetMaxVisibleLine (topRow, topRow + Frame.Height, TabWidth);

		/// <summary>
		/// Gets the  number of lines.
		/// </summary>
		public int Lines => model.Count;

		/// <summary>
		///    Sets or gets the current cursor position.
		/// </summary>
		public Point CursorPosition {
			get => new Point (currentColumn, currentRow);
			set {
				var line = model.GetLine (Math.Max (Math.Min (value.Y, model.Count - 1), 0));
				currentColumn = value.X < 0 ? 0 : value.X > line.Count ? line.Count : value.X;
				currentRow = value.Y < 0 ? 0 : value.Y > model.Count - 1
					? Math.Max (model.Count - 1, 0) : value.Y;
				SetNeedsDisplay ();
				Adjust ();
			}
		}

		/// <summary>
		/// Start column position of the selected text.
		/// </summary>
		public int SelectionStartColumn {
			get => selectionStartColumn;
			set {
				var line = model.GetLine (currentRow);
				selectionStartColumn = value < 0 ? 0 : value > line.Count ? line.Count : value;
				selecting = true;
				SetNeedsDisplay ();
				Adjust ();
			}
		}

		/// <summary>
		/// Start row position of the selected text.
		/// </summary>
		public int SelectionStartRow {
			get => selectionStartRow;
			set {
				selectionStartRow = value < 0 ? 0 : value > model.Count - 1
					? Math.Max (model.Count - 1, 0) : value;
				selecting = true;
				SetNeedsDisplay ();
				Adjust ();
			}
		}

		/// <summary>
		/// The selected text.
		/// </summary>
		public ustring SelectedText {
			get {
				if (!selecting || (model.Count == 1 && model.GetLine (0).Count == 0)) {
					return ustring.Empty;
				}

				SetWrapModel ();
				var sel = GetRegion ();
				UpdateWrapModel ();
				Adjust ();

				return sel;
			}
		}

		/// <summary>
		/// Length of the selected text.
		/// </summary>
		public int SelectedLength => GetSelectedLength ();

		/// <summary>
		/// Get or sets the selecting.
		/// </summary>
		public bool Selecting {
			get => selecting;
			set => selecting = value;
		}
		/// <summary>
		/// Allows word wrap the to fit the available container width.
		/// </summary>
		public bool WordWrap {
			get => wordWrap;
			set {
				if (value == wordWrap) {
					return;
				}
				wordWrap = value;
				ResetPosition ();
				if (wordWrap) {
					wrapManager = new WordWrapManager (model);
					model = wrapManager.WrapModel (Math.Max (Frame.Width - 2, 0), out _, out _, out _, out _);
				} else if (!wordWrap && wrapManager != null) {
					model = wrapManager.Model;
				}
				SetNeedsDisplay ();
			}
		}

		/// <summary>
		/// The bottom offset needed to use a horizontal scrollbar or for another reason.
		/// This is only needed with the keyboard navigation.
		/// </summary>
		public int BottomOffset { get; set; }

		/// <summary>
		/// The right offset needed to use a vertical scrollbar or for another reason.
		/// This is only needed with the keyboard navigation.
		/// </summary>
		public int RightOffset { get; set; }

		/// <summary>
		/// Gets or sets a value indicating whether pressing ENTER in a <see cref="TextView"/>
		/// creates a new line of text in the view or activates the default button for the toplevel.
		/// </summary>
		public bool AllowsReturn {
			get => allowsReturn;
			set {
				allowsReturn = value;
				if (allowsReturn && !multiline) {
					Multiline = true;
				}
				if (!allowsReturn && multiline) {
					Multiline = false;
					AllowsTab = false;
				}
			}
		}

		/// <summary>
		/// Gets or sets a value indicating whether pressing the TAB key in a <see cref="TextView"/>
		/// types a TAB character in the view instead of moving the focus to the next view in the tab order.
		/// </summary>
		public bool AllowsTab {
			get => allowsTab;
			set {
				allowsTab = value;
				if (allowsTab && tabWidth == 0) {
					tabWidth = 4;
				}
				if (allowsTab && !multiline) {
					Multiline = true;
				}
				if (!allowsTab && multiline) {
					Multiline = false;
				}
				if (!allowsTab && tabWidth > 0) {
					tabWidth = 0;
				}
			}
		}

		/// <summary>
		/// Gets or sets a value indicating the number of whitespace when pressing the TAB key.
		/// </summary>
		public int TabWidth {
			get => tabWidth;
			set {
				tabWidth = Math.Max (value, 0);
				if (tabWidth == 0 && AllowsTab) {
					AllowsTab = false;
				}
				if (tabWidth > 0 && !AllowsTab) {
					AllowsTab = true;
				}
			}
		}

		Dim savedHeight = null;

		/// <summary>
		/// Gets or sets a value indicating whether this <see cref="TextView"/> is a multiline text view.
		/// </summary>
		public bool Multiline {
			get => multiline;
			set {
				multiline = value;
				if (multiline && !allowsTab) {
					AllowsTab = true;
				}
				if (multiline && !allowsReturn) {
					AllowsReturn = true;
				}

				if (!multiline) {
					AllowsReturn = false;
					AllowsTab = false;
					currentColumn = 0;
					currentRow = 0;
					savedHeight = Height;
					var lyout = LayoutStyle;
					if (LayoutStyle == LayoutStyle.Computed) {
						LayoutStyle = LayoutStyle.Absolute;
					}
					Height = 1;
					LayoutStyle = lyout;
					SetNeedsDisplay ();
				} else if (multiline && savedHeight != null) {
					var lyout = LayoutStyle;
					if (LayoutStyle == LayoutStyle.Computed) {
						LayoutStyle = LayoutStyle.Absolute;
					}
					Height = savedHeight;
					LayoutStyle = lyout;
					SetNeedsDisplay ();
				}
			}
		}

		int GetSelectedLength ()
		{
			return SelectedText.Length;
		}

		CursorVisibility savedCursorVisibility = CursorVisibility.Default;

		void SaveCursorVisibility ()
		{
			if (desiredCursorVisibility != CursorVisibility.Invisible) {
				savedCursorVisibility = desiredCursorVisibility;
				DesiredCursorVisibility = CursorVisibility.Invisible;
			}
		}

		void ResetCursorVisibility ()
		{
			if (savedCursorVisibility != desiredCursorVisibility) {
				DesiredCursorVisibility = savedCursorVisibility;
				savedCursorVisibility = CursorVisibility.Default;
			} else {
				DesiredCursorVisibility = CursorVisibility.Underline;
			}
		}

		/// <summary>
		/// Loads the contents of the file into the  <see cref="TextView"/>.
		/// </summary>
		/// <returns><c>true</c>, if file was loaded, <c>false</c> otherwise.</returns>
		/// <param name="path">Path to the file to load.</param>
		public bool LoadFile (string path)
		{
			if (path == null)
				throw new ArgumentNullException (nameof (path));
			ResetPosition ();
			var res = model.LoadFile (path);
			SetNeedsDisplay ();
			return res;
		}

		/// <summary>
		/// Loads the contents of the stream into the  <see cref="TextView"/>.
		/// </summary>
		/// <returns><c>true</c>, if stream was loaded, <c>false</c> otherwise.</returns>
		/// <param name="stream">Stream to load the contents from.</param>
		public void LoadStream (Stream stream)
		{
			if (stream == null)
				throw new ArgumentNullException (nameof (stream));
			ResetPosition ();
			model.LoadStream (stream);
			SetNeedsDisplay ();
		}

		/// <summary>
		/// Closes the contents of the stream into the  <see cref="TextView"/>.
		/// </summary>
		/// <returns><c>true</c>, if stream was closed, <c>false</c> otherwise.</returns>
		public bool CloseFile ()
		{
			ResetPosition ();
			var res = model.CloseFile ();
			SetNeedsDisplay ();
			return res;
		}

		/// <summary>
		///    Gets the current cursor row.
		/// </summary>
		public int CurrentRow => currentRow;

		/// <summary>
		/// Gets the cursor column.
		/// </summary>
		/// <value>The cursor column.</value>
		public int CurrentColumn => currentColumn;

		/// <summary>
		///   Positions the cursor on the current row and column
		/// </summary>
		public override void PositionCursor ()
		{
			if (selecting) {
				var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow) - topRow, 0), Frame.Height);
				var maxRow = Math.Min (Math.Max (Math.Max (selectionStartRow, currentRow) - topRow, 0), Frame.Height);

				SetNeedsDisplay (new Rect (0, minRow, Frame.Width, maxRow));
			}
			var line = model.GetLine (currentRow);
			var retreat = 0;
			var col = 0;
			if (line.Count > 0) {
				retreat = Math.Max (SpecialRune (line [Math.Min (Math.Max (currentColumn - leftColumn - 1, 0), line.Count - 1)])
					? 1 : 0, 0);

				for (int idx = leftColumn; idx < line.Count; idx++) {
					if (idx >= currentColumn)
						break;
					var cols = Rune.ColumnWidth (line [idx]);
					if (line [idx] == '\t' && TabWidth > 0) {
						cols += TabWidth + 1;
					}
					TextModel.SetCol (ref col, Frame.Width, cols);
				}
			}
			col += retreat;
			if ((col >= leftColumn || col < Frame.Width)
				&& topRow <= currentRow && currentRow - topRow + BottomOffset < Frame.Height) {
				ResetCursorVisibility ();
				Move (col, currentRow - topRow);
			} else {
				SaveCursorVisibility ();
			}
		}

		void ClearRegion (int left, int top, int right, int bottom)
		{
			for (int row = top; row < bottom; row++) {
				Move (left, row);
				for (int col = left; col < right; col++)
					AddRune (col, row, ' ');
			}
		}

		/// <summary>
		/// Sets the driver to the default color for the control where no text is being rendered.  Defaults to <see cref="ColorScheme.Normal"/>.
		/// </summary>
		protected virtual void ColorNormal ()
		{
			Driver.SetAttribute (ColorScheme.Normal);
		}

		/// <summary>
		/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idx"/> of the
		/// current <paramref name="line"/>.  Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
		/// Defaults to <see cref="ColorScheme.Normal"/>.
		/// </summary>
		/// <param name="line"></param>
		/// <param name="idx"></param>
		protected virtual void ColorNormal (List<Rune> line, int idx)
		{
			Driver.SetAttribute (ColorScheme.Normal);
		}

		/// <summary>
		/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idx"/> of the
		/// current <paramref name="line"/>.  Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
		/// Defaults to <see cref="ColorScheme.Focus"/>.
		/// </summary>
		/// <param name="line"></param>
		/// <param name="idx"></param>
		protected virtual void ColorSelection (List<Rune> line, int idx)
		{
			Driver.SetAttribute (ColorScheme.Focus);
		}

		/// <summary>
		/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idx"/> of the
		/// current <paramref name="line"/>.  Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
		/// Defaults to <see cref="ColorScheme.HotFocus"/>.
		/// </summary>
		/// <param name="line"></param>
		/// <param name="idx"></param>
		protected virtual void ColorUsed (List<Rune> line, int idx)
		{
			Driver.SetAttribute (ColorScheme.HotFocus);
		}

		bool isReadOnly = false;

		/// <summary>
		/// Gets or sets whether the  <see cref="TextView"/> is in read-only mode or not
		/// </summary>
		/// <value>Boolean value(Default false)</value>
		public bool ReadOnly {
			get => isReadOnly;
			set {
				isReadOnly = value;
			}
		}

		CursorVisibility desiredCursorVisibility = CursorVisibility.Default;

		/// <summary>
		/// Get / Set the wished cursor when the field is focused
		/// </summary>
		public CursorVisibility DesiredCursorVisibility {
			get => desiredCursorVisibility;
			set {
				if (desiredCursorVisibility != value && HasFocus) {
					Application.Driver.SetCursorVisibility (value);
				}

				desiredCursorVisibility = value;
			}
		}

		///<inheritdoc/>
		public override bool OnEnter (View view)
		{
			//TODO: Improve it by handling read only mode of the text field
			Application.Driver.SetCursorVisibility (DesiredCursorVisibility);

			return base.OnEnter (view);
		}

		// Returns an encoded region start..end (top 32 bits are the row, low32 the column)
		void GetEncodedRegionBounds (out long start, out long end)
		{
			long selection = ((long)(uint)selectionStartRow << 32) | (uint)selectionStartColumn;
			long point = ((long)(uint)currentRow << 32) | (uint)currentColumn;
			if (selection > point) {
				start = point;
				end = selection;
			} else {
				start = selection;
				end = point;
			}
		}

		bool PointInSelection (int col, int row)
		{
			long start, end;
			GetEncodedRegionBounds (out start, out end);
			var q = ((long)(uint)row << 32) | (uint)col;
			return q >= start && q <= end - 1;
		}

		//
		// Returns a ustring with the text in the selected 
		// region.
		//
		ustring GetRegion ()
		{
			long start, end;
			GetEncodedRegionBounds (out start, out end);
			if (start == end) {
				return ustring.Empty;
			}
			int startRow = (int)(start >> 32);
			var maxrow = ((int)(end >> 32));
			int startCol = (int)(start & 0xffffffff);
			var endCol = (int)(end & 0xffffffff);
			var line = model.GetLine (startRow);

			if (startRow == maxrow)
				return StringFromRunes (line.GetRange (startCol, endCol - startCol));

			ustring res = StringFromRunes (line.GetRange (startCol, line.Count - startCol));

			for (int row = startRow + 1; row < maxrow; row++) {
				res = res + ustring.Make ((Rune)10) + StringFromRunes (model.GetLine (row));
			}
			line = model.GetLine (maxrow);
			res = res + ustring.Make ((Rune)10) + StringFromRunes (line.GetRange (0, endCol));
			return res;
		}

		//
		// Clears the contents of the selected region
		//
		void ClearRegion ()
		{
			long start, end;
			long currentEncoded = ((long)(uint)currentRow << 32) | (uint)currentColumn;
			GetEncodedRegionBounds (out start, out end);
			int startRow = (int)(start >> 32);
			var maxrow = ((int)(end >> 32));
			int startCol = (int)(start & 0xffffffff);
			var endCol = (int)(end & 0xffffffff);
			var line = model.GetLine (startRow);

			if (startRow == maxrow) {
				line.RemoveRange (startCol, endCol - startCol);
				currentColumn = startCol;
				if (wordWrap) {
					SetNeedsDisplay ();
				} else {
					SetNeedsDisplay (new Rect (0, startRow - topRow, Frame.Width, startRow - topRow + 1));
				}
				return;
			}

			line.RemoveRange (startCol, line.Count - startCol);
			var line2 = model.GetLine (maxrow);
			line.AddRange (line2.Skip (endCol));
			for (int row = startRow + 1; row <= maxrow; row++) {
				model.RemoveLine (startRow + 1);
			}
			if (currentEncoded == end) {
				currentRow -= maxrow - (startRow);
			}
			currentColumn = startCol;

			SetNeedsDisplay ();
		}

		/// <summary>
		/// Select all text.
		/// </summary>
		public void SelectAll ()
		{
			if (model.Count == 0) {
				return;
			}

			StartSelecting ();
			selectionStartColumn = 0;
			selectionStartRow = 0;
			currentColumn = model.GetLine (model.Count - 1).Count;
			currentRow = model.Count - 1;
			SetNeedsDisplay ();
		}

		/// <summary>
		/// Find the next text based on the match case with the option to replace it.
		/// </summary>
		/// <param name="textToFind">The text to find.</param>
		/// <param name="gaveFullTurn"><c>true</c>If all the text was forward searched.<c>false</c>otherwise.</param>
		/// <param name="matchCase">The match case setting.</param>
		/// <param name="matchWholeWord">The match whole word setting.</param>
		/// <param name="textToReplace">The text to replace.</param>
		/// <param name="replace"><c>true</c>If is replacing.<c>false</c>otherwise.</param>
		/// <returns><c>true</c>If the text was found.<c>false</c>otherwise.</returns>
		public bool FindNextText (ustring textToFind, out bool gaveFullTurn, bool matchCase = false,
			bool matchWholeWord = false, ustring textToReplace = null, bool replace = false)
		{
			if (model.Count == 0) {
				gaveFullTurn = false;
				return false;
			}

			SetWrapModel ();
			ResetContinuousFind ();
			var foundPos = model.FindNextText (textToFind, out gaveFullTurn, matchCase, matchWholeWord);

			return SetFoundText (textToFind, foundPos, textToReplace, replace);
		}

		/// <summary>
		/// Find the previous text based on the match case with the option to replace it.
		/// </summary>
		/// <param name="textToFind">The text to find.</param>
		/// <param name="gaveFullTurn"><c>true</c>If all the text was backward searched.<c>false</c>otherwise.</param>
		/// <param name="matchCase">The match case setting.</param>
		/// <param name="matchWholeWord">The match whole word setting.</param>
		/// <param name="textToReplace">The text to replace.</param>
		/// <param name="replace"><c>true</c>If the text was found.<c>false</c>otherwise.</param>
		/// <returns><c>true</c>If the text was found.<c>false</c>otherwise.</returns>
		public bool FindPreviousText (ustring textToFind, out bool gaveFullTurn, bool matchCase = false,
			bool matchWholeWord = false, ustring textToReplace = null, bool replace = false)
		{
			if (model.Count == 0) {
				gaveFullTurn = false;
				return false;
			}

			SetWrapModel ();
			ResetContinuousFind ();
			var foundPos = model.FindPreviousText (textToFind, out gaveFullTurn, matchCase, matchWholeWord);

			return SetFoundText (textToFind, foundPos, textToReplace, replace);
		}

		/// <summary>
		/// Reset the flag to stop continuous find.
		/// </summary>
		public void FindTextChanged ()
		{
			continuousFind = false;
		}

		/// <summary>
		/// Replaces all the text based on the match case.
		/// </summary>
		/// <param name="textToFind">The text to find.</param>
		/// <param name="matchCase">The match case setting.</param>
		/// <param name="matchWholeWord">The match whole word setting.</param>
		/// <param name="textToReplace">The text to replace.</param>
		/// <returns><c>true</c>If the text was found.<c>false</c>otherwise.</returns>
		public bool ReplaceAllText (ustring textToFind, bool matchCase = false, bool matchWholeWord = false,
			ustring textToReplace = null)
		{
			if (isReadOnly || model.Count == 0) {
				return false;
			}

			SetWrapModel ();
			ResetContinuousFind ();
			var foundPos = model.ReplaceAllText (textToFind, matchCase, matchWholeWord, textToReplace);

			return SetFoundText (textToFind, foundPos, textToReplace, false, true);
		}

		bool SetFoundText (ustring text, (Point current, bool found) foundPos,
			ustring textToReplace = null, bool replace = false, bool replaceAll = false)
		{
			if (foundPos.found) {
				StartSelecting ();
				selectionStartColumn = foundPos.current.X;
				selectionStartRow = foundPos.current.Y;
				if (!replaceAll) {
					currentColumn = selectionStartColumn + text.RuneCount;
				} else {
					currentColumn = selectionStartColumn + textToReplace.RuneCount;
				}
				currentRow = foundPos.current.Y;
				if (!isReadOnly && replace) {
					Adjust ();
					ClearSelectedRegion ();
					InsertText (textToReplace);
					StartSelecting ();
					selectionStartColumn = currentColumn - textToReplace.RuneCount;
				} else {
					UpdateWrapModel ();
					SetNeedsDisplay ();
					Adjust ();
				}
				continuousFind = true;
				return foundPos.found;
			}
			UpdateWrapModel ();
			continuousFind = false;

			return foundPos.found;
		}

		void ResetContinuousFind ()
		{
			if (!continuousFind) {
				var col = selecting ? selectionStartColumn : currentColumn;
				var row = selecting ? selectionStartRow : currentRow;
				model.ResetContinuousFind (new Point (col, row));
			}
		}

		/// <summary>
		/// Restore from original model.
		/// </summary>
		void SetWrapModel ()
		{
			if (wordWrap) {
				currentColumn = wrapManager.GetModelColFromWrappedLines (currentRow, currentColumn);
				currentRow = wrapManager.GetModelLineFromWrappedLines (currentRow);
				selectionStartColumn = wrapManager.GetModelColFromWrappedLines (selectionStartRow, selectionStartColumn);
				selectionStartRow = wrapManager.GetModelLineFromWrappedLines (selectionStartRow);
				model = wrapManager.Model;
			}
		}

		/// <summary>
		/// Update the original model.
		/// </summary>
		void UpdateWrapModel ()
		{
			if (wordWrap) {
				wrapManager.UpdateModel (model, out int nRow, out int nCol,
					out int nStartRow, out int nStartCol,
					currentRow, currentColumn,
					selectionStartRow, selectionStartColumn);
				currentRow = nRow;
				currentColumn = nCol;
				selectionStartRow = nStartRow;
				selectionStartColumn = nStartCol;
				wrapNeeded = true;
			}
		}

		///<inheritdoc/>
		public override void Redraw (Rect bounds)
		{
			ColorNormal ();

			var offB = OffSetBackground ();
			int right = Frame.Width + offB.width + RightOffset;
			int bottom = Frame.Height + offB.height + BottomOffset;
			var row = 0;
			for (int idxRow = topRow; idxRow < model.Count; idxRow++) {
				var line = model.GetLine (idxRow);
				int lineRuneCount = line.Count;
				var col = 0;

				Move (0, row);
				for (int idxCol = leftColumn; idxCol < lineRuneCount; idxCol++) {
					var rune = idxCol >= lineRuneCount ? ' ' : line [idxCol];
					var cols = Rune.ColumnWidth (rune);
					if (idxCol < line.Count && selecting && PointInSelection (idxCol, idxRow)) {
						ColorSelection (line, idxCol);
					} else if (idxCol == currentColumn && idxRow == currentRow && !selecting && !Used
						&& HasFocus && idxCol < lineRuneCount) {
						ColorUsed (line, idxCol);
					} else {
						ColorNormal (line,idxCol);
					}

					if (rune == '\t' && TabWidth > 0) {
						cols += TabWidth + 1;
						if (col + cols > right) {
							cols = right - col;
						}
						for (int i = 0; i < cols; i++) {
							if (col + i < right) {
								AddRune (col + i, row, ' ');
							}
						}
					} else if (!SpecialRune (rune)) {
						AddRune (col, row, rune);
					} else {
						col++;
					}
					if (!TextModel.SetCol (ref col, bounds.Right, cols)) {
						break;
					}
					if (idxCol + 1 < lineRuneCount && col + Rune.ColumnWidth (line [idxCol + 1]) > right) {
						break;
					}
				}
				if (col < right) {
					ColorNormal ();
					ClearRegion (col, row, right, row + 1);
				}
				row++;
			}
			if (row < bottom) {
				ColorNormal ();
				ClearRegion (bounds.Left, row, right, bottom);
			}

			PositionCursor ();
		}

		bool SpecialRune (Rune rune)
		{
			switch (rune) {
			case (uint)Key.Enter:
			case 0xd:
				return true;
			default:
				return false;
			}
		}

		///<inheritdoc/>
		public override bool CanFocus {
			get => base.CanFocus;
			set { base.CanFocus = value; }
		}

		void SetClipboard (ustring text)
		{
			if (text != null) {
				Clipboard.Contents = text;
			}
		}

		void AppendClipboard (ustring text)
		{
			Clipboard.Contents += text;
		}

		void Insert (Rune rune)
		{
			var line = GetCurrentLine ();
			if (Used) {
				line.Insert (Math.Min (currentColumn, line.Count), rune);
			} else {
				if (currentColumn < line.Count) {
					line.RemoveAt (currentColumn);
				}
				line.Insert (Math.Min (currentColumn, line.Count), rune);
			}
			if (wordWrap) {
				if (Used) {
					wrapNeeded = wrapManager.Insert (currentRow, currentColumn, rune);
				} else {
					wrapNeeded = wrapManager.RemoveAt (currentRow, currentColumn);
					wrapNeeded = wrapManager.Insert (currentRow, currentColumn, rune);
				}
				if (wrapNeeded) {
					SetNeedsDisplay ();
				}
			}
			var prow = currentRow - topRow;
			if (!wrapNeeded) {
				SetNeedsDisplay (new Rect (0, prow, Frame.Width, prow + 1));
			}
		}

		ustring StringFromRunes (List<Rune> runes)
		{
			if (runes == null)
				throw new ArgumentNullException (nameof (runes));
			int size = 0;
			foreach (var rune in runes) {
				size += Utf8.RuneLen (rune);
			}
			var encoded = new byte [size];
			int offset = 0;
			foreach (var rune in runes) {
				offset += Utf8.EncodeRune (rune, encoded, offset);
			}
			return ustring.Make (encoded);
		}

		List<Rune> GetCurrentLine () => model.GetLine (currentRow);

		void InsertText (ustring text)
		{
			if (ustring.IsNullOrEmpty (text)) {
				return;
			}

			var lines = TextModel.StringToRunes (text);

			if (lines.Count == 0) {
				return;
			}

			var line = GetCurrentLine ();

			// Optimize single line
			if (lines.Count == 1) {
				line.InsertRange (currentColumn, lines [0]);
				currentColumn += lines [0].Count;
				if (!wordWrap && currentColumn - leftColumn > Frame.Width) {
					leftColumn = Math.Max (currentColumn - Frame.Width + 1, 0);
				}
				if (wordWrap) {
					SetNeedsDisplay ();
				} else {
					SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Math.Max (currentRow - topRow + 1, 0)));
				}
				return;
			}

			List<Rune> rest = null;
			int lastp = 0;

			if (model.Count > 0 && currentColumn > 0) {
				// Keep a copy of the rest of the line
				var restCount = line.Count - currentColumn;
				rest = line.GetRange (currentColumn, restCount);
				line.RemoveRange (currentColumn, restCount);
			}

			// First line is inserted at the current location, the rest is appended
			line.InsertRange (currentColumn, lines [0]);
			//model.AddLine (currentRow, lines [0]);

			for (int i = 1; i < lines.Count; i++) {
				model.AddLine (currentRow + i, lines [i]);
			}

			if (rest != null) {
				var last = model.GetLine (currentRow + lines.Count - 1);
				lastp = last.Count;
				last.InsertRange (last.Count, rest);
			}

			// Now adjust column and row positions
			currentRow += lines.Count - 1;
			currentColumn = rest != null ? lastp : lines [lines.Count - 1].Count;
			Adjust ();
		}

		// The column we are tracking, or -1 if we are not tracking any column
		int columnTrack = -1;

		// Tries to snap the cursor to the tracking column
		void TrackColumn ()
		{
			// Now track the column
			var line = GetCurrentLine ();
			if (line.Count < columnTrack)
				currentColumn = line.Count;
			else if (columnTrack != -1)
				currentColumn = columnTrack;
			else if (currentColumn > line.Count)
				currentColumn = line.Count;
			Adjust ();
		}

		void Adjust ()
		{
			var offB = OffSetBackground ();
			var line = GetCurrentLine ();
			bool need = !NeedDisplay.IsEmpty || wrapNeeded;
			var tSize = TextModel.DisplaySize (line, -1, -1, false, TabWidth);
			var dSize = TextModel.DisplaySize (line, leftColumn, currentColumn, true, TabWidth);
			if (!wordWrap && currentColumn < leftColumn) {
				leftColumn = currentColumn;
				need = true;
			} else if (!wordWrap && (currentColumn - leftColumn + RightOffset > Frame.Width + offB.width
				|| dSize.size + RightOffset >= Frame.Width + offB.width)) {
				leftColumn = TextModel.CalculateLeftColumn (line, leftColumn, currentColumn,
					Frame.Width + offB.width - RightOffset, TabWidth);
				need = true;
			} else if (dSize.size + RightOffset < Frame.Width + offB.width
				&& tSize.size + RightOffset < Frame.Width + offB.width) {
				leftColumn = 0;
			}

			if (currentRow < topRow) {
				topRow = currentRow;
				need = true;
			} else if (currentRow - topRow + BottomOffset >= Frame.Height + offB.height) {
				topRow = Math.Min (Math.Max (currentRow - Frame.Height + 1 + BottomOffset, 0), currentRow);
				need = true;
			}
			if (need) {
				if (wrapNeeded) {
					WrapTextModel ();
					wrapNeeded = false;
				}
				SetNeedsDisplay ();
			} else {
				PositionCursor ();
			}
		}

		(int width, int height) OffSetBackground ()
		{
			int w = 0;
			int h = 0;
			if (SuperView?.Frame.Right - Frame.Right < 0) {
				w = SuperView.Frame.Right - Frame.Right - 1;
			}
			if (SuperView?.Frame.Bottom - Frame.Bottom < 0) {
				h = SuperView.Frame.Bottom - Frame.Bottom - 1;
			}
			return (w, h);
		}

		/// <summary>
		/// Will scroll the <see cref="TextView"/> to display the specified row at the top if <paramref name="isRow"/> is true or
		/// will scroll the <see cref="TextView"/> to display the specified column at the left if <paramref name="isRow"/> is false.
		/// </summary>
		/// <param name="idx">Row that should be displayed at the top or Column that should be displayed at the left,
		///  if the value is negative it will be reset to zero</param>
		/// <param name="isRow">If true (default) the <paramref name="idx"/> is a row, column otherwise.</param>
		public void ScrollTo (int idx, bool isRow = true)
		{
			if (idx < 0) {
				idx = 0;
			}
			if (isRow) {
				topRow = Math.Max (idx > model.Count - 1 ? model.Count - 1 : idx, 0);
			} else if (!wordWrap) {
				var maxlength = model.GetMaxVisibleLine (topRow, topRow + Frame.Height + RightOffset, TabWidth);
				leftColumn = Math.Max (idx > maxlength - 1 ? maxlength - 1 : idx, 0);
			}
			SetNeedsDisplay ();
		}

		bool lastWasKill;
		bool wrapNeeded;
		bool shiftSelecting;

		///<inheritdoc/>
		public override bool ProcessKey (KeyEvent kb)
		{
			int restCount;
			List<Rune> rest;

			// if the user presses Left (without any control keys) and they are at the start of the text
			if(kb.Key == Key.CursorLeft && currentColumn == 0 && currentRow == 0) {
				// do not respond (this lets the key press fall through to navigation system - which usually changes focus backward)
				return false;
			}

			// if the user presses Right (without any control keys)
			if (kb.Key == Key.CursorRight) {

				// determine where the last cursor position in the text is
				var lastRow = model.Count - 1;
				var lastCol = model.GetLine (lastRow).Count;

				// if they are at the very end of all the text do not respond (this lets the key press fall through to navigation system - which usually changes focus forward)
				if (currentColumn == lastCol && currentRow == lastRow) {
					return false;
				}
			}

			// Handle some state here - whether the last command was a kill
			// operation and the column tracking (up/down)
			switch (kb.Key) {
			case Key.N | Key.CtrlMask:
			case Key.CursorDown:
			case Key.P | Key.CtrlMask:
			case Key.CursorUp:
				lastWasKill = false;
				continuousFind = false;
				break;
			case Key.K | Key.CtrlMask:
				break;
			case Key.F | Key.CtrlMask:
			case Key.B | Key.CtrlMask:
			case (Key)((int)'B' + Key.AltMask):
			case Key.A | Key.CtrlMask:
			case Key.E | Key.CtrlMask:
			case Key.CursorRight:
			case Key.CursorLeft:
			case Key.CursorRight | Key.CtrlMask:
			case Key.CursorLeft | Key.CtrlMask:
			case Key.CursorRight | Key.ShiftMask:
			case Key.CursorLeft | Key.ShiftMask:
			case Key.CursorRight | Key.CtrlMask | Key.ShiftMask:
			case Key.CursorLeft | Key.CtrlMask | Key.ShiftMask:
			case Key.Home:
			case Key.Home | Key.CtrlMask:
			case Key.Home | Key.ShiftMask:
			case Key.Home | Key.CtrlMask | Key.ShiftMask:
			case Key.End:
			case Key.End | Key.CtrlMask:
			case Key.End | Key.ShiftMask:
			case Key.End | Key.CtrlMask | Key.ShiftMask:
				lastWasKill = false;
				columnTrack = -1;
				continuousFind = false;
				break;
			default:
				lastWasKill = false;
				columnTrack = -1;
				break;
			}

			// Dispatch the command.
			switch (kb.Key) {
			case Key.PageDown:
			case Key.V | Key.CtrlMask:
			case Key.PageDown | Key.ShiftMask:
				if (kb.Key.HasFlag (Key.ShiftMask)) {
					StartSelecting ();
				} else if (shiftSelecting && selecting) {
					StopSelecting ();
				}
				int nPageDnShift = Frame.Height - 1;
				if (currentRow >= 0 && currentRow < model.Count) {
					if (columnTrack == -1)
						columnTrack = currentColumn;
					currentRow = (currentRow + nPageDnShift) > model.Count
						? model.Count > 0 ? model.Count - 1 : 0
						: currentRow + nPageDnShift;
					if (topRow < currentRow - nPageDnShift) {
						topRow = currentRow >= model.Count ? currentRow - nPageDnShift : topRow + nPageDnShift;
						SetNeedsDisplay ();
					}
					TrackColumn ();
					PositionCursor ();
				}
				break;

			case Key.PageUp:
			case ((int)'V' + Key.AltMask):
			case Key.PageUp | Key.ShiftMask:
				if (kb.Key.HasFlag (Key.ShiftMask)) {
					StartSelecting ();
				} else if (shiftSelecting && selecting) {
					StopSelecting ();
				}
				int nPageUpShift = Frame.Height - 1;
				if (currentRow > 0) {
					if (columnTrack == -1)
						columnTrack = currentColumn;
					currentRow = currentRow - nPageUpShift < 0 ? 0 : currentRow - nPageUpShift;
					if (currentRow < topRow) {
						topRow = topRow - nPageUpShift < 0 ? 0 : topRow - nPageUpShift;
						SetNeedsDisplay ();
					}
					TrackColumn ();
					PositionCursor ();
				}
				break;

			case Key.N | Key.CtrlMask:
			case Key.CursorDown:
			case Key.CursorDown | Key.ShiftMask:
				if (kb.Key.HasFlag (Key.ShiftMask)) {
					StartSelecting ();
				} else if (shiftSelecting && selecting) {
					StopSelecting ();
				}
				MoveDown ();
				break;

			case Key.P | Key.CtrlMask:
			case Key.CursorUp:
			case Key.CursorUp | Key.ShiftMask:
				if (kb.Key.HasFlag (Key.ShiftMask)) {
					StartSelecting ();
				} else if (shiftSelecting && selecting) {
					StopSelecting ();
				}
				MoveUp ();
				break;

			case Key.F | Key.CtrlMask:
			case Key.CursorRight:
			case Key.CursorRight | Key.ShiftMask:
				if (kb.Key.HasFlag (Key.ShiftMask)) {
					StartSelecting ();
				} else if (shiftSelecting && selecting) {
					StopSelecting ();
				}
				var currentLine = GetCurrentLine ();
				if (currentColumn < currentLine.Count) {
					currentColumn++;
				} else {
					if (currentRow + 1 < model.Count) {
						currentRow++;
						currentColumn = 0;
						if (currentRow >= topRow + Frame.Height) {
							topRow++;
							SetNeedsDisplay ();
						}
					}
				}
				Adjust ();
				return true;

			case Key.B | Key.CtrlMask:
			case Key.CursorLeft:
			case Key.CursorLeft | Key.ShiftMask:
				if (kb.Key.HasFlag (Key.ShiftMask)) {
					StartSelecting ();
				} else if (shiftSelecting && selecting) {
					StopSelecting ();
				}
				if (currentColumn > 0) {
					currentColumn--;
				} else {
					if (currentRow > 0) {
						currentRow--;
						if (currentRow < topRow) {
							topRow--;
							SetNeedsDisplay ();
						}
						currentLine = GetCurrentLine ();
						currentColumn = currentLine.Count;
					}
				}
				Adjust ();
				break;

			case Key.Delete:
			case Key.Backspace:
				if (isReadOnly)
					break;
				if (selecting) {
					ClearSelectedRegion ();
					return true;
				}
				if (DeleteTextBackwards ()) {
					return true;
				}
				break;

			// Home, C-A
			case Key.Home:
			case Key.Home | Key.ShiftMask:
			case Key.A | Key.CtrlMask:
				if (kb.Key.HasFlag (Key.ShiftMask)) {
					StartSelecting ();
				} else if (shiftSelecting && selecting) {
					StopSelecting ();
				}
				currentColumn = 0;
				leftColumn = 0;
				Adjust ();
				break;
			case Key.DeleteChar:
			case Key.D | Key.CtrlMask: // Delete
				if (isReadOnly)
					break;
				if (selecting) {
					ClearSelectedRegion ();
					return true;
				}
				if (DeleteTextForwards ()) {
					return true;
				}
				break;

			case Key.End:
			case Key.End | Key.ShiftMask:
			case Key.E | Key.CtrlMask: // End
				if (kb.Key.HasFlag (Key.ShiftMask)) {
					StartSelecting ();
				} else if (shiftSelecting && selecting) {
					StopSelecting ();
				}
				currentLine = GetCurrentLine ();
				currentColumn = currentLine.Count;
				Adjust ();
				return true;

			case Key.K | Key.CtrlMask: // kill-to-end
			case Key.DeleteChar | Key.CtrlMask | Key.ShiftMask:
				if (isReadOnly)
					break;
				currentLine = GetCurrentLine ();
				var setLastWasKill = true;
				if (currentLine.Count > 0 && currentColumn == currentLine.Count) {
					DeleteTextForwards ();
					return true;
				}
				if (currentLine.Count == 0) {
					if (currentRow < model.Count - 1) {
						model.RemoveLine (currentRow);
					}
					if (model.Count > 0 || lastWasKill) {
						var val = ustring.Make ((Rune)'\n');
						if (lastWasKill) {
							AppendClipboard (val);
						} else {
							SetClipboard (val);
						}
					}
					if (model.Count == 0) {
						// Prevents from adding line feeds if there is no more lines.
						setLastWasKill = false;
					}
				} else {
					restCount = currentLine.Count - currentColumn;
					rest = currentLine.GetRange (currentColumn, restCount);
					var val = ustring.Empty;
					if (currentColumn == 0 && lastWasKill && currentLine.Count > 0) {
						val = ustring.Make ((Rune)'\n');
					}
					val += StringFromRunes (rest);
					if (lastWasKill) {
						AppendClipboard (val);
					} else {
						SetClipboard (val);
					}
					currentLine.RemoveRange (currentColumn, restCount);
				}
				SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height));
				lastWasKill = setLastWasKill;
				break;

			case Key.Backspace | Key.CtrlMask | Key.ShiftMask: // kill-to-start
				if (isReadOnly)
					break;
				currentLine = GetCurrentLine ();
				setLastWasKill = true;
				if (currentLine.Count > 0 && currentColumn == 0) {
					DeleteTextBackwards ();
					return true;
				}
				if (currentLine.Count == 0) {
					if (currentRow > 0) {
						model.RemoveLine (currentRow);
						currentRow--;
						currentLine = model.GetLine (currentRow);
						currentColumn = currentLine.Count;
					}
				} else {
					restCount = currentColumn;
					rest = currentLine.GetRange (0, restCount);
					var val = ustring.Empty;
					if (currentColumn == 0 && lastWasKill && currentLine.Count > 0) {
						val = ustring.Make ((Rune)'\n');
					}
					val += StringFromRunes (rest);
					if (lastWasKill) {
						AppendClipboard (val);
					} else {
						SetClipboard (val);
					}
					currentLine.RemoveRange (0, restCount);
					currentColumn = 0;
				}
				SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height));
				lastWasKill = setLastWasKill;
				break;

			case Key.Y | Key.CtrlMask: // Control-y, yank
				if (isReadOnly)
					break;
				Paste ();
				return true;

			case Key.Space | Key.CtrlMask:
				selecting = !selecting;
				selectionStartColumn = currentColumn;
				selectionStartRow = currentRow;
				break;

			case ((int)'C' + Key.AltMask):
			case Key.C | Key.CtrlMask:
				Copy ();
				return true;

			case ((int)'W' + Key.AltMask):
			case Key.W | Key.CtrlMask:
			case Key.X | Key.CtrlMask:
				Cut ();
				return true;

			case Key.CtrlMask | Key.CursorLeft:
			case Key.CtrlMask | Key.CursorLeft | Key.ShiftMask:
			case (Key)((int)'B' + Key.AltMask):
				if (kb.Key.HasFlag (Key.ShiftMask)) {
					StartSelecting ();
				} else if (shiftSelecting && selecting) {
					StopSelecting ();
				}
				var newPos = WordBackward (currentColumn, currentRow);
				if (newPos.HasValue) {
					currentColumn = newPos.Value.col;
					currentRow = newPos.Value.row;
				}
				Adjust ();

				break;

			case Key.CtrlMask | Key.CursorRight:
			case Key.CtrlMask | Key.CursorRight | Key.ShiftMask:
			case (Key)((int)'F' + Key.AltMask):
				if (kb.Key.HasFlag (Key.ShiftMask)) {
					StartSelecting ();
				} else if (shiftSelecting && selecting) {
					StopSelecting ();
				}
				newPos = WordForward (currentColumn, currentRow);
				if (newPos.HasValue) {
					currentColumn = newPos.Value.col;
					currentRow = newPos.Value.row;
				}
				Adjust ();
				break;

			case Key.DeleteChar | Key.CtrlMask: // kill-word-forwards
				if (isReadOnly)
					break;
				currentLine = GetCurrentLine ();
				if (currentLine.Count == 0 || currentColumn == currentLine.Count) {
					DeleteTextForwards ();
					return true;
				}
				newPos = WordForward (currentColumn, currentRow);
				restCount = 0;
				if (newPos.HasValue && currentRow == newPos.Value.row) {
					restCount = newPos.Value.col - currentColumn;
					currentLine.RemoveRange (currentColumn, restCount);
				} else if (newPos.HasValue) {
					restCount = currentLine.Count - currentColumn;
					currentLine.RemoveRange (currentColumn, restCount);
				}
				if (wordWrap && wrapManager.RemoveRange (currentRow, currentColumn, restCount)) {
					wrapNeeded = true;
				}
				if (wrapNeeded) {
					SetNeedsDisplay ();
				} else {
					SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height));
				}
				break;

			case Key.Backspace | Key.CtrlMask: // kill-word-backwards
				if (isReadOnly)
					break;
				currentLine = GetCurrentLine ();
				if (currentColumn == 0) {
					DeleteTextBackwards ();
					return true;
				}
				newPos = WordBackward (currentColumn, currentRow);
				if (newPos.HasValue && currentRow == newPos.Value.row) {
					restCount = currentColumn - newPos.Value.col;
					currentLine.RemoveRange (newPos.Value.col, restCount);
					if (wordWrap && wrapManager.RemoveRange (currentRow, newPos.Value.col, restCount)) {
						wrapNeeded = true;
					}
					currentColumn = newPos.Value.col;
				} else if (newPos.HasValue) {
					restCount = currentLine.Count - currentColumn;
					currentLine.RemoveRange (currentColumn, restCount);
					if (wordWrap && wrapManager.RemoveRange (currentRow, currentColumn, restCount)) {
						wrapNeeded = true;
					}
					currentColumn = newPos.Value.col;
					currentRow = newPos.Value.row;
				}
				if (wrapNeeded) {
					SetNeedsDisplay ();
				} else {
					SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height));
				}
				break;

			case Key.Enter:
				if (!AllowsReturn) {
					return false;
				}
				if (isReadOnly)
					break;
				currentLine = GetCurrentLine ();
				restCount = currentLine.Count - currentColumn;
				rest = currentLine.GetRange (currentColumn, restCount);
				currentLine.RemoveRange (currentColumn, restCount);
				model.AddLine (currentRow + 1, rest);
				if (wordWrap) {
					wrapManager.AddLine (currentRow, currentColumn);
					wrapNeeded = true;
				}
				currentRow++;
				bool fullNeedsDisplay = false;
				if (currentRow >= topRow + Frame.Height) {
					topRow++;
					fullNeedsDisplay = true;
				}
				currentColumn = 0;
				if (!wordWrap && currentColumn < leftColumn) {
					fullNeedsDisplay = true;
					leftColumn = 0;
				}

				if (fullNeedsDisplay)
					SetNeedsDisplay ();
				else
					SetNeedsDisplay (new Rect (0, currentRow - topRow, 2, Frame.Height));
				break;

			case Key.CtrlMask | Key.End:
			case Key.CtrlMask | Key.End | Key.ShiftMask:
				if (kb.Key.HasFlag (Key.ShiftMask)) {
					StartSelecting ();
				} else if (shiftSelecting && selecting) {
					StopSelecting ();
				}
				MoveEnd ();
				break;

			case Key.CtrlMask | Key.Home:
			case Key.CtrlMask | Key.Home | Key.ShiftMask:
				if (kb.Key.HasFlag (Key.ShiftMask)) {
					StartSelecting ();
				} else if (shiftSelecting && selecting) {
					StopSelecting ();
				}
				MoveHome ();
				break;

			case Key.T | Key.CtrlMask:
				SelectAll ();
				break;

			case Key.InsertChar:
				Used = !Used;
				SetNeedsDisplay ();
				break;

			case Key _ when ShortcutHelper.GetModifiersKey (kb) == Key.Tab:
				if (!AllowsTab) {
					return false;
				}
				InsertText (new KeyEvent ((Key)'\t', null));
				break;

			case Key _ when (ShortcutHelper.GetModifiersKey (kb) == (Key.BackTab | Key.ShiftMask)):
				if (!AllowsTab) {
					return false;
				}
				if (currentColumn > 0) {
					currentLine = GetCurrentLine ();
					if (currentLine.Count > 0 && currentLine [currentColumn - 1] == '\t') {
						currentLine.RemoveAt (currentColumn - 1);
						currentColumn--;
					}
				}
				break;

			default:
				// Ignore control characters and other special keys
				if (kb.Key < Key.Space || kb.Key > Key.CharMask)
					return false;

				InsertText (kb);
				break;
			}
			DoNeededAction ();

			return true;
		}

		bool InsertText (KeyEvent kb)
		{
			//So that special keys like tab can be processed
			if (isReadOnly)
				return true;
			if (selecting) {
				ClearSelectedRegion ();
			}
			if (Used) {
				Insert ((uint)kb.Key);
				currentColumn++;
				if (currentColumn >= leftColumn + Frame.Width) {
					leftColumn++;
					SetNeedsDisplay ();
				}
			} else {
				Insert ((uint)kb.Key);
				currentColumn++;
			}
			return true;
		}

		///<inheritdoc/>
		public override bool OnKeyUp (KeyEvent kb)
		{
			switch (kb.Key) {
			case Key.Space | Key.CtrlMask:
				return true;
			}

			return false;
		}

		void DoNeededAction ()
		{
			if (NeedDisplay.IsEmpty) {
				PositionCursor ();
			} else {
				Adjust ();
			}
		}

		bool DeleteTextForwards ()
		{
			var currentLine = GetCurrentLine ();
			if (currentColumn == currentLine.Count) {
				if (currentRow + 1 == model.Count)
					return true;
				var nextLine = model.GetLine (currentRow + 1);
				currentLine.AddRange (nextLine);
				model.RemoveLine (currentRow + 1);
				if (wordWrap && wrapManager.RemoveLine (currentRow, currentColumn, out _)) {
					wrapNeeded = true;
				}
				var sr = currentRow - topRow;
				SetNeedsDisplay (new Rect (0, sr, Frame.Width, sr + 1));
			} else {
				currentLine.RemoveAt (currentColumn);
				if (wordWrap && wrapManager.RemoveAt (currentRow, currentColumn)) {
					wrapNeeded = true;
				}
				var r = currentRow - topRow;
				SetNeedsDisplay (new Rect (currentColumn - leftColumn, r, Frame.Width, r + 1));
			}

			return false;
		}

		bool DeleteTextBackwards ()
		{
			if (currentColumn > 0) {
				// Delete backwards 
				var currentLine = GetCurrentLine ();
				currentLine.RemoveAt (currentColumn - 1);
				if (wordWrap && wrapManager.RemoveAt (currentRow, currentColumn - 1)) {
					wrapNeeded = true;
				}
				currentColumn--;
				if (currentColumn < leftColumn) {
					leftColumn--;
					SetNeedsDisplay ();
				} else
					SetNeedsDisplay (new Rect (0, currentRow - topRow, 1, Frame.Width));
			} else {
				// Merges the current line with the previous one.
				if (currentRow == 0)
					return true;
				var prowIdx = currentRow - 1;
				var prevRow = model.GetLine (prowIdx);
				var prevCount = prevRow.Count;
				model.GetLine (prowIdx).AddRange (GetCurrentLine ());
				model.RemoveLine (currentRow);
				bool lineRemoved = false;
				if (wordWrap && wrapManager.RemoveLine (currentRow, currentColumn, out lineRemoved, false)) {
					wrapNeeded = true;
				}
				currentRow--;
				if (wrapNeeded && !lineRemoved) {
					currentColumn = Math.Max (prevCount - 1, 0);
				} else {
					currentColumn = prevCount;
				}
				Adjust ();
			}

			return false;
		}

		bool copyWithoutSelection;

		/// <summary>
		/// Copy the selected text to the clipboard contents.
		/// </summary>
		public void Copy ()
		{
			SetWrapModel ();
			if (selecting) {
				SetClipboard (GetRegion ());
				copyWithoutSelection = false;
			} else {
				var currentLine = GetCurrentLine ();
				SetClipboard (ustring.Make (currentLine));
				copyWithoutSelection = true;
			}
			UpdateWrapModel ();
			DoNeededAction ();
		}

		/// <summary>
		/// Cut the selected text to the clipboard contents.
		/// </summary>
		public void Cut ()
		{
			SetWrapModel ();
			SetClipboard (GetRegion ());
			if (!isReadOnly) {
				ClearRegion ();
			}
			UpdateWrapModel ();
			selecting = false;
			DoNeededAction ();
		}

		/// <summary>
		/// Paste the clipboard contents into the current selected position.
		/// </summary>
		public void Paste ()
		{
			if (isReadOnly) {
				return;
			}

			SetWrapModel ();
			if (copyWithoutSelection) {
				var runeList = Clipboard.Contents == null ? new List<Rune> () : Clipboard.Contents.ToRuneList ();
				model.AddLine (currentRow, runeList);
				currentRow++;
			} else {
				if (selecting) {
					ClearRegion ();
				}
				InsertText (Clipboard.Contents);
				copyWithoutSelection = false;
			}
			UpdateWrapModel ();
			selecting = false;
			DoNeededAction ();
		}

		void StartSelecting ()
		{
			if (shiftSelecting && selecting) {
				return;
			}
			shiftSelecting = true;
			selecting = true;
			selectionStartColumn = currentColumn;
			selectionStartRow = currentRow;
		}

		void StopSelecting ()
		{
			shiftSelecting = false;
			selecting = false;
		}

		void ClearSelectedRegion ()
		{
			SetWrapModel ();
			if (!isReadOnly) {
				ClearRegion ();
			}
			UpdateWrapModel ();
			selecting = false;
			DoNeededAction ();
		}

		void MoveUp ()
		{
			if (currentRow > 0) {
				if (columnTrack == -1) {
					columnTrack = currentColumn;
				}
				currentRow--;
				if (currentRow < topRow) {
					topRow--;
					SetNeedsDisplay ();
				}
				TrackColumn ();
				PositionCursor ();
			}
		}

		void MoveDown ()
		{
			if (currentRow + 1 < model.Count) {
				if (columnTrack == -1) {
					columnTrack = currentColumn;
				}
				currentRow++;
				if (currentRow + BottomOffset >= topRow + Frame.Height) {
					topRow++;
					SetNeedsDisplay ();
				}
				TrackColumn ();
				PositionCursor ();
			} else if (currentRow > Frame.Height) {
				Adjust ();
			}
		}

		IEnumerable<(int col, int row, Rune rune)> ForwardIterator (int col, int row)
		{
			if (col < 0 || row < 0)
				yield break;
			if (row >= model.Count)
				yield break;
			var line = GetCurrentLine ();
			if (col >= line.Count)
				yield break;

			while (row < model.Count) {
				for (int c = col; c < line.Count; c++) {
					yield return (c, row, line [c]);
				}
				col = 0;
				row++;
				line = GetCurrentLine ();
			}
		}

		Rune RuneAt (int col, int row)
		{
			var line = model.GetLine (row);
			if (line.Count > 0) {
				return line [col > line.Count - 1 ? line.Count - 1 : col];
			} else {
				return 0;
			}
		}

		/// <summary>
		/// Will scroll the <see cref="TextView"/> to the last line and position the cursor there.
		/// </summary>
		public void MoveEnd ()
		{
			currentRow = model.Count - 1;
			var line = GetCurrentLine ();
			currentColumn = line.Count;
			TrackColumn ();
			PositionCursor ();
		}

		/// <summary>
		/// Will scroll the <see cref="TextView"/> to the first line and position the cursor there.
		/// </summary>
		public void MoveHome ()
		{
			currentRow = 0;
			topRow = 0;
			currentColumn = 0;
			leftColumn = 0;
			TrackColumn ();
			PositionCursor ();
		}

		bool MoveNext (ref int col, ref int row, out Rune rune)
		{
			var line = model.GetLine (row);
			if (col + 1 < line.Count) {
				col++;
				rune = line [col];
				if (col + 1 == line.Count) {
					col++;
				}
				return true;
			}
			while (row + 1 < model.Count) {
				col = 0;
				row++;
				line = model.GetLine (row);
				if (line.Count > 0) {
					rune = line [0];
					return true;
				}
			}
			rune = 0;
			return false;
		}

		bool MovePrev (ref int col, ref int row, out Rune rune)
		{
			var line = model.GetLine (row);

			if (col > 0) {
				col--;
				rune = line [col];
				return true;
			}
			if (row == 0) {
				rune = 0;
				return false;
			}
			while (row > 0) {
				row--;
				line = model.GetLine (row);
				col = line.Count - 1;
				if (col >= 0) {
					rune = line [col];
					return true;
				}
			}
			rune = 0;
			return false;
		}

		(int col, int row)? WordForward (int fromCol, int fromRow)
		{
			var col = fromCol;
			var row = fromRow;
			try {
				var rune = RuneAt (col, row);

				void ProcMoveNext (ref int nCol, ref int nRow, Rune nRune)
				{
					if (Rune.IsSymbol (nRune) || Rune.IsWhiteSpace (nRune)) {
						while (MoveNext (ref nCol, ref nRow, out nRune)) {
							if (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune))
								return;
						}
						if (nRow != fromRow && (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune))) {
							return;
						}
						while (MoveNext (ref nCol, ref nRow, out nRune)) {
							if (!Rune.IsLetterOrDigit (nRune) && !Rune.IsPunctuation (nRune))
								break;
						}
					} else {
						if (!MoveNext (ref nCol, ref nRow, out nRune)) {
							return;
						}

						var line = model.GetLine (fromRow);
						if ((nRow != fromRow && fromCol < line.Count)
							|| (nRow == fromRow && nCol == line.Count - 1)) {
							nCol = line.Count;
							nRow = fromRow;
							return;
						} else if (nRow != fromRow && fromCol == line.Count) {
							line = model.GetLine (nRow);
							if (Rune.IsLetterOrDigit (line [nCol]) || Rune.IsPunctuation (line [nCol])) {
								return;
							}
						}
						ProcMoveNext (ref nCol, ref nRow, nRune);
					}
				}

				ProcMoveNext (ref col, ref row, rune);

				if (fromCol != col || fromRow != row)
					return (col, row);
				return null;
			} catch (Exception) {
				return null;
			}
		}

		(int col, int row)? WordBackward (int fromCol, int fromRow)
		{
			if (fromRow == 0 && fromCol == 0)
				return null;

			var col = Math.Max (fromCol - 1, 0);
			var row = fromRow;
			try {
				var rune = RuneAt (col, row);
				int lastValidCol = Rune.IsLetterOrDigit (rune) || Rune.IsPunctuation (rune) ? col : -1;

				void ProcMovePrev (ref int nCol, ref int nRow, Rune nRune)
				{
					if (Rune.IsSymbol (nRune) || Rune.IsWhiteSpace (nRune)) {
						while (MovePrev (ref nCol, ref nRow, out nRune)) {
							if (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune)) {
								lastValidCol = nCol;
								break;
							}
						}
						if (nRow != fromRow && (Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune))) {
							if (lastValidCol > -1) {
								nCol = lastValidCol;
							}
							return;
						}
						while (MovePrev (ref nCol, ref nRow, out nRune)) {
							if (!Rune.IsLetterOrDigit (nRune) && !Rune.IsPunctuation (nRune))
								break;
							if (nRow != fromRow) {
								break;
							}
							lastValidCol = nCol;
						}
						if (lastValidCol > -1) {
							nCol = lastValidCol;
							nRow = fromRow;
						}
					} else {
						if (!MovePrev (ref nCol, ref nRow, out nRune)) {
							return;
						}

						var line = model.GetLine (nRow);
						if (nCol == 0 && nRow == fromRow && (Rune.IsLetterOrDigit (line [0]) || Rune.IsPunctuation (line [0]))) {
							return;
						}
						lastValidCol = Rune.IsLetterOrDigit (nRune) || Rune.IsPunctuation (nRune) ? nCol : lastValidCol;
						if (lastValidCol > -1 && (Rune.IsSymbol (nRune) || Rune.IsWhiteSpace (nRune))) {
							nCol = lastValidCol;
							return;
						}
						if (fromRow != nRow) {
							nCol = line.Count;
							return;
						}
						ProcMovePrev (ref nCol, ref nRow, nRune);
					}
				}

				ProcMovePrev (ref col, ref row, rune);

				if (fromCol != col || fromRow != row)
					return (col, row);
				return null;
			} catch (Exception) {
				return null;
			}
		}

		///<inheritdoc/>
		public override bool MouseEvent (MouseEvent ev)
		{
			if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked) && !ev.Flags.HasFlag (MouseFlags.Button1Pressed)
				&& !ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)
				&& !ev.Flags.HasFlag (MouseFlags.Button1Released)
				&& !ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ButtonShift)
				&& !ev.Flags.HasFlag (MouseFlags.WheeledDown) && !ev.Flags.HasFlag (MouseFlags.WheeledUp)
				&& !ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
				&& !ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked | MouseFlags.ButtonShift)) {
				return false;
			}

			if (!CanFocus) {
				return true;
			}

			if (!HasFocus) {
				SetFocus ();
			}

			continuousFind = false;

			if (ev.Flags == MouseFlags.Button1Clicked) {
				if (shiftSelecting) {
					shiftSelecting = false;
					selecting = false;
				}
				ProcessMouseClick (ev, out _);
				PositionCursor ();
				lastWasKill = false;
				columnTrack = currentColumn;
			} else if (ev.Flags == MouseFlags.WheeledDown) {
				lastWasKill = false;
				columnTrack = currentColumn;
				ScrollTo (topRow + 1);
			} else if (ev.Flags == MouseFlags.WheeledUp) {
				lastWasKill = false;
				columnTrack = currentColumn;
				ScrollTo (topRow - 1);
			} else if (ev.Flags == MouseFlags.WheeledRight) {
				lastWasKill = false;
				columnTrack = currentColumn;
				ScrollTo (leftColumn + 1, false);
			} else if (ev.Flags == MouseFlags.WheeledLeft) {
				lastWasKill = false;
				columnTrack = currentColumn;
				ScrollTo (leftColumn - 1, false);
			} else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) {
				ProcessMouseClick (ev, out List<Rune> line);
				PositionCursor ();
				if (model.Count > 0 && shiftSelecting && selecting) {
					if (currentRow - topRow + BottomOffset >= Frame.Height - 1
						&& model.Count + BottomOffset > topRow + currentRow) {
						ScrollTo (topRow + Frame.Height);
					} else if (topRow > 0 && currentRow <= topRow) {
						ScrollTo (topRow - Frame.Height);
					} else if (ev.Y >= Frame.Height) {
						ScrollTo (model.Count + BottomOffset);
					} else if (ev.Y < 0 && topRow > 0) {
						ScrollTo (0);
					}
					if (currentColumn - leftColumn + RightOffset >= Frame.Width - 1
						&& line.Count + RightOffset > leftColumn + currentColumn) {
						ScrollTo (leftColumn + Frame.Width, false);
					} else if (leftColumn > 0 && currentColumn <= leftColumn) {
						ScrollTo (leftColumn - Frame.Width, false);
					} else if (ev.X >= Frame.Width) {
						ScrollTo (line.Count + RightOffset, false);
					} else if (ev.X < 0 && leftColumn > 0) {
						ScrollTo (0, false);
					}
				}
				lastWasKill = false;
				columnTrack = currentColumn;
			} else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ButtonShift)) {
				if (!shiftSelecting) {
					StartSelecting ();
				}
				ProcessMouseClick (ev, out _);
				PositionCursor ();
				lastWasKill = false;
				columnTrack = currentColumn;
			} else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed)) {
				if (shiftSelecting) {
					shiftSelecting = false;
					selecting = false;
				}
				ProcessMouseClick (ev, out _);
				PositionCursor ();
				if (!selecting) {
					StartSelecting ();
				}
				lastWasKill = false;
				columnTrack = currentColumn;
				if (Application.mouseGrabView == null) {
					Application.GrabMouse (this);
				}
			} else if (ev.Flags.HasFlag (MouseFlags.Button1Released)) {
				Application.UngrabMouse ();
			} else if (ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked)) {
				if (ev.Flags.HasFlag (MouseFlags.ButtonShift)) {
					if (!selecting) {
						StartSelecting ();
					}
				} else if (selecting) {
					StopSelecting ();
				}
				ProcessMouseClick (ev, out List<Rune> line);
				(int col, int row)? newPos = null;
				if (currentColumn > 0 && line [currentColumn - 1] != ' ') {
					newPos = WordBackward (currentColumn, currentRow);
					if (newPos.HasValue) {
						currentColumn = newPos.Value.col;
						currentRow = newPos.Value.row;
					}
				}
				if (!selecting) {
					StartSelecting ();
				}
				if (currentRow < selectionStartRow || currentRow == selectionStartRow && currentColumn < selectionStartColumn) {
					if (currentColumn > 0 && line [currentColumn - 1] != ' ') {
						newPos = WordBackward (currentColumn, currentRow);
					}
				} else {
					newPos = WordForward (currentColumn, currentRow);
				}
				if (newPos != null && newPos.HasValue) {
					currentColumn = newPos.Value.col;
					currentRow = newPos.Value.row;
				}
				PositionCursor ();
				lastWasKill = false;
				columnTrack = currentColumn;
			}

			return true;
		}

		void ProcessMouseClick (MouseEvent ev, out List<Rune> line)
		{
			List<Rune> r = null;
			if (model.Count > 0) {
				var maxCursorPositionableLine = Math.Max ((model.Count - 1) - topRow, 0);
				if (Math.Max (ev.Y, 0) > maxCursorPositionableLine) {
					currentRow = maxCursorPositionableLine + topRow;
				} else {
					currentRow = Math.Max (ev.Y + topRow, 0);
				}
				r = GetCurrentLine ();
				var idx = TextModel.GetColFromX (r, leftColumn, Math.Max (ev.X, 0), TabWidth);
				if (idx - leftColumn >= r.Count + RightOffset) {
					currentColumn = Math.Max (r.Count - leftColumn + RightOffset, 0);
				} else {
					currentColumn = idx + leftColumn;
				}
			}

			line = r;
		}

		///<inheritdoc/>
		public override bool OnLeave (View view)
		{
			if (Application.mouseGrabView != null && Application.mouseGrabView == this) {
				Application.UngrabMouse ();
			}

			return base.OnLeave (view);
		}
	}

}
