/* jslint node: true */
'use strict';

const View			= require('./view.js').View;
const strUtil		= require('./string_util.js');
const ansi			= require('./ansi_term.js');
const colorCodes	= require('./color_codes.js');
const wordWrapText	= require('./word_wrap.js').wordWrapText;
const ansiPrep		= require('./ansi_prep.js');

const assert		= require('assert');
const _				= require('lodash');

//	:TODO: Determine CTRL-* keys for various things
	//	See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt
	//	http://wiki.synchro.net/howto:editor:slyedit#edit_mode
	//	http://sublime-text-unofficial-documentation.readthedocs.org/en/latest/reference/keyboard_shortcuts_win.html

	/* Mystic
	 [^B]  Reformat Paragraph            [^O]  Show this help file
       [^I]  Insert tab space              [^Q]  Enter quote mode
       [^K]  Cut current line of text      [^V]  Toggle insert/overwrite
       [^U]  Paste previously cut text     [^Y]  Delete current line


                            BASIC MOVEMENT COMMANDS

                  UP/^E       LEFT/^S      PGUP/^R      HOME/^F
                DOWN/^X      RIGHT/^D      PGDN/^C       END/^G
*/

//
//	Some other interesting implementations, resources, etc.
//
//	Editors - BBS
//	*	https://github.com/M-griffin/Enthral/blob/master/src/msg_fse.cpp
//
//
//	Editors - Other
//	*	http://joe-editor.sourceforge.net/
//	* 	http://www.jbox.dk/downloads/edit.c
//	*	https://github.com/dominictarr/hipster
//
//	Implementations - Word Wrap
//	*	https://github.com/protomouse/synchronet/blob/93b01c55b3102ebc3c4f4793c3a45b8c13d0dc2a/src/sbbs3/wordwrap.c
//
//	Misc notes
//	* https://github.com/dominictarr/hipster/issues/15 (Deleting lines/etc.)
//
//	Blessed
//		insertLine: CSR(top, bottom) + CUP(y, 0) + IL(1) + CSR(0, height)
//		deleteLine: CSR(top, bottom) + CUP(y, 0) + DL(1) + CSR(0, height)
//	Quick Ansi -- update only what was changed:
//	https://github.com/dominictarr/quickansi

//
//	To-Do
//
//	* Index pos % for emit scroll events
//	* Some of this shoudl be async'd where there is lots of processing (e.g. word wrap)
//	* Fix backspace when col=0 (e.g. bs to prev line)
//	* Add back word delete
//	*


const SPECIAL_KEY_MAP_DEFAULT = {
	'line feed'		: [ 'return' ],
	exit			: [ 'esc' ],
	backspace		: [ 'backspace' ],
	delete			: [ 'delete' ],
	tab				: [ 'tab' ],
	up				: [ 'up arrow' ],
	down			: [ 'down arrow' ],
	end				: [ 'end' ],
	home			: [ 'home' ],
	left			: [ 'left arrow' ],
	right			: [ 'right arrow' ],
	'delete line'	: [ 'ctrl + y' ],
	'page up'		: [ 'page up' ],
	'page down'		: [ 'page down' ],
	insert			: [ 'insert', 'ctrl + v' ],
};

exports.MultiLineEditTextView	= MultiLineEditTextView;

