import { Lock } from "@rocicorp/lock";
import { assert } from "../../../shared/src/asserts.js";
import { getSizeOfEntry } from "../../../shared/src/size-of-value.js";
import { toRefs } from "../dag/chunk.js";
import { emptyHash, newRandomHash } from "../hash.js";
import { InternalNodeImpl, DataNodeImpl, newNodeImpl, partition, createNewInternalEntryForNode, emptyDataNode, isDataNodeImpl, toChunkData } from "./node.js";
import { BTreeRead } from "./read.js";
class BTreeWrite extends BTreeRead {
  /**
   * This rw lock is used to ensure we do not mutate the btree in parallel. It
   * would be a problem if we didn't have the lock in cases like this:
   *
   * ```ts
   * const p1 = tree.put('a', 0);
   * const p2 = tree.put('b', 1);
   * await p1;
   * await p2;
   * ```
   *
   * because both `p1` and `p2` would start from the old root hash but a put
   * changes the root hash so the two concurrent puts would lead to only one of
   * them actually working, and it is not deterministic which one would finish
   * last.
   */
  #lock = new Lock();
  #modified = /* @__PURE__ */ new Map();
  minSize;
  maxSize;
  constructor(dagWrite, formatVersion, root = emptyHash, minSize = 8 * 1024, maxSize = 16 * 1024, getEntrySize = getSizeOfEntry, chunkHeaderSize) {
    super(dagWrite, formatVersion, root, getEntrySize, chunkHeaderSize);
    this.minSize = minSize;
    this.maxSize = maxSize;
  }
  #addToModified(node) {
    assert(node.isMutable);
    this.#modified.set(node.hash, node);
    this._cache.set(node.hash, node);
  }
  updateNode(node) {
    assert(node.isMutable);
    this.#modified.delete(node.hash);
    node.hash = newRandomHash();
    this.#addToModified(node);
  }
  newInternalNodeImpl(entries, level) {
    const n = new InternalNodeImpl(entries, newRandomHash(), level, true);
    this.#addToModified(n);
    return n;
  }
  newDataNodeImpl(entries) {
    const n = new DataNodeImpl(entries, newRandomHash(), true);
    this.#addToModified(n);
    return n;
  }
  newNodeImpl(entries, level) {
    const n = newNodeImpl(entries, newRandomHash(), level, true);
    this.#addToModified(n);
    return n;
  }
  put(key, value) {
    return this.#lock.withLock(async () => {
      const oldRootNode = await this.getNode(this.rootHash);
      const entrySize = this.getEntrySize(key, value);
      const rootNode = await oldRootNode.set(key, value, entrySize, this);
      if (rootNode.getChildNodeSize(this) > this.maxSize) {
        const headerSize = this.chunkHeaderSize;
        const partitions = partition(
          rootNode.entries,
          (value2) => value2[2],
          this.minSize - headerSize,
          this.maxSize - headerSize
        );
        const { level } = rootNode;
        const entries = partitions.map((entries2) => {
          const node = this.newNodeImpl(entries2, level);
          return createNewInternalEntryForNode(node, this.getEntrySize);
        });
        const newRoot = this.newInternalNodeImpl(entries, level + 1);
        this.rootHash = newRoot.hash;
        return;
      }
      this.rootHash = rootNode.hash;
    });
  }
  del(key) {
    return this.#lock.withLock(async () => {
      const oldRootNode = await this.getNode(this.rootHash);
      const newRootNode = await oldRootNode.del(key, this);
      const found = this.rootHash !== newRootNode.hash;
      if (found) {
        if (newRootNode.level > 0 && newRootNode.entries.length === 1) {
          this.rootHash = newRootNode.entries[0][1];
        } else {
          this.rootHash = newRootNode.hash;
        }
      }
      return found;
    });
  }
  clear() {
    return this.#lock.withLock(() => {
      this.#modified.clear();
      this.rootHash = emptyHash;
    });
  }
  flush() {
    return this.#lock.withLock(async () => {
      const dagWrite = this._dagRead;
      if (this.rootHash === emptyHash) {
        const chunk = dagWrite.createChunk(emptyDataNode, []);
        await dagWrite.putChunk(chunk);
        return chunk.hash;
      }
      const newChunks = [];
      const newRoot = gatherNewChunks(
        this.rootHash,
        newChunks,
        dagWrite.createChunk,
        this.#modified,
        this._formatVersion
      );
      await Promise.all(newChunks.map((chunk) => dagWrite.putChunk(chunk)));
      this.#modified.clear();
      this.rootHash = newRoot;
      return newRoot;
    });
  }
}
function gatherNewChunks(hash, newChunks, createChunk, modified, formatVersion) {
  const node = modified.get(hash);
  if (node === void 0) {
    return hash;
  }
  if (isDataNodeImpl(node)) {
    const chunk2 = createChunk(toChunkData(node, formatVersion), []);
    newChunks.push(chunk2);
    return chunk2.hash;
  }
  const refs = [];
  const { entries } = node;
  for (let i = 0; i < entries.length; i++) {
    const entry = entries[i];
    const childHash = entry[1];
    const newChildHash = gatherNewChunks(
      childHash,
      newChunks,
      createChunk,
      modified,
      formatVersion
    );
    if (newChildHash !== childHash) {
      entries[i] = [entry[0], newChildHash, entry[2]];
    }
    refs.push(newChildHash);
  }
  const chunk = createChunk(toChunkData(node, formatVersion), toRefs(refs));
  newChunks.push(chunk);
  return chunk.hash;
}
export {
  BTreeWrite
};
//# sourceMappingURL=write.js.map
