/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  asyncNew,
  Construction,
  Diagnostic,
  ImplementationError,
  InternalError,
  isObject,
  Logger,
  MatterAggregateError,
  MaybePromise,
  Observable
} from "@matter/general";
import {
  EventNumber,
  Priority,
  resolveEventName
} from "@matter/types";
const logger = Logger.get("OccurrenceManager");
class OccurrenceManager {
  #store;
  #bufferConfig;
  #cull;
  #iteratingValuesInProgress = false;
  #added = new Observable();
  // As we don't (yet) have storage with secondary indices we currently maintain indices in memory regardless of
  // whether underlying store is volatile
  #occurrences = new Array();
  #construction;
  get construction() {
    return this.#construction;
  }
  static async create(context) {
    return asyncNew(OccurrenceManager, context);
  }
  constructor({ store, bufferConfig }) {
    if (bufferConfig === void 0) {
      bufferConfig = OccurrenceManager.DefaultBufferConfig;
    }
    const { minEventAllowance, maxEventAllowance } = bufferConfig;
    if (maxEventAllowance < minEventAllowance) {
      throw new ImplementationError(
        `Maximum allowance ${maxEventAllowance} is less than minimum allowance ${minEventAllowance}`
      );
    }
    this.#store = store;
    this.#bufferConfig = bufferConfig;
    this.#construction = Construction(this, () => {
      return MaybePromise.then(this.#store.load(), (index) => {
        index.sort(
          // sort that way because Bigint & Number mix
          ({ number: numberA }, { number: numberB }) => numberA < numberB ? -1 : numberA > numberB ? 1 : 0
        );
        this.#occurrences.push(...index);
        if (this.#occurrences.length > this.#bufferConfig.minEventAllowance) {
          this.#startCull();
        }
      });
    });
  }
  get added() {
    return this.#added;
  }
  async clear() {
    await this.construction;
    await this.#store.clear();
    this.#occurrences.length = 0;
  }
  /**
   * Find the index of the first event number in the list that is greater than or equal to eventMin to optimize
   * searching.
   */
  #findMinEventNumberIndex(eventMin) {
    if (this.#occurrences.length === 0 || eventMin > this.#occurrences[this.#occurrences.length - 1].number) {
      return -1;
    }
    if (eventMin <= this.#occurrences[0].number) {
      return 0;
    }
    let low = this.#occurrences.length - 1;
    let high = low;
    let step = 1;
    while (low > 0 && this.#occurrences[low].number >= eventMin) {
      high = low;
      low = Math.max(0, low - step);
      step *= 2;
    }
    while (low <= high) {
      const mid = Math.floor((low + high) / 2);
      if (this.#occurrences[mid].number < eventMin) {
        low = mid + 1;
      } else {
        high = mid - 1;
      }
    }
    return low;
  }
  /**
   * Query the event store for events matching the given path and filters.
   * @deprecated
   */
  query(eventPath, filters, filterForFabricIndex) {
    if (filters !== void 0 && filters.length > 1) {
      throw new InternalError("Multiple filters not supported");
    }
    const startIndex = filters?.length ? this.#findMinEventNumberIndex(EventNumber(filters[0].eventMin)) : 0;
    if (startIndex === -1) {
      return [];
    }
    let isAsyncLoad = false;
    const { endpointId, clusterId, eventId } = eventPath;
    const occurrences = new Array();
    for (let i = startIndex; i < this.#occurrences.length; i++) {
      const entry = this.#occurrences[i];
      if (endpointId === entry.endpointId && clusterId === entry.clusterId && eventId === entry.eventId) {
        let occurrence = this.#store.get(entry.number);
        occurrence = MaybePromise.then(occurrence, (occurrence2) => {
          occurrence2.number = entry.number;
          return occurrence2;
        });
        occurrences.push(occurrence);
        if (MaybePromise.is(occurrence)) {
          isAsyncLoad = true;
        }
      }
    }
    logger.debug(
      `Got ${occurrences.length} events for ${resolveEventName(eventPath)} with filters: ${Diagnostic.json(filters)}`
    );
    let result;
    if (isAsyncLoad) {
      result = Promise.all(occurrences);
    } else {
      result = occurrences;
    }
    if (filterForFabricIndex !== void 0) {
      result = MaybePromise.then(
        result,
        (occurrences2) => occurrences2.filter(({ payload }) => {
          if (!isObject(payload)) {
            return true;
          }
          const { fabricIndex } = payload;
          return fabricIndex === void 0 || fabricIndex === filterForFabricIndex;
        })
      );
    }
    result = MaybePromise.then(result, (occurrences2) => {
      let filterDesc;
      if (filters?.length) {
        filterDesc = filters.map(({ nodeId, eventMin }) => `${nodeId}:${eventMin}`).join(",");
      }
      if (filterForFabricIndex) {
        if (filterDesc !== void 0) {
          filterDesc = `${filterDesc} and fabric #${filterForFabricIndex}`;
        } else {
          filterDesc = `fabric #${filterForFabricIndex}`;
        }
      }
      if (filterDesc) {
        filterDesc = ` (filters: ${filterDesc})`;
      } else {
        filterDesc = "";
      }
      logger.debug(
        `Loaded ${occurrences2.length} occurrence${occurrences2.length === 1 ? "" : "s"} for ${resolveEventName(eventPath)}${filterDesc}`
      );
      return occurrences2;
    });
    return result;
  }
  /**
   * Return an iterator over all occurrences in the store that are bigger or equal to the minimum eventNumber,
   * if provided.
   */
  async *get(eventMin) {
    if (this.#cull) {
      await this.#cull;
    }
    const startIndex = eventMin === void 0 ? 0 : this.#findMinEventNumberIndex(eventMin);
    if (startIndex === -1) {
      return;
    }
    this.#iteratingValuesInProgress = true;
    try {
      for (let i = startIndex; i < this.#occurrences.length; i++) {
        const eventNumber = this.#occurrences[i].number;
        const occurrence = this.#store.get(eventNumber);
        if (MaybePromise.is(occurrence)) {
          yield {
            ...await occurrence,
            number: eventNumber
          };
        } else {
          yield {
            ...occurrence,
            number: eventNumber
          };
        }
      }
    } finally {
      this.#iteratingValuesInProgress = false;
    }
  }
  // For ServerNode usage the ServerEnvironment takes care to close the OccurrenceManager
  close() {
    return MaybePromise.then(this.#cull, () => this.#store.close());
  }
  add(occurrence) {
    return MaybePromise.then(this.#store.add(occurrence), (entry) => {
      logger.debug(`Recorded event #${entry.number}`, Diagnostic.dict(occurrence));
      this.#occurrences.push(entry);
      if (this.#occurrences.length > this.#bufferConfig.maxEventAllowance) {
        this.#startCull();
      }
      const numberedOccurrence = {
        number: entry.number,
        ...occurrence
      };
      this.#added.emit(numberedOccurrence);
      return numberedOccurrence;
    });
  }
  remove(number) {
    const index = this.#occurrences.findIndex((entry) => entry.number === number);
    if (index === -1) {
      return;
    }
    this.#occurrences.splice(index, 1);
    if (this.#cull) {
      return this.#cull.then(() => this.#store.delete(number));
    }
    return this.#store.delete(number);
  }
  #startCull() {
    if (this.#cull || this.#iteratingValuesInProgress) {
      return;
    }
    const cull = this.#dropOldOccurrences();
    if (MaybePromise.is(cull)) {
      this.#cull = Promise.resolve(cull).finally(() => this.#cull = void 0);
    }
  }
  #dropOldOccurrences() {
    let toDelete = this.#occurrences.length - this.#bufferConfig.minEventAllowance;
    if (toDelete <= 0) {
      return;
    }
    logger.debug(`Event store is full; dropping ${toDelete} old occurrence${toDelete === 1 ? "s" : ""}`);
    const prioData = {
      [Priority.Info]: {
        count: this.#bufferConfig.minPriorityEventAllowance["info"],
        minPosition: -1
      },
      [Priority.Debug]: {
        count: this.#bufferConfig.minPriorityEventAllowance["debug"],
        minPosition: -1
      }
    };
    const asyncDrops = Array();
    for (let i = this.#occurrences.length - 1; i >= 0; i--) {
      const { priority } = this.#occurrences[i];
      if (priority !== Priority.Critical) {
        const data = prioData[priority];
        if (data.count > 0) {
          data.count--;
          if (data.count === 0) {
            data.minPosition = i;
            if (prioData[Priority.Info].minPosition > -1 && prioData[Priority.Debug].minPosition > -1) {
              break;
            }
          }
        }
      }
    }
    const occurrences = this.#occurrences;
    for (const priority of [Priority.Debug, Priority.Info, Priority.Critical]) {
      const checkUpTo = priority === Priority.Critical ? this.#occurrences.length : prioData[priority].minPosition;
      if (checkUpTo === -1) {
        continue;
      }
      for (let i = 0; i < checkUpTo && toDelete > 0; i++) {
        if (occurrences[i] === void 0) continue;
        const { priority: entryPriority, number } = occurrences[i];
        if (entryPriority === priority) {
          const drop = MaybePromise.catch(
            this.#store.delete(number),
            (error) => logger.warn(`Error dropping occurrence #${number}: ${error}`)
          );
          if (MaybePromise.is(drop)) {
            asyncDrops.push(drop);
          }
          occurrences[i] = void 0;
          toDelete--;
        }
      }
      if (toDelete <= 0) {
        break;
      }
    }
    this.#occurrences = occurrences.filter((entry) => entry);
    if (asyncDrops.length) {
      return MatterAggregateError.allSettled(asyncDrops, "Error dropping occurrences").then(() => {
      }).catch((error) => logger.error(error));
    }
  }
}
((OccurrenceManager2) => {
  OccurrenceManager2.DefaultBufferConfig = {
    minEventAllowance: 1e4,
    maxEventAllowance: 11e3,
    minPriorityEventAllowance: {
      info: 2e3,
      debug: 2e3
    }
  };
})(OccurrenceManager || (OccurrenceManager = {}));
export {
  OccurrenceManager
};
//# sourceMappingURL=OccurrenceManager.js.map