function MultiLineEditTextView(options) {
	if(!_.isBoolean(options.acceptsFocus)) {
		options.acceptsFocus = true;
	}

	if(!_.isBoolean(this.acceptsInput)) {
		options.acceptsInput = true;
	}

	if(!_.isObject(options.specialKeyMap)) {
		options.specialKeyMap = SPECIAL_KEY_MAP_DEFAULT;
	}

	View.call(this, options);

	var self = this;

	//
	//	ANSI seems to want tabs to default to 8 characters. See the following:
	//	* http://www.ansi-bbs.org/ansi-bbs2/control_chars/
	//	* http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt
	//
	//	This seems overkill though, so let's default to 4 :)
	//	:TODO: what shoudl this really be? Maybe 8 is OK
	//
	this.tabWidth			= _.isNumber(options.tabWidth) ? options.tabWidth : 4;

	this.textLines			= [ ];
	this.topVisibleIndex	= 0;
	this.mode				= options.mode || 'edit';	//	edit | preview | read-only

	if ('preview' === this.mode) {
		this.autoScroll = options.autoScroll || true;
		this.tabSwitchesView = true;
	} else {
		this.autoScroll = options.autoScroll || false;
		this.tabSwitchesView = options.tabSwitchesView || false;
	}
	//
	//	cursorPos represents zero-based row, col positions
	//	within the editor itself
	//
	this.cursorPos			= { col : 0, row : 0 };

	this.getSGRFor = function(sgrFor) {
		return {
			text : self.getSGR(),
		}[sgrFor] || self.getSGR();
	};

	this.isEditMode = function() {
		return 'edit' === self.mode;
	};

	this.isPreviewMode = function() {
		return 'preview' === self.mode;
	};

	//	:TODO: Most of the calls to this could be avoided via incrementRow(), decrementRow() that keeps track or such
	this.getTextLinesIndex = function(row) {
		if(!_.isNumber(row)) {
			row = self.cursorPos.row;
		}
		var index = self.topVisibleIndex + row;
		return index;
	};

	this.getRemainingLinesBelowRow = function(row) {
		if(!_.isNumber(row)) {
			row = self.cursorPos.row;
		}
		return self.textLines.length - (self.topVisibleIndex + row) - 1;
	};

	this.getNextEndOfLineIndex = function(startIndex) {
		for(var i = startIndex; i < self.textLines.length; i++) {
			if(self.textLines[i].eol) {
				return i;
			}
		}
		return self.textLines.length;
	};

	this.toggleTextCursor = function(action) {
		self.client.term.rawWrite(`${self.getSGRFor('text')}${'hide' === action ? ansi.hideCursor() : ansi.showCursor()}`);
	};

	this.redrawRows = function(startRow, endRow) {
		self.toggleTextCursor('hide');

		const startIndex	= self.getTextLinesIndex(startRow);
		const endIndex		= Math.min(self.getTextLinesIndex(endRow), self.textLines.length);
		const absPos		= self.getAbsolutePosition(startRow, 0);

		for(let i = startIndex; i < endIndex; ++i) {
			//${self.getSGRFor('text')}
			self.client.term.write(				
				`${ansi.goto(absPos.row++, absPos.col)}${self.getRenderText(i)}`, 
				false	//	convertLineFeeds
			);
		}

		self.toggleTextCursor('show');

		return absPos.row - self.position.row;	//	row we ended on
	};

	this.eraseRows = function(startRow, endRow) {
		self.toggleTextCursor('hide');

		const absPos		= self.getAbsolutePosition(startRow, 0);
		const absPosEnd		= self.getAbsolutePosition(endRow, 0);
		const eraseFiller	= ' '.repeat(self.dimens.width);//new Array(self.dimens.width).join(' ');

		while(absPos.row < absPosEnd.row) {
			self.client.term.write(
				`${ansi.goto(absPos.row++, absPos.col)}${eraseFiller}`,
				false	//	convertLineFeeds
			);
		}

		self.toggleTextCursor('show');
	};

	this.redrawVisibleArea = function() {
		assert(self.topVisibleIndex <= self.textLines.length);
		const lastRow = self.redrawRows(0, self.dimens.height);

		self.eraseRows(lastRow, self.dimens.height);
		/*

		//	:TOOD: create eraseRows(startRow, endRow)
		if(lastRow < self.dimens.height) {
			var absPos	= self.getAbsolutePosition(lastRow, 0);
			var empty	= new Array(self.dimens.width).join(' ');
			while(lastRow++ < self.dimens.height) {
				self.client.term.write(ansi.goto(absPos.row++, absPos.col));
				self.client.term.write(empty);
			}
		}
		*/
	};

	this.getVisibleText = function(index) {
		if(!_.isNumber(index)) {
			index = self.getTextLinesIndex();
		}
		return self.textLines[index].text.replace(/\t/g, ' ');
	};

	this.getText = function(index) {
		if(!_.isNumber(index)) {
			index = self.getTextLinesIndex();
		}
		return self.textLines.length > index ? self.textLines[index].text : '';
	};

	this.getTextLength = function(index) {
		if(!_.isNumber(index)) {
			index = self.getTextLinesIndex();
		}
		return self.textLines.length > index ? self.textLines[index].text.length : 0;
	};

	this.getCharacter = function(index, col) {
		if(!_.isNumber(col)) {
			col = self.cursorPos.col;
		}
		return self.getText(index).charAt(col);
	};

	this.isTab = function(index, col) {
		return '\t' === self.getCharacter(index, col);
	};

	this.getTextEndOfLineColumn = function(index) {
		return Math.max(0, self.getTextLength(index));
	};

	this.getRenderText = function(index) {
		let text 		= self.getVisibleText(index);
		const remain	= self.dimens.width - text.length;

		if(remain > 0) {
			text += ' '.repeat(remain + 1);
//			text += new Array(remain + 1).join(' ');
		}

		return text;
	};

	this.getTextLines = function(startIndex, endIndex) {
		var lines;
		if(startIndex === endIndex) {
			lines = [ self.textLines[startIndex] ];
		} else {
			lines = self.textLines.slice(startIndex, endIndex + 1);	//	"slice extracts up to but not including end."
		}
		return lines;
	};

	this.getOutputText = function(startIndex, endIndex, eolMarker, options) {
		const lines	= self.getTextLines(startIndex, endIndex);
		let text	= '';
		const re	= new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g');

		lines.forEach(line => {
			text += line.text.replace(re, '\t');
			
			if(options.forceLineTerms || (eolMarker && line.eol)) {
				text += eolMarker;
			}
		});

		return text;
	};

	this.getContiguousText = function(startIndex, endIndex, includeEol) {
		var lines = self.getTextLines(startIndex, endIndex);
		var text = '';
		for(var i = 0; i < lines.length; ++i) {
			text += lines[i].text;
			if(includeEol && lines[i].eol) {
				text += '\n';
			}
		}
		return text;
	};

	this.replaceCharacterInText = function(c, index, col) {
		self.textLines[index].text = strUtil.replaceAt(
			self.textLines[index].text, col, c);
	};

	/*
	this.editTextAtPosition = function(editAction, text, index, col) {
		switch(editAction) {
			case 'insert' :
				self.insertCharactersInText(text, index, col);
				break;

			case 'deleteForward' :
				break;

			case 'deleteBack' :
				break;

			case 'replace' :
				break;
		}
	};
	*/

	this.updateTextWordWrap = function(index) {
		var nextEolIndex	= self.getNextEndOfLineIndex(index);
		var wrapped			= self.wordWrapSingleLine(self.getContiguousText(index, nextEolIndex), 'tabsIntact');
		var newLines		= wrapped.wrapped;

		for(var i = 0; i < newLines.length; ++i) {
			newLines[i] = { text : newLines[i] };
		}
		newLines[newLines.length - 1].eol = true;

		Array.prototype.splice.apply(
			self.textLines,
			[ index, (nextEolIndex - index) + 1 ].concat(newLines));

		return wrapped.firstWrapRange;
	};

	this.removeCharactersFromText = function(index, col, operation, count) {
		if('delete' === operation) {
			self.textLines[index].text =
				self.textLines[index].text.slice(0, col) +
				self.textLines[index].text.slice(col + count);

			self.updateTextWordWrap(index);
			self.redrawRows(self.cursorPos.row, self.dimens.height);
			self.moveClientCursorToCursorPos();
		} else if ('backspace' === operation) {
			//	:TODO: method for splicing text
			self.textLines[index].text =
				self.textLines[index].text.slice(0, col - (count - 1)) +
				self.textLines[index].text.slice(col + 1);

			self.cursorPos.col -= (count - 1);

			self.updateTextWordWrap(index);
			self.redrawRows(self.cursorPos.row, self.dimens.height);

			self.moveClientCursorToCursorPos();
		} else if('delete line' === operation) {
			//
			//	Delete a visible line. Note that this is *not* the "physical" line, or
			//	1:n entries up to eol! This is to keep consistency with home/end, and
			//	some other text editors such as nano. Sublime for example want to
			//	treat all of these things using the physical approach, but this seems
			//	a bit odd in this context.
			//
			var isLastLine	= (index === self.textLines.length - 1);
			var hadEol		= self.textLines[index].eol;

			self.textLines.splice(index, 1);
			if(hadEol && self.textLines.length > index && !self.textLines[index].eol) {
				self.textLines[index].eol = true;
			}

			//
			//	Create a empty edit buffer if necessary
			//	:TODO: Make this a method
			if(self.textLines.length < 1) {
				self.textLines = [ { text : '', eol : true } ];
				isLastLine = false;	//	resetting
			}

			self.cursorPos.col = 0;

			var lastRow = self.redrawRows(self.cursorPos.row, self.dimens.height);
			self.eraseRows(lastRow, self.dimens.height);

			//
			//	If we just deleted the last line in the buffer, move up
			//
			if(isLastLine) {
				self.cursorEndOfPreviousLine();
			} else {
				self.moveClientCursorToCursorPos();
			}
		}
	};

	this.insertCharactersInText = function(c, index, col) {
		self.textLines[index].text = [
			self.textLines[index].text.slice(0, col),
			c,
			self.textLines[index].text.slice(col)
		].join('');

		//self.cursorPos.col++;
		self.cursorPos.col += c.length;

		var cursorOffset;
		var absPos;

		if(self.getTextLength(index) > self.dimens.width) {
			//
			//	Update word wrapping and |cursorOffset| if the cursor
			//	was within the bounds of the wrapped text
			//
			var lastCol			= self.cursorPos.col - c.length;
			var firstWrapRange	= self.updateTextWordWrap(index);
			if(lastCol >= firstWrapRange.start && lastCol <= firstWrapRange.end) {
				cursorOffset = self.cursorPos.col - firstWrapRange.start;
			}

			//	redraw from current row to end of visible area
			self.redrawRows(self.cursorPos.row, self.dimens.height);

			if(!_.isUndefined(cursorOffset)) {
				self.cursorBeginOfNextLine();
				self.cursorPos.col += cursorOffset;
				self.client.term.rawWrite(ansi.right(cursorOffset));
			} else {
				self.moveClientCursorToCursorPos();
			}
		} else {
			//
			//	We must only redraw from col -> end of current visible line
			//
			absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col);
			self.client.term.write(
				ansi.hideCursor() +
				self.getSGRFor('text') +
				self.getRenderText(index).slice(self.cursorPos.col - c.length) +
				ansi.goto(absPos.row, absPos.col) +
				ansi.showCursor(), false
				);
		}
	};

	this.getRemainingTabWidth = function(col) {
		if(!_.isNumber(col)) {
			col = self.cursorPos.col;
		}
		return self.tabWidth - (col % self.tabWidth);
	};

	this.calculateTabStops = function() {
		self.tabStops = [ 0 ];
		var col = 0;
		while(col < self.dimens.width) {
			col += self.getRemainingTabWidth(col);
			self.tabStops.push(col);
		}
	};

	this.getNextTabStop = function(col) {
		var i = self.tabStops.length;
		while(self.tabStops[--i] > col);
		return self.tabStops[++i];
	};

	this.getPrevTabStop = function(col) {
		var i = self.tabStops.length;
		while(self.tabStops[--i] >= col);
		return self.tabStops[i];
	};

	this.expandTab = function(col, expandChar) {
		expandChar = expandChar || ' ';
		return new Array(self.getRemainingTabWidth(col)).join(expandChar);
	};

	this.wordWrapSingleLine = function(s, tabHandling, width) {
		if(!_.isNumber(width)) {
			width = self.dimens.width;
		}

		return wordWrapText(
			s, 
			{
				width		: width,
				tabHandling	: tabHandling || 'expand',
				tabWidth	: self.tabWidth,
				tabChar		: '\t',
			}
		);
	};

	this.setTextLines = function(lines, index, termWithEol) {
		if(0 === index && (0 === self.textLines.length || (self.textLines.length === 1 && '' === self.textLines[0].text) )) {
			//	quick path: just set the things
			self.textLines = lines.slice(0, -1).map(l => {
				return { text : l };
			}).concat( { text : lines[lines.length - 1], eol : termWithEol } );
		} else {
			//	insert somewhere in textLines...
			if(index > self.textLines.length) {
				//	fill with empty
				self.textLines.splice(
					self.textLines.length,
					0,
					...Array.from( { length : index - self.textLines.length } ).map( () => { return { text : '' }; } )
				);
			}

			const newLines = lines.slice(0, -1).map(l => {
				return { text : l };
			}).concat( { text : lines[lines.length - 1], eol : termWithEol } );

			self.textLines.splice(
				index,
				0,
				...newLines
			);
		}
	};

	this.setAnsiWithOptions = function(ansi, options, cb) {

		function setLines(text) {
			text = strUtil.splitTextAtTerms(text);

			let index = 0;

			text.forEach(line => {
				self.setTextLines( [ line ], index, true);	//	true=termWithEol
				index += 1;
			});

			self.cursorStartOfDocument();

			if(cb) {
				return cb(null);
			}
		}

		if(options.prepped) {
			return setLines(ansi);
		}

		ansiPrep(
			ansi,
			{
				termWidth			: this.client.term.termWidth,
				termHeight			: this.client.term.termHeight,
				cols				: this.dimens.width,
				rows				: 'auto',
				startCol			: this.position.col,
				forceLineTerm		: options.forceLineTerm,
			},
			(err, preppedAnsi) => {
				return setLines(err ? ansi : preppedAnsi);
			}
		);
	};

	this.insertRawText = function(text, index, col) {
		//
		//	Perform the following on |text|:
		//	*	Normalize various line feed formats -> \n
		//	*	Remove some control characters (e.g. \b)
		//	*	Word wrap lines such that they fit in the visible workspace.
		//		Each actual line will then take 1:n elements in textLines[].
		//	*	Each tab will be appropriately expanded and take 1:n \t
		//		characters. This allows us to know when we're in tab space
		//		when doing cursor movement/etc.
		//
		//
		//	Try to handle any possible newline that can be fed to us.
		//	See http://stackoverflow.com/questions/5034781/js-regex-to-split-by-line
		//
		//	:TODO: support index/col insertion point

		if(_.isNumber(index)) {
			if(_.isNumber(col)) {
				//
				//	Modify text to have information from index
				//	before and and after column
				//
				//	:TODO: Need to clean this string (e.g. collapse tabs)
				text = self.textLines;

				//	:TODO: Remove original line @ index
			}
		} else {
			index = self.textLines.length;
		}

		text = strUtil.splitTextAtTerms(text);

		let wrapped;
		text.forEach(line => {
			wrapped = self.wordWrapSingleLine(
				line,	 	//	line to wrap
				'expand', 	//	tabHandling
				self.dimens.width
			).wrapped;

			self.setTextLines(wrapped, index, true);	//	true=termWithEol
			index += wrapped.length;
		});
	};

	this.getAbsolutePosition = function(row, col) {
		return {
			row : self.position.row + row,
			col : self.position.col + col,
		};
	};

	this.moveClientCursorToCursorPos = function() {
		var absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col);
		self.client.term.rawWrite(ansi.goto(absPos.row, absPos.col));
	};


	this.keyPressCharacter = function(c) {
		var index = self.getTextLinesIndex();

		//
		//	:TODO: stuff that needs to happen
		//	* Break up into smaller methods
		//	* Even in overtype mode, word wrapping must apply if past bounds
		//	* A lot of this can be used for backspacing also
		//	* See how Sublime treats tabs in *non* overtype mode... just overwrite them?
		//
		//

		if(self.overtypeMode) {
			//	:TODO: special handing for insert over eol mark?
			self.replaceCharacterInText(c, index, self.cursorPos.col);
			self.cursorPos.col++;
			self.client.term.write(c);
		} else {
			self.insertCharactersInText(c, index, self.cursorPos.col);
		}

		self.emitEditPosition();
	};

	this.keyPressUp = function() {
		if(self.cursorPos.row > 0) {
			self.cursorPos.row--;
			self.client.term.rawWrite(ansi.up());

			if(!self.adjustCursorToNextTab('up')) {
				self.adjustCursorIfPastEndOfLine(false);
			}
		} else {
			self.scrollDocumentDown();
			self.adjustCursorIfPastEndOfLine(true);
		}

		self.emitEditPosition();
	};

	this.keyPressDown = function() {
		var lastVisibleRow = Math.min(
			self.dimens.height,
			(self.textLines.length - self.topVisibleIndex)) - 1;

		if(self.cursorPos.row < lastVisibleRow) {
			self.cursorPos.row++;
			self.client.term.rawWrite(ansi.down());

			if(!self.adjustCursorToNextTab('down')) {
				self.adjustCursorIfPastEndOfLine(false);
			}
		} else {
			self.scrollDocumentUp();
			self.adjustCursorIfPastEndOfLine(true);
		}

		self.emitEditPosition();
	};

	this.keyPressLeft = function() {
		if(self.cursorPos.col > 0) {
			var prevCharIsTab = self.isTab();

			self.cursorPos.col--;
			self.client.term.rawWrite(ansi.left());

			if(prevCharIsTab) {
				self.adjustCursorToNextTab('left');
			}
		} else {
			self.cursorEndOfPreviousLine();
		}

		self.emitEditPosition();
	};

	this.keyPressRight = function() {
		var eolColumn = self.getTextEndOfLineColumn();
		if(self.cursorPos.col < eolColumn) {
			var prevCharIsTab = self.isTab();

			self.cursorPos.col++;
			self.client.term.rawWrite(ansi.right());

			if(prevCharIsTab) {
				self.adjustCursorToNextTab('right');
			}
		} else {
			self.cursorBeginOfNextLine();
		}

		self.emitEditPosition();
	};

	this.keyPressHome = function() {
		var firstNonWhitespace = self.getVisibleText().search(/\S/);
		if(-1 !== firstNonWhitespace) {
			self.cursorPos.col = firstNonWhitespace;
		} else {
			self.cursorPos.col = 0;
		}
		self.moveClientCursorToCursorPos();

		self.emitEditPosition();
	};

	this.keyPressEnd = function() {
		self.cursorPos.col = self.getTextEndOfLineColumn();
		self.moveClientCursorToCursorPos();
		self.emitEditPosition();
	};

	this.keyPressPageUp = function() {
		if(self.topVisibleIndex > 0) {
			self.topVisibleIndex = Math.max(0, self.topVisibleIndex - self.dimens.height);
			self.redraw();
			self.adjustCursorIfPastEndOfLine(true);
		} else {
			self.cursorPos.row = 0;
			self.moveClientCursorToCursorPos();	//	:TODO: ajust if eol, etc.
		}

		self.emitEditPosition();
	};

	this.keyPressPageDown = function() {
		var linesBelow = self.getRemainingLinesBelowRow();
		if(linesBelow > 0) {
			self.topVisibleIndex += Math.min(linesBelow, self.dimens.height);
			self.redraw();
			self.adjustCursorIfPastEndOfLine(true);
		}

		self.emitEditPosition();
	};

	this.keyPressLineFeed = function() {
		//
		//	Break up text from cursor position, redraw, and update cursor
		//	position to start of next line
		//
		var index			= self.getTextLinesIndex();
		var nextEolIndex	= self.getNextEndOfLineIndex(index);
		var text			= self.getContiguousText(index, nextEolIndex);
		var newLines		= self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped;

		newLines.unshift( { text : text.slice(0, self.cursorPos.col), eol : true } );
		for(var i = 1; i < newLines.length; ++i) {
			newLines[i] = { text : newLines[i] };
		}
		newLines[newLines.length - 1].eol = true;

		Array.prototype.splice.apply(
			self.textLines,
			[ index, (nextEolIndex - index) + 1 ].concat(newLines));

		//	redraw from current row to end of visible area
		self.redrawRows(self.cursorPos.row, self.dimens.height);
		self.cursorBeginOfNextLine();

		self.emitEditPosition();
	};

	this.keyPressInsert = function() {
		self.toggleTextEditMode();
	};

	this.keyPressTab = function() {
		var index = self.getTextLinesIndex();
		self.insertCharactersInText(self.expandTab(self.cursorPos.col, '\t') + '\t', index, self.cursorPos.col);

		self.emitEditPosition();
	};

	this.keyPressBackspace = function() {
		if(self.cursorPos.col >= 1) {
			//
			//	Don't want to delete character at cursor, but rather the character
			//	to the left of the cursor!
			//
			self.cursorPos.col -= 1;

			var index = self.getTextLinesIndex();
			var count;

			if(self.isTab()) {
				var col = self.cursorPos.col;
				var prevTabStop = self.getPrevTabStop(self.cursorPos.col);
				while(col >= prevTabStop) {
					if(!self.isTab(index, col)) {
						break;
					}
					--col;
				}

				count = (self.cursorPos.col - col);
			} else {
				count = 1;
			}

			self.removeCharactersFromText(
				index,
				self.cursorPos.col,
				'backspace',
				count);
		} else {
			//
			//	Delete character at end of line previous.
			//	* This may be a eol marker
			//	* Word wrapping will need re-applied
			//
			//	:TODO: apply word wrapping such that text can be re-adjusted if it can now fit on prev
			self.keyPressLeft();	//	same as hitting left - jump to previous line
			//self.keyPressBackspace();
		}

		self.emitEditPosition();
	};

	this.keyPressDelete = function() {
		const lineIndex = self.getTextLinesIndex();

		if(0 === self.cursorPos.col && 0 === self.textLines[lineIndex].text.length && self.textLines.length > 0) {
			//
			//	Start of line and nothing left. Just delete the line
			//
			self.removeCharactersFromText(
				lineIndex,
				0,
				'delete line'
			);
		} else {
			self.removeCharactersFromText(
				lineIndex,
				self.cursorPos.col,
				'delete',
				1
			);
		}

		self.emitEditPosition();
	};

	this.keyPressDeleteLine = function() {
		if(self.textLines.length > 0) {
			self.removeCharactersFromText(
				self.getTextLinesIndex(),
				0,
				'delete line');
		}

		self.emitEditPosition();
	};

	this.adjustCursorIfPastEndOfLine = function(forceUpdate) {
		var eolColumn = self.getTextEndOfLineColumn();
		if(self.cursorPos.col > eolColumn) {
			self.cursorPos.col = eolColumn;
			forceUpdate = true;
		}

		if(forceUpdate) {
			self.moveClientCursorToCursorPos();
		}
	};

	this.adjustCursorToNextTab = function(direction) {
		if(self.isTab()) {
			var move;
			switch(direction) {
				//
				//	Next tabstop to the right
				//
				case 'right' :
					move = self.getNextTabStop(self.cursorPos.col) - self.cursorPos.col;
					self.cursorPos.col += move;
					self.client.term.rawWrite(ansi.right(move));
					break;

				//
				//	Next tabstop to the left
				//
				case 'left' :
					move = self.cursorPos.col - self.getPrevTabStop(self.cursorPos.col);
					self.cursorPos.col -= move;
					self.client.term.rawWrite(ansi.left(move));
					break;

				case 'up' :
				case 'down' :
					//
					//	Jump to the tabstop nearest the cursor
					//
					var newCol = self.tabStops.reduce(function r(prev, curr) {
						return (Math.abs(curr - self.cursorPos.col) < Math.abs(prev - self.cursorPos.col) ? curr : prev);
					});

					if(newCol > self.cursorPos.col) {
						move = newCol - self.cursorPos.col;
						self.cursorPos.col += move;
						self.client.term.rawWrite(ansi.right(move));
					} else if(newCol < self.cursorPos.col) {
						move = self.cursorPos.col - newCol;
						self.cursorPos.col -= move;
						self.client.term.rawWrite(ansi.left(move));
					}
					break;
			}

			return true;
		}
		return false;	//	did not fall on a tab
	};

	this.cursorStartOfDocument = function() {
		self.topVisibleIndex	= 0;
		self.cursorPos			= { row : 0, col : 0 };

		self.redraw();
		self.moveClientCursorToCursorPos();
	};

	this.cursorEndOfDocument = function() {
		self.topVisibleIndex	= Math.max(self.textLines.length - self.dimens.height, 0);
		self.cursorPos.row		= (self.textLines.length - self.topVisibleIndex) - 1;
		self.cursorPos.col		= self.getTextEndOfLineColumn();

		self.redraw();
		self.moveClientCursorToCursorPos();
	};

	this.cursorBeginOfNextLine = function() {
		//	e.g. when scrolling right past eol
		var linesBelow = self.getRemainingLinesBelowRow();

		if(linesBelow > 0) {
			var lastVisibleRow	= Math.min(self.dimens.height, self.textLines.length) - 1;
			if(self.cursorPos.row < lastVisibleRow) {
				self.cursorPos.row++;
			} else {
				self.scrollDocumentUp();
			}
			self.keyPressHome();	//	same as pressing 'home'
		}
	};

	this.cursorEndOfPreviousLine = function() {
		//	e.g. when scrolling left past start of line
		var moveToEnd;
		if(self.cursorPos.row > 0) {
			self.cursorPos.row--;
			moveToEnd = true;
		} else if(self.topVisibleIndex > 0) {
			self.scrollDocumentDown();
			moveToEnd = true;
		}

		if(moveToEnd) {
			self.keyPressEnd();	//	same as pressing 'end'
		}
	};

	/*
	this.cusorEndOfNextLine = function() {
		var linesBelow = self.getRemainingLinesBelowRow();

		if(linesBelow > 0) {
			var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1;
			if(self.cursorPos.row < lastVisibleRow) {
				self.cursorPos.row++;
			} else {
				self.scrollDocumentUp();
			}
			self.keyPressEnd();	//	same as pressing 'end'
		}
	};
	*/

	this.scrollDocumentUp = function() {
		//
		//	Note: We scroll *up* when the cursor goes *down* beyond
		//	the visible area!
		//
		var linesBelow = self.getRemainingLinesBelowRow();
		if(linesBelow > 0) {
			self.topVisibleIndex++;
			self.redraw();
		}
	};

	this.scrollDocumentDown = function() {
		//
		//	Note: We scroll *down* when the cursor goes *up* beyond
		//	the visible area!
		//
		if(self.topVisibleIndex > 0) {
			self.topVisibleIndex--;
			self.redraw();
		}
	};

	this.emitEditPosition = function() {
		self.emit('edit position', 	self.getEditPosition());
	};

	this.toggleTextEditMode = function() {
		self.overtypeMode = !self.overtypeMode;
		self.emit('text edit mode', self.getTextEditMode());
	};

	this.insertRawText('');	//	init to blank/empty
}

