"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const disposable = require("./lib-utils-disposable.cjs");
const collections = require("./lib-utils-collections.cjs");
const promiseUtils = require("./lib-utils-promise-utils.cjs");
const stream = require("./lib-utils-stream.cjs");
const validationRegistry = require("./lib-validation-validation-registry.cjs");
const documents = require("./lib-workspace-documents.cjs");
const cancellation = require("../../_virtual/cancellation.cjs");
class DefaultDocumentBuilder {
  constructor(services) {
    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 collections.MultiMap();
    this.documentPhaseListeners = new collections.MultiMap();
    this.buildState = /* @__PURE__ */ new Map();
    this.documentBuildWaiters = /* @__PURE__ */ new Map();
    this.currentState = documents.DocumentState.Changed;
    this.langiumDocuments = services.workspace.LangiumDocuments;
    this.langiumDocumentFactory = services.workspace.LangiumDocumentFactory;
    this.textDocuments = services.workspace.TextDocuments;
    this.indexManager = services.workspace.IndexManager;
    this.serviceRegistry = services.ServiceRegistry;
  }
  async build(documents$1, options = {}, cancelToken = cancellation.cancellationExports.CancellationToken.None) {
    var _a, _b;
    for (const document of documents$1) {
      const key = document.uri.toString();
      if (document.state === documents.DocumentState.Validated) {
        if (typeof options.validation === "boolean" && options.validation) {
          document.state = documents.DocumentState.IndexedReferences;
          document.diagnostics = void 0;
          this.buildState.delete(key);
        } else if (typeof options.validation === "object") {
          const buildState = this.buildState.get(key);
          const previousCategories = (_a = buildState === null || buildState === void 0 ? void 0 : buildState.result) === null || _a === void 0 ? void 0 : _a.validationChecks;
          if (previousCategories) {
            const newCategories = (_b = options.validation.categories) !== null && _b !== void 0 ? _b : validationRegistry.ValidationCategory.all;
            const categories = newCategories.filter((c) => !previousCategories.includes(c));
            if (categories.length > 0) {
              this.buildState.set(key, {
                completed: false,
                options: {
                  validation: Object.assign(Object.assign({}, options.validation), { categories })
                },
                result: buildState.result
              });
              document.state = documents.DocumentState.IndexedReferences;
            }
          }
        }
      } else {
        this.buildState.delete(key);
      }
    }
    this.currentState = documents.DocumentState.Changed;
    await this.emitUpdate(documents$1.map((e) => e.uri), []);
    await this.buildDocuments(documents$1, options, cancelToken);
  }
  async update(changed, deleted, cancelToken = cancellation.cancellationExports.CancellationToken.None) {
    this.currentState = documents.DocumentState.Changed;
    for (const deletedUri of deleted) {
      this.langiumDocuments.deleteDocument(deletedUri);
      this.buildState.delete(deletedUri.toString());
      this.indexManager.remove(deletedUri);
    }
    for (const changedUri of changed) {
      const invalidated = this.langiumDocuments.invalidateDocument(changedUri);
      if (!invalidated) {
        const newDocument = this.langiumDocumentFactory.fromModel({ $type: "INVALID" }, changedUri);
        newDocument.state = documents.DocumentState.Changed;
        this.langiumDocuments.addDocument(newDocument);
      }
      this.buildState.delete(changedUri.toString());
    }
    const allChangedUris = stream.stream(changed).concat(deleted).map((uri) => uri.toString()).toSet();
    this.langiumDocuments.all.filter((doc) => !allChangedUris.has(doc.uri.toString()) && this.shouldRelink(doc, allChangedUris)).forEach((doc) => {
      const linker = this.serviceRegistry.getServices(doc.uri).references.Linker;
      linker.unlink(doc);
      doc.state = Math.min(doc.state, documents.DocumentState.ComputedScopes);
      doc.diagnostics = void 0;
    });
    await this.emitUpdate(changed, deleted);
    await promiseUtils.interruptAndCheck(cancelToken);
    const rebuildDocuments = this.sortDocuments(this.langiumDocuments.all.filter((doc) => {
      var _a;
      return doc.state < documents.DocumentState.Linked || !((_a = this.buildState.get(doc.uri.toString())) === null || _a === void 0 ? void 0 : _a.completed);
    }).toArray());
    await this.buildDocuments(rebuildDocuments, this.updateBuildOptions, cancelToken);
  }
  async emitUpdate(changed, deleted) {
    await Promise.all(this.updateListeners.map((listener) => listener(changed, deleted)));
  }
  /**
   * 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(documents2) {
    let left = 0;
    let right = documents2.length - 1;
    while (left < right) {
      while (left < documents2.length && this.hasTextDocument(documents2[left])) {
        left++;
      }
      while (right >= 0 && !this.hasTextDocument(documents2[right])) {
        right--;
      }
      if (left < right) {
        [documents2[left], documents2[right]] = [documents2[right], documents2[left]];
      }
    }
    return documents2;
  }
  hasTextDocument(doc) {
    var _a;
    return Boolean((_a = this.textDocuments) === null || _a === void 0 ? void 0 : _a.get(doc.uri));
  }
  /**
   * Check whether the given document should be relinked after changes were found in the given URIs.
   */
  shouldRelink(document, changedUris) {
    if (document.references.some((ref) => ref.error !== void 0)) {
      return true;
    }
    return this.indexManager.isAffected(document, changedUris);
  }
  onUpdate(callback) {
    this.updateListeners.push(callback);
    return disposable.Disposable.create(() => {
      const index = this.updateListeners.indexOf(callback);
      if (index >= 0) {
        this.updateListeners.splice(index, 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(documents$1, options, cancelToken) {
    this.prepareBuild(documents$1, options);
    await this.runCancelable(documents$1, documents.DocumentState.Parsed, cancelToken, (doc) => this.langiumDocumentFactory.update(doc, cancelToken));
    await this.runCancelable(documents$1, documents.DocumentState.IndexedContent, cancelToken, (doc) => this.indexManager.updateContent(doc, cancelToken));
    await this.runCancelable(documents$1, documents.DocumentState.ComputedScopes, cancelToken, async (doc) => {
      const scopeComputation = this.serviceRegistry.getServices(doc.uri).references.ScopeComputation;
      doc.precomputedScopes = await scopeComputation.computeLocalScopes(doc, cancelToken);
    });
    await this.runCancelable(documents$1, documents.DocumentState.Linked, cancelToken, (doc) => {
      const linker = this.serviceRegistry.getServices(doc.uri).references.Linker;
      return linker.link(doc, cancelToken);
    });
    await this.runCancelable(documents$1, documents.DocumentState.IndexedReferences, cancelToken, (doc) => this.indexManager.updateReferences(doc, cancelToken));
    const toBeValidated = documents$1.filter((doc) => this.shouldValidate(doc));
    await this.runCancelable(toBeValidated, documents.DocumentState.Validated, cancelToken, (doc) => this.validate(doc, cancelToken));
    for (const doc of documents$1) {
      const state = this.buildState.get(doc.uri.toString());
      if (state) {
        state.completed = true;
      }
    }
  }
  /**
   * 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(documents2, options) {
    for (const doc of documents2) {
      const key = doc.uri.toString();
      const state = this.buildState.get(key);
      if (!state || state.completed) {
        this.buildState.set(key, {
          completed: false,
          options,
          result: state === null || state === void 0 ? void 0 : state.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(documents2, targetState, cancelToken, callback) {
    const filtered = documents2.filter((doc) => doc.state < targetState);
    for (const document of filtered) {
      await promiseUtils.interruptAndCheck(cancelToken);
      await callback(document);
      document.state = targetState;
      await this.notifyDocumentPhase(document, targetState, cancelToken);
    }
    const targetStateDocs = documents2.filter((doc) => doc.state === targetState);
    await this.notifyBuildPhase(targetStateDocs, targetState, cancelToken);
    this.currentState = targetState;
  }
  onBuildPhase(targetState, callback) {
    this.buildPhaseListeners.add(targetState, callback);
    return disposable.Disposable.create(() => {
      this.buildPhaseListeners.delete(targetState, callback);
    });
  }
  onDocumentPhase(targetState, callback) {
    this.documentPhaseListeners.add(targetState, callback);
    return disposable.Disposable.create(() => {
      this.documentPhaseListeners.delete(targetState, callback);
    });
  }
  waitUntil(state, uriOrToken, cancelToken) {
    let uri = void 0;
    if (uriOrToken && "path" in uriOrToken) {
      uri = uriOrToken;
    } else {
      cancelToken = uriOrToken;
    }
    cancelToken !== null && cancelToken !== void 0 ? cancelToken : cancelToken = cancellation.cancellationExports.CancellationToken.None;
    if (uri) {
      const document = this.langiumDocuments.getDocument(uri);
      if (document && document.state > state) {
        return Promise.resolve(uri);
      }
    }
    if (this.currentState >= state) {
      return Promise.resolve(void 0);
    } else if (cancelToken.isCancellationRequested) {
      return Promise.reject(promiseUtils.OperationCancelled);
    }
    return new Promise((resolve, reject) => {
      const buildDisposable = this.onBuildPhase(state, () => {
        buildDisposable.dispose();
        cancelDisposable.dispose();
        if (uri) {
          const document = this.langiumDocuments.getDocument(uri);
          resolve(document === null || document === void 0 ? void 0 : document.uri);
        } else {
          resolve(void 0);
        }
      });
      const cancelDisposable = cancelToken.onCancellationRequested(() => {
        buildDisposable.dispose();
        cancelDisposable.dispose();
        reject(promiseUtils.OperationCancelled);
      });
    });
  }
  async notifyDocumentPhase(document, state, cancelToken) {
    const listeners = this.documentPhaseListeners.get(state);
    const listenersCopy = listeners.slice();
    for (const listener of listenersCopy) {
      try {
        await listener(document, cancelToken);
      } catch (err) {
        if (!promiseUtils.isOperationCancelled(err)) {
          throw err;
        }
      }
    }
  }
  async notifyBuildPhase(documents2, state, cancelToken) {
    if (documents2.length === 0) {
      return;
    }
    const listeners = this.buildPhaseListeners.get(state);
    const listenersCopy = listeners.slice();
    for (const listener of listenersCopy) {
      await promiseUtils.interruptAndCheck(cancelToken);
      await listener(documents2, cancelToken);
    }
  }
  /**
   * 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(document) {
    return Boolean(this.getBuildOptions(document).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(document, cancelToken) {
    var _a, _b;
    const validator = this.serviceRegistry.getServices(document.uri).validation.DocumentValidator;
    const validationSetting = this.getBuildOptions(document).validation;
    const options = typeof validationSetting === "object" ? validationSetting : void 0;
    const diagnostics = await validator.validateDocument(document, options, cancelToken);
    if (document.diagnostics) {
      document.diagnostics.push(...diagnostics);
    } else {
      document.diagnostics = diagnostics;
    }
    const state = this.buildState.get(document.uri.toString());
    if (state) {
      (_a = state.result) !== null && _a !== void 0 ? _a : state.result = {};
      const newCategories = (_b = options === null || options === void 0 ? void 0 : options.categories) !== null && _b !== void 0 ? _b : validationRegistry.ValidationCategory.all;
      if (state.result.validationChecks) {
        state.result.validationChecks.push(...newCategories);
      } else {
        state.result.validationChecks = [...newCategories];
      }
    }
  }
  getBuildOptions(document) {
    var _a, _b;
    return (_b = (_a = this.buildState.get(document.uri.toString())) === null || _a === void 0 ? void 0 : _a.options) !== null && _b !== void 0 ? _b : {};
  }
}
exports.DefaultDocumentBuilder = DefaultDocumentBuilder;
//# sourceMappingURL=lib-workspace-document-builder.cjs.map
