/*
	Terminal Kit

	Copyright (c) 2009 - 2022 Cédric Ronvel

	The MIT License (MIT)

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
*/

"use strict" ;



const NextGenEvents = require( 'nextgen-events' ) ;
const Promise = require( 'seventh' ) ;
const string = require( 'string-kit' ) ;
const autoComplete = require( './autoComplete.js' ) ;



const defaultKeyBindings = {
	ENTER: 'submit' ,
	KP_ENTER: 'submit' ,
	ESCAPE: 'cancel' ,
	BACKSPACE: 'backDelete' ,
	DELETE: 'delete' ,
	LEFT: 'backward' ,
	RIGHT: 'forward' ,
	UP: 'historyPrevious' ,
	DOWN: 'historyNext' ,
	HOME: 'startOfInput' ,
	END: 'endOfInput' ,
	TAB: 'autoComplete' ,
	CTRL_R: 'autoCompleteUsingHistory' ,
	CTRL_LEFT: 'previousWord' ,
	CTRL_RIGHT: 'nextWord' ,
	ALT_D: 'deleteNextWord' ,
	CTRL_W: 'deletePreviousWord' ,
	CTRL_U: 'deleteAllBefore' ,
	CTRL_K: 'deleteAllAfter'
} ;



const defaultTokenRegExp = /\S+/g ;



