const {
	Decoration,
	DecorationSet,
	Direction,
	EditorView,
	PluginSpec,
	Prec,
	RangeSet,
	RangeSetBuilder,
	ViewPlugin,
	ViewUpdate,
	syntaxTree
} = require( 'ext.CodeMirror.v6.lib' );
const mwModeConfig = require( './codemirror.mediawiki.config.js' );

/**
 * @type {Decoration}
 * @private
 */
const isolate = Decoration.mark( {
	class: 'cm-bidi-isolate',
	bidiIsolate: Direction.LTR
} );

/**
 * @param {EditorView} view
 * @return {RangeSet}
 * @private
 */
function computeIsolates( view ) {
	const set = new RangeSetBuilder();

	if ( view.editorAttrs.dir === 'rtl' ) {
		for ( const { from, to } of view.visibleRanges ) {
			let startPos = null;
			syntaxTree( view.state ).iterate( {
				from,
				to,
				enter( node ) {
					// Determine if this is a bracket node (start or end of a tag).
					const isBracket = node.name.split( '_' )
						.some( ( tag ) => [
							mwModeConfig.tags.htmlTagBracket,
							mwModeConfig.tags.extTagBracket
						].includes( tag ) );

					if ( startPos === null && isBracket ) {
						// If we find a bracket node, we keep track of the start position.
						startPos = node.from;
					} else if ( isBracket ) {
						// When we find the closing bracket, add the isolate.
						set.add( startPos, node.to, isolate );
						startPos = null;
					}
				}
			} );
		}
	}

	return set.finish();
}

/**
 * @private
 */
class CodeMirrorBidiIsolation {
	/**
	 * @constructor
	 * @param {EditorView} view The editor view.
	 */
	constructor( view ) {
		/** @type {DecorationSet} */
		this.isolates = computeIsolates( view );
		/** @type {Tree} */
		this.tree = syntaxTree( view.state );
		/** @type {Direction} */
		this.dir = view.textDirection;
	}

	/**
	 * @param {ViewUpdate} update
	 */
	update( update ) {
		if ( update.docChanged || update.viewportChanged ||
			syntaxTree( update.state ) !== this.tree ||
			update.view.textDirection !== this.dir
		) {
			this.isolates = computeIsolates( update.view );
			this.tree = syntaxTree( update.state );
		}
	}
}

/**
 * @type {PluginSpec}
 * @private
 */
const bidiIsolationSpec = {
	provide: ( plugin ) => {
		/**
		 * @param {EditorView} view
		 * @return {DecorationSet}
		 */
		const access = ( view ) => view.plugin( plugin ) ?
			( view.plugin( plugin ).isolates || Decoration.none ) :
			Decoration.none;

		// Use the lowest precedence to ensure that other decorations
		// don't break up the isolating decorations.
		return Prec.lowest( [
			EditorView.decorations.of( access ),
			EditorView.bidiIsolatedRanges.of( access )
		] );
	}
};

/**
 * Bidirectional isolation plugin for CodeMirror for use on RTL pages.
 * This ensures HTML and MediaWiki tags are always displayed left-to-right.
 *
 * Use this plugin by passing in `bidiIsolation: true` when instantiating
 * a [CodeMirrorModeMediaWiki]{@link CodeMirrorModeMediaWiki} object.
 *
 * @module CodeMirrorBidiIsolation
 * @see https://codemirror.net/examples/bidi/
 */
module.exports = ViewPlugin.fromClass( CodeMirrorBidiIsolation, bidiIsolationSpec );