import { isAstNode, isAstNodeDescription, isLinkingError } from "../syntax-tree.js";
import { findRootNode, streamAst, streamReferences } from "../utils/ast-utils.js";
import { cancellation_exports } from "../utils/cancellation.js";
import { interruptAndCheck } from "../utils/promise-utils.js";
import { DocumentState } from "../workspace/documents.js";
const RefResolving = Symbol("RefResolving");
var DefaultLinker = class {
	constructor(e) {
		this.reflection = e.shared.AstReflection, this.langiumDocuments = () => e.shared.workspace.LangiumDocuments, this.scopeProvider = e.references.ScopeProvider, this.astNodeLocator = e.workspace.AstNodeLocator, this.profiler = e.shared.profilers.LangiumProfiler, this.languageId = e.LanguageMetaData.languageId;
	}
	async link(e, n = cancellation_exports.CancellationToken.None) {
		if (this.profiler?.isActive("linking")) {
			let r = this.profiler.createTask("linking", this.languageId);
			r.start();
			try {
				for (let i of streamAst(e.parseResult.value)) await interruptAndCheck(n), streamReferences(i).forEach((n) => {
					let a = `${i.$type}:${n.property}`;
					r.startSubTask(a);
					try {
						this.doLink(n, e);
					} finally {
						r.stopSubTask(a);
					}
				});
			} finally {
				r.stop();
			}
		} else for (let r of streamAst(e.parseResult.value)) await interruptAndCheck(n), streamReferences(r).forEach((n) => this.doLink(n, e));
	}
	doLink(e, n) {
		let i = e.reference;
		if ("_ref" in i && i._ref === void 0) {
			i._ref = RefResolving;
			try {
				let n = this.getCandidate(e);
				isLinkingError(n) ? i._ref = n : (i._nodeDescription = n, i._ref = this.loadAstNode(n) ?? this.createLinkingError(e, n));
			} catch (n) {
				console.error(`An error occurred while resolving reference to '${i.$refText}':`, n);
				let r = n.message ?? String(n);
				i._ref = {
					info: e,
					message: `An error occurred while resolving reference to '${i.$refText}': ${r}`
				};
			}
			n.references.push(i);
		} else if ("_items" in i && i._items === void 0) {
			i._items = RefResolving;
			try {
				let n = this.getCandidates(e), a = [];
				if (isLinkingError(n)) i._linkingError = n;
				else for (let e of n) {
					let n = this.loadAstNode(e);
					n && a.push({
						ref: n,
						$nodeDescription: e
					});
				}
				i._items = a;
			} catch (n) {
				i._linkingError = {
					info: e,
					message: `An error occurred while resolving reference to '${i.$refText}': ${n}`
				}, i._items = [];
			}
			n.references.push(i);
		}
	}
	unlink(e) {
		for (let n of e.references) "_ref" in n ? (n._ref = void 0, delete n._nodeDescription) : "_items" in n && (n._items = void 0, delete n._linkingError);
		e.references = [];
	}
	getCandidate(e) {
		return this.scopeProvider.getScope(e).getElement(e.reference.$refText) ?? this.createLinkingError(e);
	}
	getCandidates(e) {
		let n = this.scopeProvider.getScope(e).getElements(e.reference.$refText).distinct((e) => `${e.documentUri}#${e.path}`).toArray();
		return n.length > 0 ? n : this.createLinkingError(e);
	}
	buildReference(a, o, s, c) {
		let u = this, d = {
			$refNode: s,
			$refText: c,
			_ref: void 0,
			get ref() {
				if (isAstNode(this._ref)) return this._ref;
				if (isAstNodeDescription(this._nodeDescription)) this._ref = u.loadAstNode(this._nodeDescription) ?? u.createLinkingError({
					reference: d,
					container: a,
					property: o
				}, this._nodeDescription);
				else if (this._ref === void 0) {
					this._ref = RefResolving;
					let e = findRootNode(a).$document, n = u.getLinkedNode({
						reference: d,
						container: a,
						property: o
					});
					if (n.error && e && e.state < DocumentState.ComputedScopes) {
						this._ref = void 0;
						return;
					}
					this._ref = n.node ?? n.error, this._nodeDescription = n.descr, e?.references.push(this);
				} else this._ref === RefResolving && u.throwCyclicReferenceError(a, o, c);
				return isAstNode(this._ref) ? this._ref : void 0;
			},
			get $nodeDescription() {
				return this._nodeDescription;
			},
			get error() {
				return isLinkingError(this._ref) ? this._ref : void 0;
			}
		};
		return d;
	}
	buildMultiReference(e, n, a, o) {
		let s = this, c = {
			$refNode: a,
			$refText: o,
			_items: void 0,
			get items() {
				if (Array.isArray(this._items)) return this._items;
				if (this._items === void 0) {
					this._items = RefResolving;
					let a = findRootNode(e).$document, o = s.getCandidates({
						reference: c,
						container: e,
						property: n
					}), l = [];
					if (isLinkingError(o)) this._linkingError = o;
					else for (let e of o) {
						let n = s.loadAstNode(e);
						n && l.push({
							ref: n,
							$nodeDescription: e
						});
					}
					this._items = l, a?.references.push(this);
				} else this._items === RefResolving && s.throwCyclicReferenceError(e, n, o);
				return Array.isArray(this._items) ? this._items : [];
			},
			get error() {
				if (this._linkingError) return this._linkingError;
				if (!(this.items.length > 0)) return this._linkingError = s.createLinkingError({
					reference: c,
					container: e,
					property: n
				});
			}
		};
		return c;
	}
	throwCyclicReferenceError(e, n, r) {
		throw Error(`Cyclic reference resolution detected: ${this.astNodeLocator.getAstNodePath(e)}/${n} (symbol '${r}')`);
	}
	getLinkedNode(e) {
		try {
			let n = this.getCandidate(e);
			if (isLinkingError(n)) return { error: n };
			let i = this.loadAstNode(n);
			return i ? {
				node: i,
				descr: n
			} : {
				descr: n,
				error: this.createLinkingError(e, n)
			};
		} catch (n) {
			console.error(`An error occurred while resolving reference to '${e.reference.$refText}':`, n);
			let r = n.message ?? String(n);
			return { error: {
				info: e,
				message: `An error occurred while resolving reference to '${e.reference.$refText}': ${r}`
			} };
		}
	}
	loadAstNode(e) {
		if (e.node) return e.node;
		let n = this.langiumDocuments().getDocument(e.documentUri);
		if (n) return this.astNodeLocator.getAstNode(n.parseResult.value, e.path);
	}
	createLinkingError(e, n) {
		let r = findRootNode(e.container).$document;
		return r && r.state < DocumentState.ComputedScopes && console.warn(`Attempted reference resolution before document reached ComputedScopes state (${r.uri}).`), {
			info: e,
			message: `Could not resolve reference to ${this.reflection.getReferenceType(e)} named '${e.reference.$refText}'.`,
			targetDescription: n
		};
	}
};
export { DefaultLinker, RefResolving };
