import type { ValidationState } from '../../common/static-wrappers/interfaces/validation-interface';
import { markRaw } from 'vue';
import { Prop, toNative, Watch } from 'vue-facing-decorator';
import TsxComponent, { Component } from '../../app/vuetsx';
import { isNullOrEmpty } from '../../common/utils/is-null-or-empty';
import LoadingIndicator from '../loading-indicator';

const MONACO_CDN = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.20.0/min';

let monacoLoadPromise: Promise<void> | null = null;

const loadMonaco = (): Promise<void> => {
	if ((window as any).monaco) {
		return Promise.resolve();
	}

	if (monacoLoadPromise) {
		return monacoLoadPromise;
	}

	monacoLoadPromise = new Promise<void>((resolve, reject) => {
		const loaderScript = document.createElement('script');
		loaderScript.src = `${MONACO_CDN}/vs/loader.js`;
		loaderScript.onload = () => {
			const require = (window as any).require;
			require.config({ paths: { vs: `${MONACO_CDN}/vs` } });
			require(
				['vs/editor/editor.main'],
				() => {
					resolve();
				},
				(err: any) => {
					reject(err);
				},
			);
		};
		loaderScript.onerror = () => {
			monacoLoadPromise = null;
			reject(new Error('Failed to load Monaco Editor'));
		};
		document.head.appendChild(loaderScript);
	});

	return monacoLoadPromise;
};

interface MonacoEditorArgs {
	value?: string;
	changed?: (v: string) => void;
	language?: string;
	height?: string;
	/** Invoked when the Monaco CDN load fails — callers can fall back to a plain editor. */
	loadFailed?: () => void;
	/** Extra monaco editor options merged into the defaults (e.g. minimap, wordWrap). */
	options?: Record<string, unknown>;
	/** Validation state of the bound value (the base TsxComponent prop — declared here so TSX callers can pass it). */
	validationState?: ValidationState;
}

@Component
export class MonacoEditorComponent extends TsxComponent<MonacoEditorArgs> implements MonacoEditorArgs {
	@Prop() value: string;
	@Prop() language: string;
	@Prop() height!: string;
	@Prop() changed: (newValue: string) => void;
	@Prop() loadFailed: () => void;
	@Prop() options: Record<string, unknown>;
	editor: any;
	monacoLoading: boolean = false;

	raiseChangeEvent() {
		this.populateValidationDeclaration();

		if (this.changed != null) {
			// NEVER assign `this.value` here: `value` is a prop and the readonly
			// proxy trap THROWS in Vue 3, which would kill the relay before
			// `changed` fires. The parent feeds the new value back via the prop.
			this.changed(this._getValue());
		}
	}

	mounted() {
		if ((window as any).monaco) {
			this.initMonaco();
		} else {
			this.monacoLoading = true;

			loadMonaco().then(() => {
				this.monacoLoading = false;
				this.initMonaco();
			}).catch(() => {
				// CDN unreachable / loader error: stop the spinner and let the
				// caller degrade (e.g. swap in a plain textarea).
				this.monacoLoading = false;

				if (this.loadFailed != null) {
					this.loadFailed();
				}
			});
		}
	}

	getOptions() {
		return { ...(this.options || {}) };
	}

	initMonaco() {
		// markRaw is MANDATORY: `editor` is a reactive class field, so without it
		// Vue wraps the monaco instance (and every object reached through it) in
		// reactive proxies. Monaco's piece-tree walks compare node identity
		// against sentinels — proxied nodes never match, so the first
		// `getValue()` inside a change event spins forever and freezes the tab.
		this.editor = markRaw((window as any).monaco.editor.create((this.$el as HTMLElement), {
			value: this.value,
			language: this.language,
			theme: 'vs',
			...this.getOptions(),
		}));

		this._editorMounted(this.editor);
	};

	refreshLayout() {
		this.editor && this.$nextTick(() => {
			this.editor.layout();
		});
	}

	_getEditor() {
		if (!this.editor) {
			return null;
		}

		return this.editor;
	};

	_setValue(value) {
		const editor = this._getEditor();
		if (editor) {
			return editor.setValue(value);
		}
	};

	_getValue() {
		const editor = this._getEditor();
		if (!editor) {
			return '';
		}

		return editor.getValue();
	};

	_editorMounted(editor) {
		editor.onDidChangeModelContent((event) => {
			this.raiseChangeEvent();
		});
	};

	@Watch('value')
	onValueChanged(val: string, oldVal: string) {
		if (val != oldVal) {
			const currentVal = this._getValue();
			if (currentVal != val) {
				this._setValue(val);
			}
		}
	}

	render(h) {
		return (
			<div class="monaco_editor_container" style={`position:relative;width:100%; height: ${!isNullOrEmpty(this.height) ? (this.height) : '300px'};`}>
				{this.monacoLoading && <LoadingIndicator visible={this.monacoLoading} />}
			</div>
		);
	}
}

const MonacoEditor = toNative(MonacoEditorComponent);
export default MonacoEditor;
