import { Disposable as h } from "../utils/disposable.js";
import { MultiMap as m } from "../utils/collections.js";
import { interruptAndCheck as c, OperationCancelled as g, isOperationCancelled as b } from "../utils/promise-utils.js";
import { stream as w } from "../utils/stream.js";
import { ValidationCategory as v } from "../validation/validation-registry.js";
import { DocumentState as o } from "./documents.js";
import { c as f } from "../../../../../../../_virtual/cancellation.js";
class M {
  constructor(t) {
    this.updateBuildOptions = {
      // Default: run only the built-in validation checks and those in the _fast_ category (includes those without category)
      validation: {
        categories: ["built-in", "fast"]
      }
    }, this.updateListeners = [], this.buildPhaseListeners = new m(), this.documentPhaseListeners = new m(), this.buildState = /* @__PURE__ */ new Map(), this.documentBuildWaiters = /* @__PURE__ */ new Map(), this.currentState = o.Changed, this.langiumDocuments = t.workspace.LangiumDocuments, this.langiumDocumentFactory = t.workspace.LangiumDocumentFactory, this.textDocuments = t.workspace.TextDocuments, this.indexManager = t.workspace.IndexManager, this.serviceRegistry = t.ServiceRegistry;
  }
  async build(t, e = {}, i = f.CancellationToken.None) {
    var r, a;
    for (const s of t) {
      const n = s.uri.toString();
      if (s.state === o.Validated) {
        if (typeof e.validation == "boolean" && e.validation)
          s.state = o.IndexedReferences, s.diagnostics = void 0, this.buildState.delete(n);
        else if (typeof e.validation == "object") {
          const l = this.buildState.get(n), u = (r = l == null ? void 0 : l.result) === null || r === void 0 ? void 0 : r.validationChecks;
          if (u) {
            const p = ((a = e.validation.categories) !== null && a !== void 0 ? a : v.all).filter((D) => !u.includes(D));
            p.length > 0 && (this.buildState.set(n, {
              completed: !1,
              options: {
                validation: Object.assign(Object.assign({}, e.validation), { categories: p })
              },
              result: l.result
            }), s.state = o.IndexedReferences);
          }
        }
      } else
        this.buildState.delete(n);
    }
    this.currentState = o.Changed, await this.emitUpdate(t.map((s) => s.uri), []), await this.buildDocuments(t, e, i);
  }
  async update(t, e, i = f.CancellationToken.None) {
    this.currentState = o.Changed;
    for (const s of e)
      this.langiumDocuments.deleteDocument(s), this.buildState.delete(s.toString()), this.indexManager.remove(s);
    for (const s of t) {
      if (!this.langiumDocuments.invalidateDocument(s)) {
        const l = this.langiumDocumentFactory.fromModel({ $type: "INVALID" }, s);
        l.state = o.Changed, this.langiumDocuments.addDocument(l);
      }
      this.buildState.delete(s.toString());
    }
    const r = w(t).concat(e).map((s) => s.toString()).toSet();
    this.langiumDocuments.all.filter((s) => !r.has(s.uri.toString()) && this.shouldRelink(s, r)).forEach((s) => {
      this.serviceRegistry.getServices(s.uri).references.Linker.unlink(s), s.state = Math.min(s.state, o.ComputedScopes), s.diagnostics = void 0;
    }), await this.emitUpdate(t, e), await c(i);
    const a = this.sortDocuments(this.langiumDocuments.all.filter((s) => {
      var n;
      return s.state < o.Linked || !(!((n = this.buildState.get(s.uri.toString())) === null || n === void 0) && n.completed);
    }).toArray());
    await this.buildDocuments(a, this.updateBuildOptions, i);
  }
  async emitUpdate(t, e) {
    await Promise.all(this.updateListeners.map((i) => i(t, e)));
  }
  /**
   * Sort the given documents by priority. By default, documents with an open text document are prioritized.
   * This is useful to ensure that visible documents show their diagnostics before all other documents.
   *
   * This improves the responsiveness in large workspaces as users usually don't care about diagnostics
   * in files that are currently not opened in the editor.
   */
  sortDocuments(t) {
    let e = 0, i = t.length - 1;
    for (; e < i; ) {
      for (; e < t.length && this.hasTextDocument(t[e]); )
        e++;
      for (; i >= 0 && !this.hasTextDocument(t[i]); )
        i--;
      e < i && ([t[e], t[i]] = [t[i], t[e]]);
    }
    return t;
  }
  hasTextDocument(t) {
    var e;
    return !!(!((e = this.textDocuments) === null || e === void 0) && e.get(t.uri));
  }
  /**
   * Check whether the given document should be relinked after changes were found in the given URIs.
   */
  shouldRelink(t, e) {
    return t.references.some((i) => i.error !== void 0) ? !0 : this.indexManager.isAffected(t, e);
  }
  onUpdate(t) {
    return this.updateListeners.push(t), h.create(() => {
      const e = this.updateListeners.indexOf(t);
      e >= 0 && this.updateListeners.splice(e, 1);
    });
  }
  /**
   * Build the given documents by stepping through all build phases. If a document's state indicates
   * that a certain build phase is already done, the phase is skipped for that document.
   *
   * @param documents The documents to build.
   * @param options the {@link BuildOptions} to use.
   * @param cancelToken A cancellation token that can be used to cancel the build.
   * @returns A promise that resolves when the build is done.
   */
  async buildDocuments(t, e, i) {
    this.prepareBuild(t, e), await this.runCancelable(t, o.Parsed, i, (a) => this.langiumDocumentFactory.update(a, i)), await this.runCancelable(t, o.IndexedContent, i, (a) => this.indexManager.updateContent(a, i)), await this.runCancelable(t, o.ComputedScopes, i, async (a) => {
      const s = this.serviceRegistry.getServices(a.uri).references.ScopeComputation;
      a.precomputedScopes = await s.computeLocalScopes(a, i);
    }), await this.runCancelable(t, o.Linked, i, (a) => this.serviceRegistry.getServices(a.uri).references.Linker.link(a, i)), await this.runCancelable(t, o.IndexedReferences, i, (a) => this.indexManager.updateReferences(a, i));
    const r = t.filter((a) => this.shouldValidate(a));
    await this.runCancelable(r, o.Validated, i, (a) => this.validate(a, i));
    for (const a of t) {
      const s = this.buildState.get(a.uri.toString());
      s && (s.completed = !0);
    }
  }
  /**
   * Runs prior to beginning the build process to update the {@link DocumentBuildState} for each document
   *
   * @param documents collection of documents to be built
   * @param options the {@link BuildOptions} to use
   */
  prepareBuild(t, e) {
    for (const i of t) {
      const r = i.uri.toString(), a = this.buildState.get(r);
      (!a || a.completed) && this.buildState.set(r, {
        completed: !1,
        options: e,
        result: a == null ? void 0 : a.result
      });
    }
  }
  /**
   * Runs a cancelable operation on a set of documents to bring them to a specified {@link DocumentState}.
   *
   * @param documents The array of documents to process.
   * @param targetState The target {@link DocumentState} to bring the documents to.
   * @param cancelToken A token that can be used to cancel the operation.
   * @param callback A function to be called for each document.
   * @returns A promise that resolves when all documents have been processed or the operation is canceled.
   * @throws Will throw `OperationCancelled` if the operation is canceled via a `CancellationToken`.
   */
  async runCancelable(t, e, i, r) {
    const a = t.filter((n) => n.state < e);
    for (const n of a)
      await c(i), await r(n), n.state = e, await this.notifyDocumentPhase(n, e, i);
    const s = t.filter((n) => n.state === e);
    await this.notifyBuildPhase(s, e, i), this.currentState = e;
  }
  onBuildPhase(t, e) {
    return this.buildPhaseListeners.add(t, e), h.create(() => {
      this.buildPhaseListeners.delete(t, e);
    });
  }
  onDocumentPhase(t, e) {
    return this.documentPhaseListeners.add(t, e), h.create(() => {
      this.documentPhaseListeners.delete(t, e);
    });
  }
  waitUntil(t, e, i) {
    let r;
    if (e && "path" in e ? r = e : i = e, i ?? (i = f.CancellationToken.None), r) {
      const a = this.langiumDocuments.getDocument(r);
      if (a && a.state > t)
        return Promise.resolve(r);
    }
    return this.currentState >= t ? Promise.resolve(void 0) : i.isCancellationRequested ? Promise.reject(g) : new Promise((a, s) => {
      const n = this.onBuildPhase(t, () => {
        if (n.dispose(), l.dispose(), r) {
          const u = this.langiumDocuments.getDocument(r);
          a(u == null ? void 0 : u.uri);
        } else
          a(void 0);
      }), l = i.onCancellationRequested(() => {
        n.dispose(), l.dispose(), s(g);
      });
    });
  }
  async notifyDocumentPhase(t, e, i) {
    const a = this.documentPhaseListeners.get(e).slice();
    for (const s of a)
      try {
        await s(t, i);
      } catch (n) {
        if (!b(n))
          throw n;
      }
  }
  async notifyBuildPhase(t, e, i) {
    if (t.length === 0)
      return;
    const a = this.buildPhaseListeners.get(e).slice();
    for (const s of a)
      await c(i), await s(t, i);
  }
  /**
   * Determine whether the given document should be validated during a build. The default
   * implementation checks the `validation` property of the build options. If it's set to `true`
   * or a `ValidationOptions` object, the document is included in the validation phase.
   */
  shouldValidate(t) {
    return !!this.getBuildOptions(t).validation;
  }
  /**
   * Run validation checks on the given document and store the resulting diagnostics in the document.
   * If the document already contains diagnostics, the new ones are added to the list.
   */
  async validate(t, e) {
    var i, r;
    const a = this.serviceRegistry.getServices(t.uri).validation.DocumentValidator, s = this.getBuildOptions(t).validation, n = typeof s == "object" ? s : void 0, l = await a.validateDocument(t, n, e);
    t.diagnostics ? t.diagnostics.push(...l) : t.diagnostics = l;
    const u = this.buildState.get(t.uri.toString());
    if (u) {
      (i = u.result) !== null && i !== void 0 || (u.result = {});
      const d = (r = n == null ? void 0 : n.categories) !== null && r !== void 0 ? r : v.all;
      u.result.validationChecks ? u.result.validationChecks.push(...d) : u.result.validationChecks = [...d];
    }
  }
  getBuildOptions(t) {
    var e, i;
    return (i = (e = this.buildState.get(t.uri.toString())) === null || e === void 0 ? void 0 : e.options) !== null && i !== void 0 ? i : {};
  }
}
export {
  M as DefaultDocumentBuilder
};