/*
	inputField( [options] , callback )
		* options `Object` where:
			* y `number` the line where the input field will start (default to the current cursor location)
			* x `number` the column where the input field will start (default to the current cursor location,
			  or 1 if the *y* option is defined)
			* echo `boolean` if true (the default), input are displayed on the terminal
			* echoChar `string` or `true` if set, all characters are replaced by this one (useful for password fields),
			  if true, it is replaced by a dot: •
			* default `string` default input/placeholder
			* cursorPosition `integer` (default: -1, end of input) set the cursor position/offset in the input,
			  negative value starts from the end
			* cancelable `boolean` if true (default: false), it is cancelable by user using the cancel key (default: ESC),
			  thus will return undefined
			* style `Function` style used, default to the terminal instance (no style)
			* hintStyle `Function` style used for hint (auto-completion preview), default to `terminal.brightBlack` (gray)
			* maxLength `number` maximum length of the input
			* minLength `number` minimum length of the input
			* history `Array` (optional) an history array, so UP and DOWN keys move up and down in the history
			* autoComplete `Array` or `Function( inputString , [callback] )` (optional) an array of possible completion,
			  so the TAB key will auto-complete the input field. If it is a function, it should accept an input `string`
			  and return the completed `string` (if no completion can be done, it should return the input string,
			  if multiple candidate are possible, it should return an array of string), if **the function accepts 2 arguments**
			  (checked using *function*.length), then **the auto-completer will be asynchronous**!
			  If it does not accept a callback but returns a *thenable* (Promise-like), it will be asynchronous too.
				/!\ Also, if autoCompleteMenu is set and the array contains a special property 'prefix', it will be prepended
				after autoCompleteMenu()!
			* autoCompleteMenu `boolean` or `Object` of options, used in conjunction with the 'autoComplete' options, if *truthy*
			  any auto-complete attempt having many completion candidates will display a menu to let the user choose between each
			  possibilities. If an object is given, it should contain options for the [.singleLineMenu()](#ref.singleLineMenu)
			  that is used for the completion (notice: some options are overwritten: 'y' and 'exitOnUnexpectedKey')
			* autoCompleteHint `boolean` if true (default: false) use the hintStyle to write the auto-completion preview
			  at the right of the input
			* keyBindings `Object` overide default key bindings
			* tokenHook `Function( token , isEndOfInput , previousTokens , term , config )` this is a hook called for each
			  token of the input, where:
				* token `String` is the current token
				* isEndOfInput `boolean` true if this is the **last token and if it ends the input string**
				  (e.g. it is possible for the last token to be followed by some blank char, in that case *isEndOfInput*
				  would be false)
				* previousTokens `Array` of `String` is a array containing all tokens before the current one
				* term is a Terminal instance
				* config `Object` is an object containing dynamic settings that can be altered by the hook, where:
					* style `Function` style in use (see the *style* option)
					* hintStyle `Function` style in use for hint (see the *hintStyle* option)
					* tokenRegExp `RegExp` the regexp in use for tokenization (see the *tokenRegExp* option)
					* autoComplete `Array` or `Function( inputString , [callback] )` (see the *autoComplete* option)
					* autoCompleteMenu `boolean` or `Object` (see the *autoCompleteMenu* option)
					* autoCompleteHint `boolean` enable/disable the auto-completion preview (see the *autoCompleteHint* option)
			  The config settings are always reset on new input, on new tokenization pass.
			  The hook can return a *style* (`Function`, like the *style* option) that will be used to print that token.
			  Used together, this can achieve syntax hilighting, as well as dynamic behavior suitable for a shell.
			  Finally, it can return a string, styled or not, that will be displayed in place of that token,
			  useful if the token should have multiple styles (but that string **MUST** contains the same number of
			  printable character, or it would lead hazardous behavior).
			* tokenResetHook `Function( term , config )` this is a hook called before the first token
			* tokenRegExp `RegExp` this is the regex used to tokenize the input, by default a token is space-delimited,
			  so "one two three" would be tokenized as [ "one" , "two" , "three" ].
		* callback( error , input )
			* error `mixed` truthy if an underlying error occurs
			* input `string` the user input
*/
module.exports = function inputField( options , callback ) {
	if ( typeof options === 'function' ) { callback = options ; options = {} ; }
	else if ( ! options || typeof options !== 'object' ) { options = {} ; }

	if ( options.echo === undefined ) { options.echo = true ; }

	if ( typeof options.maxLength !== 'number' ) { options.maxLength = Infinity ; }
	if ( typeof options.minLength !== 'number' ) { options.minLength = 0 ; }

	if ( options.echoChar && typeof options.echoChar !== 'string' ) { options.echoChar = '•' ; }

	if ( options.autoCompleteMenu ) {
		if ( typeof options.autoCompleteMenu !== 'object' ) { options.autoCompleteMenu = {} ; }
		options.autoCompleteMenu.exitOnUnexpectedKey = true ;
		delete options.autoCompleteMenu.y ;
	}

	var keyBindings = options.keyBindings || defaultKeyBindings ;

	if ( options.tokenRegExp && ( ! ( options.tokenRegExp instanceof RegExp ) || ! options.tokenRegExp.flags.includes( 'g' ) ) ) {
		throw new Error( ".inputField(): if set, the 'tokenRegExp' option should be a RegExp with the 'g' flag" ) ;
	}

	if ( ! this.grabbing ) { this.grabInput() ; }



	var controller , finished = false , paused = false , alreadyCleanedUp = false ,
		offset = options.cursorPosition !== undefined ? options.cursorPosition : -1 ,
		echo = !! options.echo ,
		start = {} , end = {} , cursor = {} , endHint = {} ,
		inputs = [] , inputIndex ,
		alwaysRedraw = options.tokenHook || options.autoCompleteHint ,
		hint = [] , meta = false ;

	var dynamic = {
		style: options.style || this ,
		hintStyle: options.hintStyle || this.brightBlack ,
		tokenRegExp: options.tokenRegExp || defaultTokenRegExp ,
		autoComplete: options.autoComplete ,
		autoCompleteMenu: options.autoCompleteMenu ,
		autoCompleteHint: !! options.autoCompleteHint
	} ;



	// Now inputs is an array of input, input being an array of char (thanks to JS using UCS-2 instead of UTF-8)

	if ( Array.isArray( options.history ) ) {
		inputs = options.history.map( str => string.unicode.toArray( str ).slice( 0 , options.maxLength ) ) ;
	}


	if ( options.default && typeof options.default === 'string' ) {
		inputs.push( string.unicode.toArray( options.default ).slice( 0 , options.maxLength ) ) ;
	}
	else {
		inputs.push( [] ) ;
	}



	var init = () => {
		inputIndex = inputs.length - 1 ;
		offset = boundOffset( offset ) ;

		if ( options.y !== undefined ) {
			options.x = options.x || 1 ;
			this.moveTo.eraseLineAfter( options.x , options.y ) ;
			finishInit( options.x , options.y ) ;
		}
		else {
			// Get the cursor location before getting started
			this.getCursorLocation( ( error , x , y ) => {
				if ( error ) {
					// Some bad terminals (windows...) doesn't support cursor location request, we should fallback to a decent behavior.
					// So we just move to the last line, create a new line, and add a little prompt (it would be misleading otherwise).
					//cleanup( error ) ; return ;
					this.row.eraseLineAfter( this.height )( '\n> ' ) ;
					x = 3 ;
					y = this.height ;
				}

				finishInit( x , y ) ;
			} ) ;
		}
	} ;



	var finishInit = ( x , y ) => {
		start.x = end.x = cursor.x = x ;
		start.y = end.y = cursor.y = y ;

		if ( inputs[ inputIndex ].length ) {
			// There is already something (placeholder, ...), so redraw now!
			computeAllCoordinate() ;
			redraw() ;
		}

		this.on( 'key' , onKey ) ;
		//controller.ready = true ;
		controller.emit( 'ready' ) ;
	} ;



	var cleanup = ( error , input ) => {
		if ( alreadyCleanedUp ) { return ; }
		alreadyCleanedUp = true ;

		finished = true ;
		this.removeListener( 'key' , onKey ) ;

		if ( error === 'abort' ) { return ; }

		this.styleReset() ;

		if ( error ) {
			if ( callback ) { callback( error ) ; }
			else { controller.promise.reject( error ) ; }
			return ;
		}

		var value ;

		if ( typeof input === 'string' ) { value = input ; }
		else if ( input ) { value = input.join( '' ) ; }

		if ( callback ) { callback( undefined , value ) ; }
		else { controller.promise.resolve( value ) ; }
	} ;



	// Compute the coordinate of the cursor and end of a string, given a start coordinate
	var computeAllCoordinate = () => {
		var scroll ,
			inputWidth = string.unicode.arrayWidth( inputs[ inputIndex ] ) ,
			hintWidth = string.unicode.arrayWidth( hint ) ;

		end = offsetCoordinate( inputWidth ) ;
		endHint = offsetCoordinate( inputWidth + hintWidth ) ;

		if ( endHint.y > this.height ) {
			// We have gone out of the screen, scroll!
			scroll = endHint.y - this.height ;

			dynamic.style.noFormat( '\n'.repeat( scroll ) ) ;

			start.y -= scroll ;
			end.y -= scroll ;
			endHint.y -= scroll ;
		}

		cursorCoordinate() ;
	} ;



	// Cursor coordinate from the current offset
	var cursorCoordinate = () => {
		cursor = offsetCoordinate( string.unicode.arrayWidth( inputs[ inputIndex ] , offset ) ) ;
	} ;



	// Compute the coordinate of an offset, given a start coordinate
	var offsetCoordinate = offset_ => {
		return {
			x: 1 + ( start.x + offset_ - 1 ) % this.width ,
			y: start.y + Math.floor( ( start.x + offset_ - 1 ) / this.width )
		} ;
	} ;



	var boundOffset = offset_ => {
		if ( typeof offset_ !== 'number' || isNaN( offset_ ) ) { return inputs[ inputIndex ].length ; }

		if ( offset_ < 0 ) { offset_ = inputs[ inputIndex ].length + 1 + offset_ ; }

		if ( offset_ < 0 ) { offset_ = 0 ; }
		else if ( offset_ >= inputs[ inputIndex ].length ) { offset_ = inputs[ inputIndex ].length ; }

		return offset_ ;
	} ;



	// Compute the coordinate of the end of a string, given a start coordinate
	var redraw = ( extraLines , forceClear ) => {
		var i , hintCleared ;

		extraLines = extraLines || 0 ;

		if ( ! dynamic.autoCompleteHint && forceClear ) {
			// Used by history, when autoCompleteHint is off, the current line is not erased
			this.moveTo( end.x , end.y ) ;
			dynamic.style.noFormat.eraseLineAfter( '' ) ;
		}

		this.moveTo( start.x , start.y ) ;

		if ( options.tokenHook ) { writeTokens( inputs[ inputIndex ].join( '' ) ) ; }
		else if ( options.echoChar ) { dynamic.style.noFormat( options.echoChar.repeat( inputs[ inputIndex ].length ) ) ; }
		else { dynamic.style.noFormat( inputs[ inputIndex ].join( '' ) ) ; }

		hintCleared = clearHint() ;

		if ( extraLines > 0 ) {
			// If the previous input was using more lines, erase them now
			for ( i = 1 ; i <= extraLines ; i ++ ) {
				this.moveTo( 1 , end.y + i ) ;
				dynamic.style.noFormat.eraseLineAfter( '' ) ;
			}
		}

		if ( ! hintCleared && ( cursor.y < end.y || end.x === this.width ) ) {
			this.moveTo( end.x , end.y ) ;
			dynamic.style.noFormat.eraseLineAfter( '' ) ;
		}

		this.moveTo( cursor.x , cursor.y ) ;
	} ;



	// Not used internally for instance, only for controller.redrawCursor()
	var redrawCursor = () => {
		if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , redrawCursor ) ; return ; }
		this.moveTo( cursor.x , cursor.y ) ;
	} ;



	var pause = () => {
		if ( paused ) { return ; }
		paused = true ;

		// Don't redraw now if not ready, it will be drawn once ready (avoid double-draw)
		//if ( controller.hasState( 'ready' ) ) { redraw() ; }
	} ;



	var resume = () => {
		if ( ! paused ) { return ; }
		paused = false ;

		// Don't redraw now if not ready, it will be drawn once ready (avoid double-draw)
		if ( controller.hasState( 'ready' ) ) { redraw() ; }
	} ;



	var clearHint = () => {
		// First, check if there are some hints to be cleared
		if ( ! dynamic.autoCompleteHint ) { return false ; }

		var y = end.y ;

		this.moveTo( end.x , end.y ) ;
		dynamic.style.noFormat.eraseLineAfter( '' ) ;

		// If the previous input was using more lines, erase them now
		while ( y < endHint.y ) {
			y ++ ;
			this.moveTo( 1 , y ) ;
			dynamic.style.noFormat.eraseLineAfter( '' ) ;
		}

		this.moveTo( cursor.x , cursor.y ) ;

		return true ;
	} ;



	// Compute the coordinate of the end of a string, given a start coordinate
	var autoCompleteMenu = ( menu ) => {
		paused = true ;

		this.singleLineMenu( menu , dynamic.autoCompleteMenu , ( error , response ) => {
			// Unpause unconditionnally
			paused = false ;
			if ( error ) { return ; }

			if ( response.selectedText ) {
				// Prepend something before the text
				if ( menu.prefix ) { response.selectedText = menu.prefix + response.selectedText ; }

				// Append something after the text
				if ( menu.postfix ) { response.selectedText += menu.postfix ; }

				response.selectedText = string.unicode.toArray( response.selectedText ).slice( 0 , options.maxLength ) ;

				inputs[ inputIndex ] = response.selectedText.concat(
					inputs[ inputIndex ].slice( offset , options.maxLength + offset - response.selectedText.length )
				) ;

				offset = response.selectedText.length ;
			}

			if ( echo ) {
				// Erase the menu
				this.column.eraseLineAfter( 1 ) ;

				// If the input field was ending on the last line, we need to move it one line up
				if ( end.y >= this.height && start.y > 1 ) { start.y -- ; }

				computeAllCoordinate() ;
				redraw() ;
				this.moveTo( cursor.x , cursor.y ) ;
			}

			if ( response.unexpectedKey && response.unexpectedKey !== 'TAB' ) {
				// Forward the key to the event handler
				onKey( response.unexpectedKey , undefined , response.unexpectedKeyData ) ;
			}
		} )
			.on( 'highlight' , eventData => controller.emit( 'highlight' ,  eventData ) ) ;
	} ;



	var writeTokens = ( text ) => {
		var match , lastIndex , lastEndIndex = 0 , tokens = [] , tokenStyle , isEndOfInput ;

		// Reset dynamic stuffs
		dynamic.style = options.style || this ;
		dynamic.hintStyle = options.hintStyle || this.brightBlack ;
		dynamic.tokenRegExp = options.tokenRegExp || defaultTokenRegExp ;
		dynamic.autoComplete = options.autoComplete ;
		dynamic.autoCompleteMenu = options.autoCompleteMenu ;
		dynamic.autoCompleteHint = !! options.autoCompleteHint ;

		dynamic.tokenRegExp.lastIndex = 0 ;

		if ( options.tokenResetHook ) { options.tokenResetHook( this , dynamic ) ; }

		while ( ( match = dynamic.tokenRegExp.exec( text ) ) !== null ) {
			// Back-up that now, since it can be modified by the hook
			lastIndex = dynamic.tokenRegExp.lastIndex ;

			if ( match.index > lastEndIndex ) { dynamic.style.noFormat( text.slice( lastEndIndex , match.index ) ) ; }

			isEndOfInput = match.index + match[ 0 ].length === text.length ;

			tokenStyle = options.tokenHook( match[ 0 ] , isEndOfInput , tokens , this , dynamic ) ;

			if ( typeof tokenStyle === 'function' ) { tokenStyle.noFormat( match[ 0 ] ) ; }
			else if ( typeof tokenStyle === 'string' ) { this.noFormat( tokenStyle ) ; }
			else { dynamic.style.noFormat( match[ 0 ] ) ; }

			tokens.push( match[ 0 ] ) ;

			lastEndIndex = match.index + match[ 0 ].length ;

			// Restore it, if it was modified
			dynamic.tokenRegExp.lastIndex = lastIndex ;
		}

		if ( lastEndIndex < text.length ) { dynamic.style.noFormat( text.slice( lastEndIndex ) ) ; }
	} ;



	var autoCompleteHint = () => {
		// The cursor should be at the end ATM
		if ( ! dynamic.autoComplete || ! dynamic.autoCompleteHint || offset < inputs[ inputIndex ].length ) {
			return ;
		}

		var autoCompleted , inputText = inputs[ inputIndex ].join( '' ) ;

		var finishCompletion = () => {
			if ( Array.isArray( autoCompleted ) ) { return ; }

			hint = string.unicode.toArray( autoCompleted.slice( inputText.length ) )
				.slice( 0 , options.maxLength - inputs[ inputIndex ].length ) ;

			computeAllCoordinate() ;
			this.moveTo( end.x , end.y ) ;	// computeAllCoordinate() can add some newline
			dynamic.hintStyle.noFormat( hint.join( '' ) ) ;
			this.moveTo( cursor.x , cursor.y ) ;
		} ;

		if ( Array.isArray( dynamic.autoComplete ) ) {
			autoCompleted = autoComplete( dynamic.autoComplete , inputText , dynamic.autoCompleteMenu ) ;
		}
		else if ( typeof dynamic.autoComplete === 'function' ) {
			if ( dynamic.autoComplete.length === 2 ) {
				dynamic.autoComplete( inputText , ( error , autoCompleted_ ) => {
					if ( error ) { cleanup( error ) ; return ; }

					autoCompleted = autoCompleted_ ;
					finishCompletion() ;
				} ) ;
				return ;
			}

			autoCompleted = dynamic.autoComplete( inputText ) ;

			if ( Promise.isThenable( autoCompleted ) ) {
				autoCompleted.then(
					autoCompleted_ => {
						autoCompleted = autoCompleted_ ;
						finishCompletion() ;
					} ,
					error => { cleanup( error ) ; }
				) ;
				return ;
			}
		}

		finishCompletion() ;
	} ;



	// The main method: the key event handler
	var onKey = ( key , trash , data ) => {

		if ( finished || paused ) { return ; }

		var leftPart , autoCompleteUsed , autoCompleted , extraLines , charToDelete , cutOffset , altKey ,
			lastOffset = offset ;

		// if previous keystroke triggered the 'meta' keybinding, prepend ALT_ to this key
		if ( meta ) {
			meta = false ;
			altKey = 'ALT_' + key.toUpperCase() ;

			if ( data ) { data.isCharacter = false ; }
			if ( keyBindings[ altKey ] ) { key = altKey ; }
		}

		if ( data && data.isCharacter ) {
			// if data.isCharacter, this is a regular UTF-8 character, not a special key

			if ( inputs[ inputIndex ].length >= options.maxLength ) { return ; }

			// Insert version
			//inputs[ inputIndex ] = inputs[ inputIndex ].slice( 0 , offset ) + key + inputs[ inputIndex ].slice( offset ) ;
			inputs[ inputIndex ].splice( offset , 0 , key ) ;
			offset ++ ;

			if ( echo ) {
				if ( offset === inputs[ inputIndex ].length && ! alwaysRedraw ) {
					dynamic.style.noFormat( options.echoChar || key ) ;
					// Now it's done by computeAllCoordinate()
					//if ( cursor.x >= this.width ) { dynamic.style.noFormat( '\n' ) ; }
					computeAllCoordinate() ;
				}
				else {
					// redraw() is mandatory in insert mode
					computeAllCoordinate() ;
					redraw() ;
					if ( dynamic.autoCompleteHint ) { autoCompleteHint() ; }
				}
			}
		}
		else {
			// Here we have a special key

			switch( keyBindings[ key ] ) {
				case 'submit' :
					if ( inputs[ inputIndex ].length < options.minLength ) { break ; }
					clearHint() ;
					cleanup( undefined , inputs[ inputIndex ] ) ;
					break ;

				case 'cancel' :
					if ( options.cancelable ) { cleanup() ; }
					break ;

				case 'meta' :
					meta = true ;
					break ;

				case 'backDelete' :
					if ( inputs[ inputIndex ].length && offset > 0 ) {
						charToDelete = inputs[ inputIndex ][ offset - 1 ] ;
						inputs[ inputIndex ].splice( offset - 1 , 1 ) ;
						offset -- ;

						if ( echo ) {
							// The cursor position check should happen BEFORE we modify it with computeAllCoordinate()
							if ( cursor.y < end.y || cursor.x === 1 || alwaysRedraw ) {
								computeAllCoordinate() ;
								// Every time something is deleted, we need a redraw with the forceClear option on
								redraw( undefined , true ) ;
								if ( dynamic.autoCompleteHint ) { autoCompleteHint() ; }
							}
							else {
								computeAllCoordinate() ;

								// .backDelete() does not work with full-width, Terminal Kit should stop using it until a reliable escape sequence combo is found
								//this.backDelete() ;
								if ( string.unicode.isFullWidth( charToDelete ) ) {
									this.left( 2 ) ;
									this.delete( 2 ) ;
								}
								else {
									this.left( 1 ) ;
									this.delete( 1 ) ;
								}
							}
						}
					}
					break ;

				case 'delete' :
					if ( inputs[ inputIndex ].length && offset < inputs[ inputIndex ].length ) {
						charToDelete = inputs[ inputIndex ][ offset ] ;
						inputs[ inputIndex ].splice( offset , 1 ) ;

						if ( echo ) {
							// The cursor position check should happen BEFORE we modify it with computeAllCoordinate()
							if ( cursor.y < end.y || alwaysRedraw ) {
								computeAllCoordinate() ;
								// Every time something is deleted, we need a redraw with the forceClear option on
								redraw( undefined , true ) ;
								if ( dynamic.autoCompleteHint ) { autoCompleteHint() ; }
							}
							else {
								computeAllCoordinate() ;
								this.delete( string.unicode.isFullWidth( charToDelete ) ? 2 : 1 ) ;
							}
						}
					}
					break ;

				case 'deleteAllBefore' :
					if ( inputs[ inputIndex ].length && offset > 0 ) {
						//inputs[ inputIndex ] = inputs[ inputIndex ].slice( 0 , offset - 1 ) + inputs[ inputIndex ].slice( offset ) ;
						inputs[ inputIndex ].splice( 0 , offset ) ;
						offset = 0 ;

						if ( echo ) {
							computeAllCoordinate() ;
							// Need forceClear
							redraw( undefined , true ) ;
						}
					}
					break ;

				case 'deleteAllAfter' :
					if ( inputs[ inputIndex ].length && offset < inputs[ inputIndex ].length ) {
						inputs[ inputIndex ].splice( offset , inputs[ inputIndex ].length - offset ) ;

						if ( echo ) {
							computeAllCoordinate() ;
							// Need forceClear
							redraw( undefined , true ) ;
							if ( dynamic.autoCompleteHint ) { autoCompleteHint() ; }
						}
					}
					break ;

				case 'backward' :
					if ( inputs[ inputIndex ].length && offset > 0 ) {
						if ( dynamic.autoCompleteHint && offset === inputs[ inputIndex ].length ) {
							clearHint() ;
						}

						offset -- ;

						if ( echo ) {
							computeAllCoordinate() ;
							this.moveTo( cursor.x , cursor.y ) ;
						}
					}
					break ;

				case 'forward' :
					if ( inputs[ inputIndex ].length && offset < inputs[ inputIndex ].length ) {
						offset ++ ;

						if ( echo ) {
							computeAllCoordinate() ;
							this.moveTo( cursor.x , cursor.y ) ;
						}

						if ( dynamic.autoCompleteHint && offset === inputs[ inputIndex ].length ) {
							autoCompleteHint() ;
						}
					}

					break ;

				case 'deletePreviousWord' :
					if ( inputs[ inputIndex ].length && offset > 0 ) {
						if ( dynamic.autoCompleteHint && offset === inputs[ inputIndex ].length ) {
							clearHint() ;
						}

						cutOffset = offset -- ;

						while ( offset > 0 && inputs[ inputIndex ][ offset ] === ' ' ) { offset -- ; }
						while ( offset > 0 && inputs[ inputIndex ][ offset - 1 ] !== ' ' ) { offset -- ; }

						inputs[ inputIndex ].splice( offset , cutOffset - offset ) ;

						if ( echo ) {
							computeAllCoordinate() ;
							this.moveTo( cursor.x , cursor.y ) ;
							redraw( undefined , true ) ;
						}
					}
					break ;

				case 'deleteNextWord' :
					if ( inputs[ inputIndex ].length && offset < inputs[ inputIndex ].length ) {
						cutOffset = offset ;

						while ( offset < inputs[ inputIndex ].length && inputs[ inputIndex ][ offset ] === ' ' ) { offset ++ ; }
						while ( offset < inputs[ inputIndex ].length && inputs[ inputIndex ][ offset ] !== ' ' ) { offset ++ ; }
						while ( offset < inputs[ inputIndex ].length && inputs[ inputIndex ][ offset ] === ' ' ) { offset ++ ; }

						inputs[ inputIndex ].splice( cutOffset , offset - cutOffset ) ;
						offset = Math.min( inputs[ inputIndex ].length , cutOffset ) ;

						if ( echo ) {
							computeAllCoordinate() ;
							this.moveTo( cursor.x , cursor.y ) ;
							redraw( undefined , true ) ;
						}

						if ( dynamic.autoCompleteHint && offset === inputs[ inputIndex ].length ) {
							autoCompleteHint() ;
						}
					}
					break ;

				case 'previousWord' :
					if ( inputs[ inputIndex ].length && offset > 0 ) {
						if ( dynamic.autoCompleteHint && offset === inputs[ inputIndex ].length ) {
							clearHint() ;
						}

						offset -- ;

						while ( offset > 0 && inputs[ inputIndex ][ offset ] === ' ' ) { offset -- ; }
						while ( offset > 0 && inputs[ inputIndex ][ offset - 1 ] !== ' ' ) { offset -- ; }

						if ( echo ) {
							computeAllCoordinate() ;
							this.moveTo( cursor.x , cursor.y ) ;
						}
					}
					break ;

				case 'nextWord' :
					if ( inputs[ inputIndex ].length && offset < inputs[ inputIndex ].length ) {
						while ( offset < inputs[ inputIndex ].length && inputs[ inputIndex ][ offset ] === ' ' ) { offset ++ ; }
						while ( offset < inputs[ inputIndex ].length && inputs[ inputIndex ][ offset ] !== ' ' ) { offset ++ ; }

						if ( echo ) {
							computeAllCoordinate() ;
							this.moveTo( cursor.x , cursor.y ) ;
						}

						if ( dynamic.autoCompleteHint && offset === inputs[ inputIndex ].length ) {
							autoCompleteHint() ;
						}
					}

					break ;

				case 'startOfInput' :
					if ( dynamic.autoCompleteHint && offset === inputs[ inputIndex ].length ) {
						clearHint() ;
					}

					offset = 0 ;

					if ( echo ) {
						computeAllCoordinate() ;
						this.moveTo( cursor.x , cursor.y ) ;
					}
					break ;

				case 'endOfInput' :
					offset = inputs[ inputIndex ].length ;

					if ( echo ) {
						computeAllCoordinate() ;
						this.moveTo( cursor.x , cursor.y ) ;
					}

					if ( dynamic.autoCompleteHint && lastOffset !== inputs[ inputIndex ].length ) {
						autoCompleteHint() ;
					}

					break ;

				case 'historyNext' :
					if ( inputIndex < inputs.length - 1 ) {
						inputIndex ++ ;
						offset = inputs[ inputIndex ].length ;

						if ( echo ) {
							extraLines = end.y - start.y ;
							computeAllCoordinate() ;
							extraLines -= end.y - start.y ;
							redraw( extraLines , true ) ;
							this.moveTo( cursor.x , cursor.y ) ;
						}

						// Not sure if this is desirable
						//if ( dynamic.autoCompleteHint ) { autoCompleteHint() ; }
					}
					break ;

				case 'historyPrevious' :
					if ( inputIndex > 0 ) {
						inputIndex -- ;
						offset = inputs[ inputIndex ].length ;

						if ( echo ) {
							extraLines = end.y - start.y ;
							computeAllCoordinate() ;
							extraLines -= end.y - start.y ;
							redraw( extraLines , true ) ;
							this.moveTo( cursor.x , cursor.y ) ;
						}

						// Not sure if this is desirable
						//if ( dynamic.autoCompleteHint ) { autoCompleteHint() ; }
					}
					break ;

				case 'autoCompleteUsingHistory' :
				case 'autoComplete' :
					autoCompleteUsed = keyBindings[ key ] === 'autoCompleteUsingHistory' ? options.history : dynamic.autoComplete ;

					if ( ! autoCompleteUsed ) { break ; }

					leftPart = inputs[ inputIndex ].slice( 0 , offset ) ;

					var finishCompletion = () => {
						if ( Array.isArray( autoCompleted ) ) {
							if ( dynamic.autoCompleteMenu ) { autoCompleteMenu( autoCompleted ) ; }
							return ;
						}

						leftPart = string.unicode.toArray( autoCompleted ).slice( 0 , options.maxLength ) ;

						inputs[ inputIndex ] = leftPart.concat(
							inputs[ inputIndex ].slice( offset , options.maxLength + offset - leftPart.length )
						) ;

						offset = leftPart.length ;

						if ( echo ) {
							computeAllCoordinate() ;
							redraw() ;
						}
					} ;

					if ( Array.isArray( autoCompleteUsed ) ) {
						autoCompleted = autoComplete( autoCompleteUsed , leftPart.join( '' ) , dynamic.autoCompleteMenu ) ;
					}
					else if ( typeof autoCompleteUsed === 'function' ) {
						if ( autoCompleteUsed.length === 2 ) {
							autoCompleteUsed( leftPart.join( '' ) , ( error , autoCompleted_ ) => {
								if ( error ) { cleanup( error ) ; return ; }

								autoCompleted = autoCompleted_ ;
								finishCompletion() ;
							} ) ;
							return ;
						}

						autoCompleted = autoCompleteUsed( leftPart.join( '' ) ) ;

						if ( Promise.isThenable( autoCompleted ) ) {
							autoCompleted.then(
								autoCompleted_ => {
									autoCompleted = autoCompleted_ ;
									finishCompletion() ;
								} ,
								error => { cleanup( error ) ; }
							) ;
							return ;
						}
					}

					finishCompletion() ;

					break ;
			}
		}
	} ;


	// Return a controller for the input field

	controller = Object.create( NextGenEvents.prototype ) ;

	controller.defineStates( 'ready' ) ;

	// /!\ .ready is deprecated, it is now a getter to .hasState('ready')
	Object.defineProperty( controller , 'ready' , {
		get: function() { return this.hasState( 'ready' ) ; }
	} ) ;

	// Tmp, for compatibility
	controller.widgetType = 'inputField' ;

	// Stop everything and do not even call the callback
	controller.abort = () => {
		if ( finished ) { return ; }
		cleanup( 'abort' ) ;
	} ;

	// Stop and call the completion callback with the current input
	controller.stop = () => {
		if ( finished ) { return ; }
		cleanup( undefined , inputs[ inputIndex ] ) ;
	} ;

	// Pause and resume: the input field will not respond to event when paused
	controller.pause = pause ;
	controller.resume = resume ;
	controller.focus = ( value ) => {
		if ( value ) { resume() ; }
		else { pause() ; }
	} ;

	// Get the current input
	controller.getInput = () => inputs[ inputIndex ].join( '' ) ;

	controller.value = controller.getInput ;

	// Get the current position
	controller.getPosition = () => ( { x: start.x , y: start.y } ) ;

	// Hide the input field
	controller.hide = () => {
		if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , controller.hide ) ; return ; }

		var i , j ;

		for ( i = start.x , j = start.y ; j <= end.y ; i = 1 , j ++ ) {
			this.moveTo.eraseLineAfter( i , j ) ;
		}

		echo = false ;
	} ;

	// Show the input field
	controller.show = () => {
		if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , controller.show ) ; return ; }
		echo = true ;
		redraw() ;
	} ;

	// Redraw the input field
	controller.redraw = () => {
		if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , controller.redraw ) ; return ; }
		redraw( undefined , true ) ;
	} ;

	// Redraw the cursor
	controller.redrawCursor = () => {
		if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , controller.redrawCursor ) ; return ; }
		redrawCursor() ;
	} ;

	controller.getCursorPosition = () => offset ;

	controller.setCursorPosition = newOffset => {
		newOffset = boundOffset( newOffset ) ;

		if ( newOffset !== offset ) {
			if ( dynamic.autoCompleteHint && offset === inputs[ inputIndex ].length ) {
				clearHint() ;
			}

			offset = newOffset ;

			if ( echo ) {
				computeAllCoordinate() ;
				this.moveTo( cursor.x , cursor.y ) ;
			}

			if ( dynamic.autoCompleteHint && offset === inputs[ inputIndex ].length ) {
				autoCompleteHint() ;
			}
		}
	} ;

	// Rebase the input field where the cursor is
	controller.rebase = ( x , y ) => {
		if ( ! controller.hasState( 'ready' ) ) { controller.once( 'ready' , controller.rebase ) ; return ; }

		var rebaseTo = ( x_ , y_ ) => {

			start.x = x_ ;
			start.y = y_ ;

			if ( options.echo ) {
				echo = true ;
				computeAllCoordinate() ;
				redraw() ;
			}

			controller.emit( 'rebased' ) ;
		} ;

		if ( x !== undefined && y !== undefined ) {
			rebaseTo( x , y ) ;
			return ;
		}

		// First, disable echoing: getCursorLocation is async!
		echo = false ;

		this.getCursorLocation( ( error , x_ , y_ ) => {
			if ( error ) {
				// Some bad terminals (windows...) doesn't support cursor location request, we should fallback to a decent behavior.
				// Here we just ignore the rebase.
				//cleanup( error ) ;
				return ;
			}

			rebaseTo( x_ , y_ ) ;
		} ) ;
	} ;

	controller.promise = new Promise() ;


	// Init the input field
	init() ;

	return controller ;
} ;