require('util').inherits(MultiLineEditTextView, View);

MultiLineEditTextView.prototype.setWidth = function(width) {
	MultiLineEditTextView.super_.prototype.setWidth.call(this, width);

	this.calculateTabStops();
};

MultiLineEditTextView.prototype.redraw = function() {
	MultiLineEditTextView.super_.prototype.redraw.call(this);

	this.redrawVisibleArea();
};

MultiLineEditTextView.prototype.setFocus = function(focused) {
	this.client.term.rawWrite(this.getSGRFor('text'));
	this.moveClientCursorToCursorPos();

	MultiLineEditTextView.super_.prototype.setFocus.call(this, focused);
};

MultiLineEditTextView.prototype.setText = function(text, options = { scrollMode : 'default' } ) {
	this.textLines = [ ];
	this.addText(text, options);
	/*this.insertRawText(text);

	if(this.isEditMode()) {
		this.cursorEndOfDocument();
	} else if(this.isPreviewMode()) {
		this.cursorStartOfDocument();
	}*/
};

MultiLineEditTextView.prototype.setAnsi = function(ansi, options = { prepped : false }, cb) {
	this.textLines = [ ];
	return this.setAnsiWithOptions(ansi, options, cb);
};

MultiLineEditTextView.prototype.addText = function(text, options = { scrollMode : 'default' }) {
	this.insertRawText(text);

	switch(options.scrollMode) {
		case 'default' :
			if(this.isEditMode() || this.autoScroll) {
				this.cursorEndOfDocument();
			} else {
				this.cursorStartOfDocument();
			}
			break;

		case 'top' :
		case 'start' :
			this.cursorStartOfDocument();
			break;

		case 'end' :
		case 'bottom' :
			this.cursorEndOfDocument();
			break;
	}
};

