import { rangeToWholeLine } from "../helpers/range-to-whole-line.js";
import { parseJsonPointerPath } from "../helpers/json/json-pointer-path.js";
import { ensureJsonPointerLinkSupport } from "../helpers/json/json-pointer-links.js";
import { createJsonModel } from "../helpers/json/create-json-model.js";
import { computed, markRaw, ref, shallowRef, toValue, watch } from "vue";
import { deepClone } from "@scalar/workspace-store/helpers/deep-clone";
import * as monaco from "monaco-editor";
import { apply } from "@scalar/json-magic/diff";
//#region src/v2/features/editor/hooks/use-three-way-merge-editor.ts
/**
* Shared state and logic for a 3-way merge editor (base | local | remote → result).
* Create editors by calling init() with the three container elements (e.g. from refs in onMounted).
* Call dispose() in onUnmounted.
*
* @example
* ```vue
* <script setup lang="ts">
* const localRef = ref<HTMLDivElement>()
* const remoteRef = ref<HTMLDivElement>()
* const resultRef = ref<HTMLDivElement>()
*
* const mergeEditor = useThreeWayMergeEditor({
*   baseDocument,
*   resolvedDocument,
*   conflicts,
*   onApplyChanges: (doc) => emit('applyChanges', { resolvedDocument: doc }),
*   onError: (msg) => toast(msg, 'error'),
* })
*
* onMounted(() => {
*   const local = localRef.value
*   const remote = remoteRef.value
*   const result = resultRef.value
*   if (local && remote && result) {
*     mergeEditor.init({ local, remote, result })
*   }
* })
*
* onUnmounted(() => {
*   mergeEditor.dispose()
* })
* <\/script>
*
* <template>
*   <div ref="localRef" />
*   <div ref="remoteRef" />
*   <div ref="resultRef" />
*   <button @click="mergeEditor.goToNextConflict">Next conflict</button>
*   <button :disabled="mergeEditor.conflictsLeft > 0" @click="mergeEditor.applyResolvedConflicts">
*     Apply ({{ mergeEditor.conflictsLeft }} left)
*   </button>
* </template>
* ```
*/
function useThreeWayMergeEditor(options) {
	const { baseDocument, resolvedDocument, conflicts, onApplyChanges, onError } = options;
	const resolvedConflicts = ref([]);
	const conflictRangesState = shallowRef([]);
	const localConflictRangesState = shallowRef([]);
	const remoteConflictRangesState = shallowRef([]);
	const lastNavigatedConflictIndex = ref(-1);
	const localChangesEditor = shallowRef();
	const remoteChangesEditor = shallowRef();
	const resultEditor = shallowRef();
	let resultCodeLensProviderDisposable;
	let jsonPointerLinkSupportDispose;
	const codeLensCommandDisposables = [];
	/** Models created in init(); disposed in dispose() to avoid leaks when the sync conflict modal closes. */
	let modelsToDispose = [];
	const normalizeConflicts = computed(() => {
		return toValue(conflicts).map((conflict) => {
			const localChanges = conflict[0];
			const remoteChanges = conflict[1];
			let smallestPath = localChanges[0].path;
			for (const localChange of localChanges) if (localChange.path.length < smallestPath.length) smallestPath = localChange.path;
			for (const remoteChange of remoteChanges) if (remoteChange.path.length < smallestPath.length) smallestPath = remoteChange.path;
			return {
				local: {
					path: smallestPath,
					changes: localChanges,
					type: localChanges[0].type
				},
				remote: {
					path: smallestPath,
					changes: remoteChanges,
					type: remoteChanges[0].type
				}
			};
		});
	});
	const documentWithLocalChanges = computed(() => apply(deepClone(toValue(resolvedDocument)), toValue(conflicts).flatMap((it) => it[0])));
	const documentWithRemoteChanges = computed(() => apply(deepClone(toValue(resolvedDocument)), toValue(conflicts).flatMap((it) => it[1])));
	const conflictsLeft = computed(() => resolvedConflicts.value.filter((status) => status === "idle").length);
	watch(normalizeConflicts, (nextConflicts) => {
		resolvedConflicts.value = nextConflicts.map((_, index) => resolvedConflicts.value[index] ?? "idle");
	}, { immediate: true });
	const focusEditorRange = (diffEditor, range) => {
		if (!diffEditor || !range) return;
		const modifiedEditor = diffEditor.getModifiedEditor();
		const safeRange = new monaco.Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
		modifiedEditor.setSelection(safeRange);
		modifiedEditor.revealRangeNearTop(safeRange);
	};
	const goToNextConflict = () => {
		const unresolvedConflictIndexes = normalizeConflicts.value.map((_, index) => index).filter((index) => resolvedConflicts.value[index] === "idle");
		if (!unresolvedConflictIndexes.length) return;
		const nextConflictIndex = unresolvedConflictIndexes.find((index) => index > lastNavigatedConflictIndex.value) ?? unresolvedConflictIndexes[0];
		if (nextConflictIndex === void 0) return;
		if (!normalizeConflicts.value[nextConflictIndex]) return;
		const localRange = localConflictRangesState.value.find((cr) => cr.index === nextConflictIndex)?.range;
		const remoteRange = remoteConflictRangesState.value.find((cr) => cr.index === nextConflictIndex)?.range;
		const resultRange = conflictRangesState.value.find((cr) => cr.index === nextConflictIndex)?.range;
		focusEditorRange(localChangesEditor.value, localRange);
		focusEditorRange(remoteChangesEditor.value, remoteRange);
		focusEditorRange(resultEditor.value, resultRange);
		resultEditor.value?.getModifiedEditor().focus();
		lastNavigatedConflictIndex.value = nextConflictIndex;
	};
	const applyResolvedConflicts = () => {
		if (conflictsLeft.value > 0) {
			onError?.("You have conflicts left");
			return;
		}
		const modifiedResultModel = resultEditor.value?.getModel()?.modified;
		if (!modifiedResultModel) return;
		let nextResolvedDocument = null;
		try {
			nextResolvedDocument = JSON.parse(modifiedResultModel.getValue());
		} catch {
			onError?.("You have formatting errors");
			return;
		}
		onApplyChanges(nextResolvedDocument);
	};
	const init = (containers) => {
		const base = toValue(baseDocument);
		const resolved = toValue(resolvedDocument);
		const localDoc = documentWithLocalChanges.value;
		const remoteDoc = documentWithRemoteChanges.value;
		const originalModelLocal = monaco.editor.createModel(JSON.stringify(resolved, null, 2), "json");
		const modifiedModelLocal = createJsonModel(JSON.stringify(localDoc, null, 2));
		const originalModelRemote = monaco.editor.createModel(JSON.stringify(resolved, null, 2), "json");
		const modifiedModelRemote = createJsonModel(JSON.stringify(remoteDoc, null, 2));
		const originalResultModel = monaco.editor.createModel(JSON.stringify(base, null, 2), "json");
		const modifiedResultModel = createJsonModel(JSON.stringify(resolved, null, 2));
		modelsToDispose = [
			originalModelLocal,
			modifiedModelLocal.model,
			originalModelRemote,
			modifiedModelRemote.model,
			originalResultModel,
			modifiedResultModel.model
		];
		const localDiffEditor = monaco.editor.createDiffEditor(containers.local, {
			originalEditable: false,
			readOnly: true,
			automaticLayout: true,
			renderSideBySide: false
		});
		localDiffEditor.setModel({
			original: originalModelLocal,
			modified: modifiedModelLocal.model
		});
		localChangesEditor.value = localDiffEditor;
		const remoteDiffEditor = monaco.editor.createDiffEditor(containers.remote, {
			originalEditable: false,
			readOnly: true,
			automaticLayout: true,
			renderSideBySide: false
		});
		remoteDiffEditor.setModel({
			original: originalModelRemote,
			modified: modifiedModelRemote.model
		});
		remoteChangesEditor.value = remoteDiffEditor;
		const resultDiffEditor = monaco.editor.createDiffEditor(containers.result, {
			originalEditable: false,
			readOnly: false,
			renderSideBySide: false,
			automaticLayout: true
		});
		resultDiffEditor.setModel({
			original: originalResultModel,
			modified: modifiedResultModel.model
		});
		const resultModifiedEditor = resultDiffEditor.getModifiedEditor();
		resultModifiedEditor.updateOptions({ codeLens: true });
		const focusResultPath = async (path) => {
			const range = await modifiedResultModel.getRangeFromPath(path);
			if (!range) return;
			const safeRange = new monaco.Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
			resultModifiedEditor.setSelection(safeRange);
			resultModifiedEditor.revealRangeNearTop(safeRange);
		};
		const navigateJsonPointer = async (pointer) => {
			const path = parseJsonPointerPath(pointer);
			if (!path) return;
			await focusResultPath(path);
		};
		jsonPointerLinkSupportDispose = ensureJsonPointerLinkSupport(navigateJsonPointer).dispose;
		let resultHighlightDecorations = [];
		let suppressedChangeEvents = 0;
		const applyCurrentCodeLensCommandId = "scalar.conflict.applyCurrent";
		const applyRemoteCodeLensCommandId = "scalar.conflict.applyRemote";
		const ignoreCodeLensCommandId = "scalar.conflict.ignore";
		const noOpCodeLensCommandId = resultModifiedEditor.addCommand(0, () => void 0) ?? "scalar.conflict.status.noop";
		const getBoxDecorationsForRange = (range) => {
			const startLine = range.startLineNumber;
			const endLine = range.endLineNumber;
			if (startLine === endLine) return [{
				range,
				options: {
					isWholeLine: true,
					className: "json-focus-highlight-box-single"
				}
			}];
			const topRange = new monaco.Range(startLine, 1, startLine, range.endColumn);
			const middleRange = new monaco.Range(startLine + 1, 1, Math.max(startLine + 1, endLine - 1), 1);
			const bottomRange = new monaco.Range(endLine, 1, endLine, range.endColumn);
			const decorations = [{
				range: topRange,
				options: {
					isWholeLine: true,
					className: "json-focus-highlight-box-top"
				}
			}, {
				range: bottomRange,
				options: {
					isWholeLine: true,
					className: "json-focus-highlight-box-bottom"
				}
			}];
			if (endLine - startLine > 1) decorations.push({
				range: middleRange,
				options: {
					isWholeLine: true,
					className: "json-focus-highlight-box-middle"
				}
			});
			return decorations;
		};
		const refreshCodeLensProvider = () => {
			resultCodeLensProviderDisposable?.dispose();
			resultCodeLensProviderDisposable = monaco.languages.registerCodeLensProvider("json", { provideCodeLenses: (model) => {
				if (model.uri.toString() !== modifiedResultModel.model.uri.toString()) return {
					lenses: [],
					dispose: () => void 0
				};
				return {
					lenses: conflictRangesState.value.flatMap((conflictRange) => {
						const lensRange = new monaco.Range(conflictRange.range.startLineNumber, 1, conflictRange.range.startLineNumber, 1);
						return [
							{
								range: lensRange,
								command: {
									id: noOpCodeLensCommandId,
									title: `Status: ${resolvedConflicts.value[conflictRange.index] ?? "idle"}`
								}
							},
							{
								range: lensRange,
								command: {
									id: applyCurrentCodeLensCommandId,
									title: "Accept Current",
									arguments: [conflictRange.index]
								}
							},
							{
								range: lensRange,
								command: {
									id: applyRemoteCodeLensCommandId,
									title: "Accept Remote",
									arguments: [conflictRange.index]
								}
							},
							{
								range: lensRange,
								command: {
									id: ignoreCodeLensCommandId,
									title: "Ignore",
									arguments: [conflictRange.index]
								}
							}
						];
					}),
					dispose: () => void 0
				};
			} });
		};
		const getConflictRange = async (editorModel, index, path) => {
			const { model } = editorModel;
			const fallbackRange = new monaco.Range(1, 1, 1, model.getLineMaxColumn(1));
			let range = null;
			for (let depth = path.length; depth >= 0; depth -= 1) {
				const candidatePath = path.slice(0, depth);
				const nodeRange = await editorModel.getRangeFromPath(candidatePath);
				if (!nodeRange) continue;
				range = rangeToWholeLine(model, nodeRange);
				break;
			}
			return {
				index,
				path,
				range: markRaw(range ?? fallbackRange)
			};
		};
		const refreshConflictRangesAndDecorations = async () => {
			const [resultRanges, localRanges, remoteRanges] = await Promise.all([
				Promise.all(normalizeConflicts.value.map((conflict, index) => getConflictRange(modifiedResultModel, index, conflict.local.path))),
				Promise.all(normalizeConflicts.value.map((conflict, index) => getConflictRange(modifiedModelLocal, index, conflict.local.path))),
				Promise.all(normalizeConflicts.value.map((conflict, index) => getConflictRange(modifiedModelRemote, index, conflict.local.path)))
			]);
			conflictRangesState.value = resultRanges.filter((r) => Boolean(r));
			localConflictRangesState.value = localRanges.filter((r) => Boolean(r));
			remoteConflictRangesState.value = remoteRanges.filter((r) => Boolean(r));
			resultHighlightDecorations = resultModifiedEditor.deltaDecorations(resultHighlightDecorations, conflictRangesState.value.flatMap((cr) => getBoxDecorationsForRange(cr.range)));
			refreshCodeLensProvider();
		};
		const setConflictResolution = (index, resolution) => {
			resolvedConflicts.value[index] = resolution;
			refreshCodeLensProvider();
		};
		const applyConflictChoice = async (index, source) => {
			const conflict = normalizeConflicts.value[index];
			if (!conflict) return;
			let currentDocument = null;
			try {
				currentDocument = JSON.parse(modifiedResultModel.model.getValue());
			} catch {
				return;
			}
			const changes = source === "local" ? conflict.local.changes : conflict.remote.changes;
			const nextDocument = apply(deepClone(currentDocument), changes);
			suppressedChangeEvents += 1;
			modifiedResultModel.model.setValue(JSON.stringify(nextDocument, null, 2));
			setConflictResolution(index, source);
			await refreshConflictRangesAndDecorations();
		};
		codeLensCommandDisposables.push(monaco.editor.registerCommand(applyCurrentCodeLensCommandId, (_accessor, index) => {
			applyConflictChoice(index, "local");
		}));
		codeLensCommandDisposables.push(monaco.editor.registerCommand(applyRemoteCodeLensCommandId, (_accessor, index) => {
			applyConflictChoice(index, "remote");
		}));
		codeLensCommandDisposables.push(monaco.editor.registerCommand(ignoreCodeLensCommandId, (_accessor, index) => {
			setConflictResolution(index, "ignore");
		}));
		refreshConflictRangesAndDecorations();
		resultModifiedEditor.onDidChangeModelContent((event) => {
			if (suppressedChangeEvents > 0) {
				suppressedChangeEvents -= 1;
				return;
			}
			let hasStatusUpdates = false;
			for (const change of event.changes) {
				const matchingConflicts = conflictRangesState.value.filter((cr) => monaco.Range.areIntersectingOrTouching(change.range, cr.range));
				if (!matchingConflicts.length) continue;
				matchingConflicts.forEach((conflict) => {
					if (resolvedConflicts.value[conflict.index] !== "manual") {
						resolvedConflicts.value[conflict.index] = "manual";
						hasStatusUpdates = true;
					}
				});
			}
			if (hasStatusUpdates) refreshCodeLensProvider();
		});
		resultEditor.value = resultDiffEditor;
	};
	const dispose = () => {
		localChangesEditor.value?.dispose();
		localChangesEditor.value = void 0;
		remoteChangesEditor.value?.dispose();
		remoteChangesEditor.value = void 0;
		resultEditor.value?.dispose();
		resultEditor.value = void 0;
		for (const model of modelsToDispose) model.dispose();
		modelsToDispose = [];
		resultCodeLensProviderDisposable?.dispose();
		resultCodeLensProviderDisposable = void 0;
		jsonPointerLinkSupportDispose?.();
		jsonPointerLinkSupportDispose = void 0;
		codeLensCommandDisposables.forEach((d) => d.dispose());
		codeLensCommandDisposables.length = 0;
	};
	return {
		init,
		dispose,
		conflictsLeft,
		goToNextConflict,
		applyResolvedConflicts
	};
}
//#endregion
export { useThreeWayMergeEditor };

//# sourceMappingURL=use-three-way-merge-editor.js.map