/**
 * WordPress dependencies
 */
import { useCallback, useEffect, useState } from '@safe-wordpress/element';

/**
 * External dependencies
 */
import { langs } from '@uiw/codemirror-extensions-langs';
import { color } from '@uiw/codemirror-extensions-color';
import { lintGutter, linter } from '@codemirror/lint';
import { syntaxTree } from '@codemirror/language';
import CodeMirror, { EditorView, ViewUpdate } from '@uiw/react-codemirror';
import { CSSLint } from 'csslint';
import { isEqual } from 'lodash';
import { isDefined } from '@nab/utils';
import type { Diagnostic } from '@codemirror/lint';
import type { SyntaxNode } from '@lezer/common';
import type { CSSProperties } from 'react';
import type { Maybe } from '@nab/types';

/**
 * Internal dependencies
 */
import type { Props } from './props';

const CSS_LINTER_RULES: Record< string, boolean > = {
	...CSSLint.getRules()
		.map( ( r ) => r.id )
		.reduce( ( acc, id ) => ( { ...acc, [ id ]: true } ), {} ),
	'overqualified-elements': false,
	'qualified-headings': false,
};

export const CssEditor = ( {
	value,
	className,
	before,
	after,
	placeholder,
	focusedSelector,
	readOnly,
	onBlur,
	onChange,
	onCreateEditor,
	onChangeSelectedRule,
	onFocus,
}: Props & {
	readonly onChangeSelectedRule?: (
		selectors?: ReadonlyArray< string >
	) => void;
	readonly focusedSelector?: string;
} ): JSX.Element => {
	const [ activeSelectors, setActiveSelectors ] = useState<
		Maybe< Array< string > >
	>( [] );
	const [ editorView, setEditorView ] = useState< EditorView | null >( null );

	useEffect( () => {
		if ( ! editorView || ! focusedSelector ) {
			return;
		}

		if ( editorView.hasFocus ) {
			return;
		}

		const anchor = findSelectorInEditor( editorView, focusedSelector );
		if ( undefined === anchor ) {
			return;
		}

		editorView.focus();
		editorView.dispatch( {
			selection: { anchor },
			effects: EditorView.scrollIntoView( anchor, { y: 'center' } ),
		} );
	}, [ editorView, focusedSelector ] );

	function cssLinter() {
		return linter( ( view ): Diagnostic[] => {
			const text = view.state.doc.toString();
			const results = CSSLint.verify( text, CSS_LINTER_RULES );
			const diagnostics: Diagnostic[] = [];

			for ( const message of results.messages ) {
				if ( message.rule?.id === 'ids' ) {
					continue;
				}

				if ( message.line && message.col ) {
					diagnostics.push( {
						from:
							view.state.doc.line( message.line ).from +
							( message.col - 1 ),
						to:
							view.state.doc.line( message.line ).from +
							message.col,
						severity:
							message.type === 'error' ? 'error' : 'warning',
						message: message.message,
					} );
				}
			}

			return diagnostics;
		} );
	}

	const handleRuleSelected = useCallback(
		( viewUpdate: ViewUpdate ) => {
			if ( ! onChangeSelectedRule ) {
				return;
			}

			if ( viewUpdate.transactions.some( ( tr ) => tr.docChanged ) ) {
				return;
			}

			const state = viewUpdate.state;
			const cursorPos = state.selection.main.head;
			const tree = syntaxTree( state );

			let node: SyntaxNode | null = tree.resolve( cursorPos, -1 );

			// Find the parent RuleSet node
			while ( node && node.name !== 'RuleSet' ) {
				node = node.parent;
			}

			if ( ! node ) {
				return;
			}

			const text = state.doc.sliceString( node.from, node.to );
			if ( ! text.includes( '{' ) ) {
				return;
			}

			const selectors = text
				.split( '{' )[ 0 ]
				?.trim()
				.split( ',' )
				.map( ( s ) => s.trim() )
				.filter( ( s ) => !! s.length );

			if ( ! isEqual( selectors, activeSelectors ) ) {
				onChangeSelectedRule( selectors );
				setActiveSelectors( selectors );
			}
		},
		[ activeSelectors, onChangeSelectedRule ]
	);

	return (
		<CodeMirror
			className={ className }
			value={ value }
			placeholder={ placeholder }
			indentWithTab={ true }
			{ ...{ indentunit: '  ' } }
			readOnly={ readOnly }
			editable={ ! readOnly }
			theme="light"
			onCreateEditor={ ( view, ...args ) => {
				setEditorView( view );
				onCreateEditor?.( view, ...args );
			} }
			basicSetup={ {
				lineNumbers: false,
				foldGutter: false,
				highlightActiveLine: ! readOnly,
			} }
			extensions={ [
				EditorView.lineWrapping,
				color,
				langs.css?.(),
				...[ readOnly ? [] : [ cssLinter(), lintGutter() ] ],
			].filter( isDefined ) }
			onChange={ readOnly ? undefined : onChange }
			onFocus={ onFocus }
			onBlur={ onBlur }
			onUpdate={ handleRuleSelected }
			style={
				{
					'--nab-code-before': JSON.stringify( before ),
					'--nab-code-after': JSON.stringify( after ),
				} as unknown as CSSProperties
			}
		/>
	);
};

// =======
// HELPERS
// =======

function findSelectorInEditor(
	view: EditorView,
	selector: string
): Maybe< number > {
	const selectors = selector
		.split( ',' )
		.map( ( s ) => s.trim() )
		.filter( ( s ) => !! s );

	let found = -1;
	syntaxTree( view.state ).iterate( {
		enter( node ) {
			if ( node.name !== 'RuleSet' ) {
				return;
			}

			const foundSelectors =
				view.state.doc
					.sliceString( node.from, node.to )
					.split( '{' )[ 0 ]
					?.trim()
					.split( ',' )
					.map( ( s ) => s.trim() )
					.filter( ( s ) => !! s ) ?? [];

			if (
				foundSelectors.length !== selectors.length ||
				! selectors.every( ( s ) => foundSelectors.includes( s ) )
			) {
				return;
			}

			found = node.from;
		},
	} );

	if ( found === -1 ) {
		return undefined;
	}

	const docText = view.state.doc.toString();
	const openBrace = docText.indexOf( '{', found );
	if ( openBrace === -1 ) {
		return undefined;
	}

	return openBrace + 1;
}
