const { syntaxTree, Decoration } = require( 'ext.CodeMirror.lib' );
const {
	localCompletionSource,
	javascript,
	javascriptLanguage,
	scopeCompletionSource
} = require( '../lib/codemirror.bundle.modes.js' );
const CodeMirrorMode = require( './codemirror.mode.js' );
const { doctag, markDocTagType, getViewPlugin } = require( './codemirror.doctag.js' );
const CodeMirrorWorker = require( '../workers/codemirror.worker.js' );
const CodeMirrorValidator = require( '../codemirror.validator.js' );

// Extracted from globals/globals.json (NPM)
const builtin = new Set( [
	'Array',
	'ArrayBuffer',
	'Atomics',
	'BigInt',
	'BigInt64Array',
	'BigUint64Array',
	'Boolean',
	'constructor',
	'DataView',
	'Date',
	'decodeURI',
	'decodeURIComponent',
	'encodeURI',
	'encodeURIComponent',
	'Error',
	'escape',
	'eval',
	'EvalError',
	'Float32Array',
	'Float64Array',
	'Function',
	'globalThis',
	'hasOwnProperty',
	'Infinity',
	'Int16Array',
	'Int32Array',
	'Int8Array',
	'isFinite',
	'isNaN',
	'isPrototypeOf',
	'JSON',
	'Map',
	'Math',
	'NaN',
	'Number',
	'Object',
	'parseFloat',
	'parseInt',
	'Promise',
	'propertyIsEnumerable',
	'Proxy',
	'RangeError',
	'ReferenceError',
	'Reflect',
	'RegExp',
	'Set',
	'SharedArrayBuffer',
	'String',
	'Symbol',
	'SyntaxError',
	'toLocaleString',
	'toString',
	'TypeError',
	'Uint16Array',
	'Uint32Array',
	'Uint8Array',
	'Uint8ClampedArray',
	'undefined',
	'unescape',
	'URIError',
	'valueOf',
	'WeakMap',
	'WeakSet'
] );

const globals = Decoration.mark( { class: 'cm-globals' } ),
	doctagName = Decoration.mark( { class: 'cm-doctag-var' } );

const markGlobals = ( tree, visibleRanges, state ) => {
	const decorations = [];
	for ( const { from, to } of visibleRanges ) {
		tree.iterate( {
			from,
			to,
			enter: ( { type, from: f, to: t } ) => {
				const name = state.sliceDoc( f, t );
				if ( type.is( 'VariableName' ) && builtin.has( name ) ) {
					// Exclude shadowed globals
					// However, this may be slow for large files with complex scopes
					const completions = localCompletionSource( { state, pos: t, explicit: true } );
					if (
						!completions ||
						!completions.options.some( ( { label } ) => label === name )
					) {
						decorations.push( globals.range( f, t ) );
					}
				} else if ( type.is( 'BlockComment' ) && /^\/\*{2}(?!\*)/.test( name ) ) {
					// JSDoc annotations
					const comment = name.slice( 2 ),
						pos = f + 2,
						re = /(^[ \t]*\*\s*)(@[a-z]+)(\s+\{)?|\{(@[a-z]+)/gim;
					let mt = re.exec( comment );
					while ( mt ) {
						if ( mt[ 4 ] ) {
							// Inline tag, e.g. {@link}
							decorations.push(
								doctag.range( pos + mt.index + 1, pos + mt.index + mt[ 0 ].length )
							);
						} else {
							const index = markDocTagType( decorations, pos, mt ),
								m = /(^\s+)([a-z_]\w*)\s+-/i.exec( comment.slice( index ) );
							if ( m ) {
								// JSDoc name annotation, e.g. @param {string} name - description
								const start = pos + index + m[ 1 ].length,
									end = start + m[ 2 ].length;
								decorations.push( doctagName.range( start, end ) );
							}
						}
						mt = re.exec( comment );
					}
				}
			}
		} );
	}
	return Decoration.set( decorations );
};

/**
 * JavaScript language support for CodeMirror.
 *
 * @example
 * const require = await mw.loader.using( [ 'ext.CodeMirror', 'ext.CodeMirror.modes' ] );
 * const CodeMirror = require( 'ext.CodeMirror' );
 * const { javascript } = require( 'ext.CodeMirror.modes' );
 * const cm = new CodeMirror( myTextarea, javascript() );
 * cm.initialize();
 * @extends CodeMirrorMode
 * @hideconstructor
 */
class CodeMirrorJavaScript extends CodeMirrorMode {

	/**
	 * @param {string} name
	 * @internal
	 * @hideconstructor
	 */
	constructor( name ) {
		super( name );

		/**
		 * The API-powered validator.
		 *
		 * @type {CodeMirrorValidator}
		 */
		this.validator = new CodeMirrorValidator( 'javascript' );
	}

	/** @inheritDoc */
	get language() {
		return javascriptLanguage;
	}

	/** @inheritDoc */
	get lintSource() {
		return async ( view ) => {
			const data = await this.worker.lint( view );
			return data.map( ( {
				ruleId,
				message,
				severity,
				line,
				column,
				endLine,
				endColumn,
				fix,
				suggestions = []
			} ) => {
				const start = CodeMirrorWorker.pos( view, line, column );
				const diagnostic = {
					rule: ruleId,
					source: 'ESLint',
					message: message + ( ruleId ? ` (${ ruleId })` : '' ),
					severity: severity === 1 ? 'info' : 'error',
					from: start,
					to: endLine === undefined ?
						start + 1 :
						CodeMirrorWorker.pos( view, endLine, endColumn )
				};
				if ( fix || suggestions.length ) {
					diagnostic.actions = [
						...fix ? [ { name: 'fix', fix } ] : [],
						...suggestions.map( ( suggestion ) => ( {
							name: suggestion.messageId || 'suggestion',
							fix: suggestion.fix,
							tooltip: suggestion.desc
						} ) )
					].map( ( { name, fix: { range: [ from, to ], text }, tooltip } ) => ( {
						name,
						tooltip,
						apply( v ) {
							v.dispatch( { changes: { from, to, insert: text } } );
						}
					} ) );
				}
				return diagnostic;
			} );
		};
	}

	/** @inheritdoc */
	get lintApi() {
		return async ( { state: { doc } } ) => {
			const errors = await this.validator.execute( doc.toString() );
			return errors.map( ( { message, line, column } ) => {
				const from = doc.line( line ).from + column;
				return {
					severity: 'error',
					source: 'Peast',
					message,
					from,
					to: from
				};
			} );
		};
	}

	/** @inheritDoc */
	get bracketMatchingConfig() {
		return {
			exclude( state, pos ) {
				return syntaxTree( state ).resolveInner( pos, 0 ).name === 'RegExp';
			}
		};
	}

	/** @inheritDoc */
	get support() {
		return [
			this.docTagExtension,
			javascript().support,
			javascriptLanguage.data.of( { autocomplete: scopeCompletionSource( window ) } ),
			getViewPlugin( markGlobals )
		];
	}
}

module.exports = CodeMirrorJavaScript;