import { areEqual } from "../../../shared/src/arrays.js";
import { assert, unreachable } from "../../../shared/src/asserts.js";
import { normalizeUndefined } from "./data.js";
import { throwFilterOutput } from "./filter-operators.js";
class Exists {
  #input;
  #relationshipName;
  #not;
  #parentJoinKey;
  #noSizeReuse;
  #cache;
  #cacheHitCountsForTesting;
  #output = throwFilterOutput;
  /**
   * This instance variable is `true` when this operator is processing a `push`,
   * and is used to disable reuse of cached sizes across rows with the
   * same parent join key value.
   * This is necessary because during a push relationships can be inconsistent
   * due to push communicating changes (which may change multiple Nodes) one
   * Node at a time.
   */
  #inPush = false;
  constructor(input, relationshipName, parentJoinKey, type, cacheHitCountsForTesting) {
    this.#input = input;
    this.#relationshipName = relationshipName;
    this.#input.setFilterOutput(this);
    this.#cache = /* @__PURE__ */ new Map();
    this.#cacheHitCountsForTesting = cacheHitCountsForTesting;
    assert(
      this.#input.getSchema().relationships[relationshipName],
      `Input schema missing ${relationshipName}`
    );
    this.#not = type === "NOT EXISTS";
    this.#parentJoinKey = parentJoinKey;
    this.#noSizeReuse = areEqual(
      parentJoinKey,
      this.#input.getSchema().primaryKey
    );
  }
  setFilterOutput(output) {
    this.#output = output;
  }
  beginFilter() {
    this.#output.beginFilter();
  }
  endFilter() {
    this.#cache = /* @__PURE__ */ new Map();
    this.#output.endFilter();
  }
  *filter(node) {
    let exists;
    if (!this.#noSizeReuse && !this.#inPush) {
      const key = this.#getCacheKey(node, this.#parentJoinKey);
      exists = this.#cache.get(key);
      if (exists === void 0) {
        exists = yield* this.#fetchExists(node);
        this.#cache.set(key, exists);
      } else if (this.#cacheHitCountsForTesting) {
        this.#cacheHitCountsForTesting.set(
          key,
          (this.#cacheHitCountsForTesting.get(key) ?? 0) + 1
        );
      }
    }
    const result = (yield* this.#filter(node, exists)) && (yield* this.#output.filter(node));
    return result;
  }
  destroy() {
    this.#input.destroy();
  }
  getSchema() {
    return this.#input.getSchema();
  }
  *push(change) {
    assert(!this.#inPush, "Unexpected re-entrancy");
    this.#inPush = true;
    try {
      switch (change.type) {
        // add, remove and edit cannot change the size of the
        // this.#relationshipName relationship, so simply #pushWithFilter
        case "add":
        case "edit":
        case "remove": {
          yield* this.#pushWithFilter(change);
          return;
        }
        case "child":
          if (change.child.relationshipName !== this.#relationshipName || change.child.change.type === "edit" || change.child.change.type === "child") {
            yield* this.#pushWithFilter(change);
            return;
          }
          switch (change.child.change.type) {
            case "add": {
              const size = yield* this.#fetchSize(change.node);
              if (size === 1) {
                if (this.#not) {
                  yield* this.#output.push(
                    {
                      type: "remove",
                      node: {
                        row: change.node.row,
                        relationships: {
                          ...change.node.relationships,
                          [this.#relationshipName]: () => []
                        }
                      }
                    },
                    this
                  );
                } else {
                  yield* this.#output.push(
                    {
                      type: "add",
                      node: change.node
                    },
                    this
                  );
                }
              } else {
                yield* this.#pushWithFilter(change, size > 0);
              }
              return;
            }
            case "remove": {
              const size = yield* this.#fetchSize(change.node);
              if (size === 0) {
                if (this.#not) {
                  yield* this.#output.push(
                    {
                      type: "add",
                      node: change.node
                    },
                    this
                  );
                } else {
                  yield* this.#output.push(
                    {
                      type: "remove",
                      node: {
                        row: change.node.row,
                        relationships: {
                          ...change.node.relationships,
                          [this.#relationshipName]: () => [
                            change.child.change.node
                          ]
                        }
                      }
                    },
                    this
                  );
                }
              } else {
                yield* this.#pushWithFilter(change, size > 0);
              }
              return;
            }
          }
          return;
        default:
          unreachable(change);
      }
    } finally {
      this.#inPush = false;
    }
  }
  /**
   * Returns whether or not the node's this.#relationshipName
   * relationship passes the exist/not exists filter condition.
   * If the optional `size` is passed it is used.
   * Otherwise, if there is a stored size for the row it is used.
   * Otherwise the size is computed by streaming the node's
   * relationship with this.#relationshipName (this computed size is also
   * stored).
   */
  *#filter(node, exists) {
    exists = exists ?? (yield* this.#fetchExists(node));
    return this.#not ? !exists : exists;
  }
  #getCacheKey(node, def) {
    const values = [];
    for (const key of def) {
      values.push(normalizeUndefined(node.row[key]));
    }
    return JSON.stringify(values);
  }
  /**
   * Pushes a change if this.#filter is true for its row.
   */
  *#pushWithFilter(change, exists) {
    if (yield* this.#filter(change.node, exists)) {
      yield* this.#output.push(change, this);
    }
  }
  *#fetchExists(node) {
    return (yield* this.#fetchSize(node)) > 0;
  }
  *#fetchSize(node) {
    const relationship = node.relationships[this.#relationshipName];
    assert(
      relationship,
      () => `Exists: relationship "${this.#relationshipName}" not found on node`
    );
    let size = 0;
    for (const n of relationship()) {
      if (n === "yield") {
        yield "yield";
      } else {
        size++;
      }
    }
    return size;
  }
}
export {
  Exists
};
//# sourceMappingURL=exists.js.map
