import { compareUTF8, lessThanEq, lessThan, greaterThan } from "compare-utf8";
import { assert } from "../../shared/src/asserts.js";
import { binarySearch } from "../../shared/src/binary-search.js";
import { deepEqual } from "../../shared/src/json.js";
import { decodeIndexKey } from "./db/index.js";
import { Regular, InitialRun } from "./invoke-kind-enum.js";
import { SubscriptionTransactionWrapper } from "./transactions.js";
const emptySet = /* @__PURE__ */ new Set();
const unitializedLastValue = Symbol();
class SubscriptionImpl {
  #body;
  #onData;
  #lastValue = unitializedLastValue;
  #keys = emptySet;
  #scans = [];
  onError;
  onDone;
  #isEqual;
  constructor(body, onData, onError, onDone, isEqual = deepEqual) {
    this.#body = body;
    this.#onData = onData;
    this.onError = onError;
    this.onDone = onDone;
    this.#isEqual = isEqual;
  }
  hasIndexSubscription(indexName) {
    for (const scan of this.#scans) {
      if (scan.options.indexName === indexName) {
        return true;
      }
    }
    return false;
  }
  invoke(tx, _kind, _diffs) {
    return this.#body(tx);
  }
  matches(diffs) {
    for (const [indexName, diff] of diffs) {
      if (diffMatchesSubscription(this.#keys, this.#scans, indexName, diff)) {
        return true;
      }
    }
    return false;
  }
  updateDeps(keys, scans) {
    this.#keys = keys;
    this.#scans = scans;
  }
  onData(result) {
    if (this.#lastValue === unitializedLastValue || !this.#isEqual(this.#lastValue, result)) {
      this.#lastValue = result;
      this.#onData(result);
    }
  }
}
class WatchSubscription {
  #callback;
  #prefix;
  #indexName;
  #initialValuesInFirstDiff;
  onError = void 0;
  onDone = void 0;
  constructor(callback, options) {
    this.#callback = callback;
    this.#prefix = options?.prefix ?? "";
    this.#indexName = options?.indexName;
    this.#initialValuesInFirstDiff = options?.initialValuesInFirstDiff ?? false;
  }
  hasIndexSubscription(indexName) {
    return this.#indexName === indexName;
  }
  onData(result) {
    if (result !== void 0) {
      this.#callback(result);
    }
  }
  invoke(tx, kind, diffs) {
    const invoke = async (indexName, prefix, compareKey, convertInternalDiff) => {
      let diff;
      if (kind === InitialRun) {
        if (!this.#initialValuesInFirstDiff) {
          return void 0;
        }
        assert(diffs === void 0);
        const newDiff2 = [];
        for await (const entry of tx.scan({ prefix, indexName }).entries()) {
          newDiff2.push({
            op: "add",
            key: entry[0],
            newValue: entry[1]
          });
        }
        diff = newDiff2;
      } else {
        assert(diffs);
        const maybeDiff = diffs.get(indexName ?? "") ?? [];
        diff = convertInternalDiff(maybeDiff);
      }
      const newDiff = [];
      const { length } = diff;
      for (let i = diffBinarySearch(diff, prefix, compareKey); i < length; i++) {
        if (compareKey(diff[i]).startsWith(prefix)) {
          newDiff.push(diff[i]);
        } else {
          break;
        }
      }
      return kind === InitialRun || newDiff.length > 0 ? newDiff : void 0;
    };
    if (this.#indexName) {
      return invoke(
        this.#indexName,
        this.#prefix,
        (diff) => diff.key[0],
        (internalDiff) => convertDiffValues(internalDiff, decodeIndexKey)
      );
    }
    return invoke(
      void 0,
      this.#prefix,
      (diff) => diff.key,
      (internalDiff) => convertDiffValues(internalDiff, (k) => k)
    );
  }
  matches(diffs) {
    const diff = diffs.get(this.#indexName ?? "");
    if (diff === void 0) {
      return false;
    }
    return watcherMatchesDiff(diff, this.#prefix, this.#indexName);
  }
  updateDeps(_keys, _scans) {
  }
}
function convertDiffValues(diff, convertKey) {
  return diff.map((op) => {
    const key = convertKey(op.key);
    switch (op.op) {
      case "add":
        return {
          op: "add",
          key,
          newValue: op.newValue
        };
      case "change":
        return {
          op: "change",
          key,
          oldValue: op.oldValue,
          newValue: op.newValue
        };
      case "del":
        return {
          op: "del",
          key,
          oldValue: op.oldValue
        };
    }
  });
}
class SubscriptionsManagerImpl {
  #subscriptions = /* @__PURE__ */ new Set();
  #pendingSubscriptions = /* @__PURE__ */ new Set();
  #queryInternal;
  #lc;
  hasPendingSubscriptionRuns = false;
  #signal;
  constructor(queryInternal, lc, signal) {
    this.#queryInternal = queryInternal;
    this.#lc = lc;
    this.#signal = signal;
  }
  add(subscription) {
    this.#subscriptions.add(subscription);
    void this.#scheduleInitialSubscriptionRun(
      subscription
    );
    return () => this.#subscriptions.delete(subscription);
  }
  clear() {
    for (const subscription of this.#subscriptions) {
      subscription.onDone?.();
    }
    this.#subscriptions.clear();
  }
  fire(diffs) {
    const subscriptions = subscriptionsForDiffs(this.#subscriptions, diffs);
    return this.#fireSubscriptions(subscriptions, Regular, diffs);
  }
  async #fireSubscriptions(subscriptions, kind, diffs) {
    if (this.#signal.aborted) {
      return;
    }
    const subs = [...subscriptions];
    if (subs.length === 0) {
      return;
    }
    const results = await this.#queryInternal(
      (tx) => Promise.allSettled(
        subs.map(async (s) => {
          const stx = new SubscriptionTransactionWrapper(tx);
          try {
            return await s.invoke(stx, kind, diffs);
          } finally {
            s.updateDeps(stx.keys, stx.scans);
          }
        })
      )
    );
    this.callCallbacks(subs, results);
  }
  // Public method so that ZQL can wrap it in a transaction.
  callCallbacks(subs, results) {
    for (let i = 0; i < subs.length; i++) {
      const s = subs[i];
      const result = results[i];
      if (result.status === "fulfilled") {
        s.onData(result.value);
      } else {
        if (s.onError) {
          s.onError(result.reason);
        } else {
          this.#lc.error?.("Error in subscription body:", result.reason);
        }
      }
    }
  }
  async #scheduleInitialSubscriptionRun(s) {
    this.#pendingSubscriptions.add(s);
    if (!this.hasPendingSubscriptionRuns) {
      this.hasPendingSubscriptionRuns = true;
      await Promise.resolve();
      this.hasPendingSubscriptionRuns = false;
      const subscriptions = [...this.#pendingSubscriptions];
      this.#pendingSubscriptions.clear();
      await this.#fireSubscriptions(
        subscriptions,
        InitialRun,
        void 0
      );
    }
  }
  shouldComputeDiffs() {
    return this.#subscriptions.size > 0;
  }
  shouldComputeDiffsForIndex(indexName) {
    for (const s of this.#subscriptions) {
      if (s.hasIndexSubscription(indexName)) {
        return true;
      }
    }
    return false;
  }
}
function diffMatchesSubscription(keys, scans, indexName, diff) {
  if (indexName === "") {
    for (const diffEntry of diff) {
      if (keys.has(diffEntry.key)) {
        return true;
      }
    }
  }
  for (const scanInfo of scans) {
    if (scanInfoMatchesDiff(scanInfo, indexName, diff)) {
      return true;
    }
  }
  return false;
}
function scanInfoMatchesDiff(scanInfo, changeIndexName, diff) {
  for (const diffEntry of diff) {
    if (scanInfoMatchesKey(scanInfo, changeIndexName, diffEntry.key)) {
      return true;
    }
  }
  return false;
}
function scanInfoMatchesKey(scanInfo, changeIndexName, changedKey) {
  const {
    indexName = "",
    limit,
    prefix,
    startKey,
    startExclusive,
    startSecondaryKey
  } = scanInfo.options;
  if (changeIndexName !== indexName) {
    return false;
  }
  if (!indexName) {
    if (limit !== void 0 && limit <= 0) {
      return false;
    }
    if (!prefix && !startKey) {
      return true;
    }
    if (prefix && (!changedKey.startsWith(prefix) || isKeyPastInclusiveLimit(scanInfo, changedKey))) {
      return false;
    }
    if (startKey && (startExclusive && lessThanEq(changedKey, startKey) || lessThan(changedKey, startKey) || isKeyPastInclusiveLimit(scanInfo, changedKey))) {
      return false;
    }
    return true;
  }
  if (!prefix && !startKey && !startSecondaryKey) {
    return true;
  }
  const [changedKeySecondary, changedKeyPrimary] = decodeIndexKey(changedKey);
  if (prefix) {
    if (!changedKeySecondary.startsWith(prefix)) {
      return false;
    }
  }
  if (startSecondaryKey && (startExclusive && lessThanEq(changedKeySecondary, startSecondaryKey) || lessThan(changedKeySecondary, startSecondaryKey))) {
    return false;
  }
  if (startKey && (startExclusive && lessThanEq(changedKeyPrimary, startKey) || lessThan(changedKeyPrimary, startKey))) {
    return false;
  }
  return true;
}
function isKeyPastInclusiveLimit(scanInfo, changedKey) {
  const { inclusiveLimitKey } = scanInfo;
  return scanInfo.options.limit !== void 0 && inclusiveLimitKey !== void 0 && greaterThan(changedKey, inclusiveLimitKey);
}
function* subscriptionsForDiffs(subscriptions, diffs) {
  for (const subscription of subscriptions) {
    if (subscription.matches(diffs)) {
      yield subscription;
    }
  }
}
function watcherMatchesDiff(diff, prefix, indexName) {
  if (prefix === "") {
    return true;
  }
  const compareKey = indexName ? (diffOp) => decodeIndexKey(diffOp.key)[0] : (diffOp) => diffOp.key;
  const i = diffBinarySearch(diff, prefix, compareKey);
  return i < diff.length && compareKey(diff[i]).startsWith(prefix);
}
function diffBinarySearch(diff, prefix, compareKey) {
  return binarySearch(
    diff.length,
    (i) => compareUTF8(prefix, compareKey(diff[i]))
  );
}
export {
  SubscriptionImpl,
  SubscriptionImpl as SubscriptionImplForTesting,
  SubscriptionsManagerImpl,
  WatchSubscription,
  diffBinarySearch,
  scanInfoMatchesKey
};
//# sourceMappingURL=subscriptions.js.map
