( () => {

	/**
	 * Debug console
	 * Based on JavaScript Shell 1.4 by Jesse Ruderman (GPL/LGPL/MPL tri-license)
	 *
	 * TODO:
	 *    * Refactor, more jQuery, etc.
	 *    * Spinner?
	 *    * A prompt in front of input lines and the textarea
	 *    * Collapsible backtrace display
	 */

	const histList = [ '' ];
	let histPos = 0,
		question,
		input,
		output,
		$spinner,
		sessionContent = null,
		sessionKey = null,
		pending = false,
		clearNextRequest = false;

	function refocus() {
		// Needed for Mozilla to scroll correctly
		input.blur();
		input.focus();
	}

	function initConsole() {
		input = document.getElementById( 'mw-scribunto-input' );
		output = document.getElementById( 'mw-scribunto-output' );
		$spinner = $.createSpinner( { size: 'small', type: 'block' } );

		recalculateInputHeight();
		println( mw.msg( 'scribunto-console-intro' ), 'mw-scribunto-message' );
	}

	/**
	 * Use onkeydown because IE doesn't support onkeypress for arrow keys
	 *
	 * @param {jQuery.Event} e
	 */
	function inputKeydown( e ) {
		if ( e.shiftKey && e.keyCode === 13 ) {
			// shift-enter
			// don't do anything; allow the shift-enter to insert a line break as normal
		} else if ( e.keyCode === 13 ) {
			// enter
			// execute the input on enter
			go();
		} else if ( e.keyCode === 38 ) {
			// up
			// go up in history if at top or ctrl-up
			if ( e.ctrlKey || caretInFirstLine( input ) ) {
				hist( 'up' );
			}
		} else if ( e.keyCode === 40 ) {
			// down
			// go down in history if at end or ctrl-down
			if ( e.ctrlKey || caretInLastLine( input ) ) {
				hist( 'down' );
			}
		}

		setTimeout( recalculateInputHeight, 0 );
	}

	function inputFocus() {
		if ( sessionContent === null ) {
			// No previous state to clear
			return;
		}

		if ( clearNextRequest ) {
			// User already knows
			return;
		}

		if ( getContent() !== sessionContent ) {
			printClearBar( 'scribunto-console-cleared' );
			clearNextRequest = true;
		}
	}

	function caretInFirstLine( textbox ) {
		// IE doesn't support selectionStart/selectionEnd
		if ( textbox.selectionStart === undefined ) {
			return true;
		}

		const firstLineBreak = textbox.value.indexOf( '\n' );

		return ( ( firstLineBreak === -1 ) || ( textbox.selectionStart <= firstLineBreak ) );
	}

	function caretInLastLine( textbox ) {
		// IE doesn't support selectionStart/selectionEnd
		if ( textbox.selectionEnd === undefined ) {
			return true;
		}

		const lastLineBreak = textbox.value.lastIndexOf( '\n' );

		return ( textbox.selectionEnd > lastLineBreak );
	}

	function recalculateInputHeight() {
		const rows = input.value.split( /\n/ ).length +
			// prevent scrollbar flickering in Mozilla
			1 +
			// leave room for scrollbar in Opera
			( window.opera ? 1 : 0 );

		// without this check, it is impossible to select text in Opera 7.60 or Opera 8.0.
		if ( input.rows !== rows ) {
			input.rows = rows;
		}
	}

	function println( s, type ) {
		if ( ( s = String( s ) ) ) {
			const newdiv = document.createElement( 'div' );
			newdiv.appendChild( document.createTextNode( s ) );
			newdiv.className = type;
			output.appendChild( newdiv );
			return newdiv;
		}
	}

	function printClearBar( msg ) {
		$( '<div>' )
			.attr( 'class', 'mw-scribunto-clear' )
			.text( mw.msg( msg ) )
			.appendTo( output );
	}

	function hist( direction ) {
		// histList[0] = first command entered, [1] = second, etc.
		// type something, press up --> thing typed is now in "limbo"
		// (last item in histList) and should be reachable by pressing
		// down again.

		const L = histList.length;

		if ( L === 1 ) {
			return;
		}

		if ( direction === 'up' ) {
			if ( histPos === L - 1 ) {
				// Save this entry in case the user hits the down key.
				histList[ histPos ] = input.value;
			}

			if ( histPos > 0 ) {
				histPos--;
				// Use a timeout to prevent up from moving cursor within new text
				// Set to nothing first for the same reason
				setTimeout(
					() => {
						input.value = '';
						input.value = histList[ histPos ];
						const caretPos = input.value.length;
						if ( input.setSelectionRange ) {
							input.setSelectionRange( caretPos, caretPos );
						}
					},
					0
				);
			}
		} else {
			// direction down
			if ( histPos < L - 1 ) {
				histPos++;
				input.value = histList[ histPos ];
			} else if ( histPos === L - 1 ) {
				// Already on the current entry: clear but save
				if ( input.value ) {
					histList[ histPos ] = input.value;
					++histPos;
					input.value = '';
				}
			}
		}
	}

	function printQuestion( q ) {
		println( q, 'mw-scribunto-input' );
	}

	function printError( er ) {
		if ( er.name ) {
			// lineNumberString should not be '', to avoid a very wacky bug in IE 6.
			const lineNumberString = ( er.lineNumber !== undefined ) ? ( ' on line ' + er.lineNumber + ': ' ) : ': ';
			// Because IE doesn't have error.toString.
			println( er.name + lineNumberString + er.message, 'mw-scribunto-error' );
		} else {
			// Because security errors in Moz /only/ have toString.
			println( er, 'mw-scribunto-error' );
		}
	}

	function setPending() {
		pending = true;
		input.readOnly = true;
		$spinner.insertBefore( input );
	}

	function clearPending() {
		$spinner.remove();
		pending = false;
		input.readOnly = false;
	}

	function go() {
		if ( pending ) {
			// If there is an XHR request pending, don't send another one
			// We set readOnly on the textarea to give a UI indication, this is
			// just for paranoia.
			return;
		}

		question = input.value;

		if ( question === '' ) {
			return;
		}

		histList[ histList.length - 1 ] = question;
		histList[ histList.length ] = '';
		histPos = histList.length - 1;

		// Unfortunately, this has to happen *before* the script is run, so that
		// print() output will go in the right place.
		input.value = '';
		// can't preventDefault on input, so also clear it later
		setTimeout( () => {
			input.value = '';
		}, 0 );

		recalculateInputHeight();
		printQuestion( question );

		const params = {
			action: 'scribunto-console',
			title: mw.config.get( 'wgPageName' ),
			question: question
		};

		const content = getContent();
		let sentContent = false;

		if ( !sessionKey || sessionContent !== content ) {
			params.clear = true;
			params.content = content;
			sentContent = true;
		}
		if ( sessionKey ) {
			params.session = sessionKey;
		}
		if ( clearNextRequest ) {
			params.clear = true;
			clearNextRequest = false;
		}

		const api = new mw.Api();
		setPending();

		api.postWithToken( 'csrf', params )
			.done( ( result ) => {
				if ( result.sessionIsNew === '' && !sentContent ) {
					// Session was lost. Resend query, with content
					printClearBar( 'scribunto-console-cleared-session-lost' );
					sessionContent = null;
					clearPending();
					input.value = params.question;
					go();
					return;
				}
				sessionKey = result.session;
				sessionContent = content;
				if ( result.type === 'error' ) {
					$( '<div>' ).addClass( 'mw-scribunto-error' ).html( result.html ).appendTo( output );
				} else {
					if ( result.print !== '' ) {
						println( result.print, 'mw-scribunto-print' );
					}
					if ( result.return !== '' ) {
						println( result.return, 'mw-scribunto-normalOutput' );
					}
				}
				clearPending();
				setTimeout( refocus, 0 );
			} )
			.fail( ( code, result ) => {
				if ( result.error && result.error.info ) {
					printError( result.error.info );
				} else if ( result.exception ) {
					printError( 'Error sending API request: ' + result.exception );
				} else {
					mw.log( result );
					printError( 'error' );
				}
				clearPending();
				setTimeout( refocus, 0 );
			} );
	}

	function getContent() {
		const $textarea = $( '#wpTextbox1' ),
			context = $textarea.data( 'wikiEditor-context' );

		if ( context === undefined || context.codeEditor === undefined ) {
			return $textarea.val();
		} else {
			return $textarea.textSelection( 'getContents' );
		}
	}

	function onClearClick() {
		$( '#mw-scribunto-output' ).empty();
		clearNextRequest = true;
		refocus();
	}

	function initEditPage() {
		let $console = $( '#mw-scribunto-console' );
		if ( !$console.length ) {
			// There is no console in the DOM; on read-only (protected) pages,
			// we need to add it here, because the hook does not insert
			// it server-side.
			const $wpTextbox1 = $( '#wpTextbox1' );
			if ( !$wpTextbox1.length || !$wpTextbox1.prop( 'readonly' ) ) {
				return;
			}

			$console = $( '<div>' ).attr( { id: 'mw-scribunto-console' } );
			$wpTextbox1.after( $console );
		}

		$( '<fieldset>' )
			.attr( 'class', 'mw-scribunto-console-fieldset' )
			.append( $( '<legend>' ).text( mw.msg( 'scribunto-console-title' ) ) )
			.append( $( '<div id="mw-scribunto-output"></div>' ) )
			.append(
				$( '<div>' ).append(
					$( '<textarea>' )
						.attr( {
							id: 'mw-scribunto-input',
							class: 'mw-scribunto-input',
							wrap: 'off',
							rows: 1,
							dir: 'ltr',
							lang: 'en'
						} )
						.on( 'keydown', inputKeydown )
						.on( 'focus', inputFocus )
				)
			)
			.append(
				$( '<div>' ).append(
					$( '<input>' )
						.attr( {
							type: 'button',
							value: mw.msg( 'scribunto-console-clear' )
						} )
						.on( 'click', onClearClick )
				)
			)
			.wrap( '<form>' )
			.appendTo( $console );

		initConsole();
	}

	$( () => {
		const action = mw.config.get( 'wgAction' );
		if ( action === 'edit' || action === 'submit' || action === 'editredlink' ) {
			initEditPage();
		}
	} );

} )();