MultiLineEditTextView.prototype.getData = function(options = { forceLineTerms : false } ) {
	return this.getOutputText(0, this.textLines.length, '\r\n', options);
};

MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) {
	switch(propName) {
		case 'mode'			: 
			this.mode = value;
			if('preview' === value && !this.specialKeyMap.next) {
				this.specialKeyMap.next = [ 'tab' ];
			} 
			break;

		case 'autoScroll'	: 
			this.autoScroll = value;
			break;

		case 'tabSwitchesView' :
			this.tabSwitchesView = value; 
			this.specialKeyMap.next = this.specialKeyMap.next || [];
			this.specialKeyMap.next.push('tab');
			break;
	}

	MultiLineEditTextView.super_.prototype.setPropertyValue.call(this, propName, value);
};

const HANDLED_SPECIAL_KEYS = [
	'up', 'down', 'left', 'right',
	'home', 'end',
	'page up', 'page down',
	'line feed',
	'insert',
	'tab',
	'backspace', 'delete',
	'delete line',
];

const PREVIEW_MODE_KEYS = [
	'up', 'down', 'page up', 'page down'
];

MultiLineEditTextView.prototype.onKeyPress = function(ch, key) {
	const self = this;
	let handled;

	if(key) {
		HANDLED_SPECIAL_KEYS.forEach(function aKey(specialKey) {
			if(self.isKeyMapped(specialKey, key.name)) {

				if(self.isPreviewMode() && -1 === PREVIEW_MODE_KEYS.indexOf(specialKey)) {
					return;
				}

				if('tab' !== key.name || !self.tabSwitchesView) {
					self[_.camelCase('keyPress ' + specialKey)]();
					handled = true;
				}
			}
		});
	}

	if(self.isEditMode() && ch && strUtil.isPrintable(ch)) {
		this.keyPressCharacter(ch);
	}

	if(!handled) {
		MultiLineEditTextView.super_.prototype.onKeyPress.call(this, ch, key);
	}
};

MultiLineEditTextView.prototype.scrollUp = function() {
	this.scrollDocumentUp();
};

MultiLineEditTextView.prototype.scrollDown = function() {
	this.scrollDocumentDown();
};

MultiLineEditTextView.prototype.deleteLine = function(line) {
	this.textLines.splice(line, 1);
};

MultiLineEditTextView.prototype.getLineCount = function() {
	return this.textLines.length;
};

MultiLineEditTextView.prototype.getTextEditMode = function() {
	return this.overtypeMode ? 'overtype' : 'insert';
};

MultiLineEditTextView.prototype.getEditPosition = function() {
	var currentIndex = this.getTextLinesIndex() + 1;

	return {
		row		: this.getTextLinesIndex(this.cursorPos.row),
		col 	: this.cursorPos.col,
		percent	: Math.floor(((currentIndex / this.textLines.length) * 100)),
		below	: this.getRemainingLinesBelowRow(),
	};
};