import { assert, assertNumber } from "../../../shared/src/asserts.js";
import { isProd } from "../../../shared/src/config.js";
import { emptyHash } from "../hash.js";
function computeRefCountUpdates(headChanges, putChunks, delegate) {
  return new RefCountUpdates(headChanges, putChunks, delegate).compute();
}
class RefCountUpdates {
  #newHeads;
  #oldHeads;
  #putChunks;
  #delegate;
  #refsCounted;
  #refCountUpdates;
  #loadedRefCountPromises;
  #isLazyDelegate;
  constructor(headChanges, putChunks, delegate) {
    const newHeads = [];
    const oldHeads = [];
    for (const changedHead of headChanges) {
      if (changedHead.old !== changedHead.new) {
        changedHead.old && oldHeads.push(changedHead.old);
        changedHead.new && newHeads.push(changedHead.new);
      }
    }
    this.#newHeads = newHeads;
    this.#oldHeads = oldHeads;
    this.#putChunks = putChunks;
    this.#delegate = delegate;
    this.#refCountUpdates = /* @__PURE__ */ new Map();
    this.#loadedRefCountPromises = /* @__PURE__ */ new Map();
    this.#isLazyDelegate = delegate.areRefsCounted !== void 0;
    this.#refsCounted = this.#isLazyDelegate ? /* @__PURE__ */ new Set() : null;
  }
  async compute() {
    for (const n of this.#newHeads) {
      await this.#changeRefCount(n, 1);
    }
    await Promise.all(
      Array.from(
        this.#putChunks.values(),
        (hash) => this.#ensureRefCountLoaded(hash)
      )
    );
    if (this.#isLazyDelegate) {
      assert(this.#delegate.areRefsCounted);
      assert(this.#refsCounted);
      let refCountsUpdated;
      do {
        refCountsUpdated = false;
        for (const hash of this.#putChunks.values()) {
          if (!this.#delegate.areRefsCounted(hash) && !this.#refsCounted.has(hash) && this.#refCountUpdates.get(hash) !== 0) {
            await this.#updateRefsCounts(hash, 1);
            refCountsUpdated = true;
            break;
          }
        }
      } while (refCountsUpdated);
    }
    for (const o of this.#oldHeads) {
      await this.#changeRefCount(o, -1);
    }
    if (!isProd) {
      for (const [hash, update] of this.#refCountUpdates) {
        assert(
          update >= 0,
          `ref count update must be non-negative. ${hash}:${update}`
        );
      }
    }
    return this.#refCountUpdates;
  }
  async #changeRefCount(hash, delta) {
    await this.#ensureRefCountLoaded(hash);
    if (this.#updateRefCount(hash, delta)) {
      await this.#updateRefsCounts(hash, delta);
    }
  }
  async #updateRefsCounts(hash, delta) {
    if (hash === emptyHash) {
      return;
    }
    const refs = await this.#delegate.getRefs(hash);
    if (!isProd) {
      assert(
        refs || this.#isLazyDelegate && !this.#putChunks.has(hash),
        "refs must be defined"
      );
    }
    if (refs !== void 0) {
      this.#refsCounted?.add(hash);
      const ps = refs.map((ref) => this.#changeRefCount(ref, delta));
      await Promise.all(ps);
    }
  }
  #ensureRefCountLoaded(hash) {
    let p = this.#loadedRefCountPromises.get(hash);
    if (p === void 0) {
      p = (async () => {
        const value = await this.#delegate.getRefCount(hash) || 0;
        this.#refCountUpdates.set(hash, value);
        return value;
      })();
      this.#loadedRefCountPromises.set(hash, p);
    }
    return p;
  }
  #updateRefCount(hash, delta) {
    const oldCount = this.#refCountUpdates.get(hash);
    assertNumber(oldCount);
    this.#refCountUpdates.set(hash, oldCount + delta);
    return oldCount === 0 && delta === 1 || oldCount === 1 && delta === -1;
  }
}
export {
  computeRefCountUpdates
};
//# sourceMappingURL=gc.js.map
