/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Bytes } from "#util/Bytes.js";
const DNS_HEADER_SIZE = 12;
const FOOTPRINT_STOPLIST = /* @__PURE__ */ new Set(["_tcp", "_udp", "local", "_sub"]);
const FNV_OFFSET = 2166136261;
const FNV_PRIME = 16777619;
const MAX_NAME_POINTERS = 128;
function foldAsciiByte(byte) {
  return byte >= 65 && byte <= 90 ? byte + 32 : byte;
}
function hashLabelBytes(source, start, length) {
  let hash = FNV_OFFSET;
  for (let i = 0; i < length; i++) {
    hash = Math.imul(hash ^ foldAsciiByte(source[start + i]), FNV_PRIME);
  }
  return hash >>> 0;
}
function footprintHash(name) {
  const labels = name.toLowerCase().split(".");
  for (let i = labels.length - 1; i >= 0; i--) {
    const label = labels[i];
    if (label.length === 0 || FOOTPRINT_STOPLIST.has(label)) {
      continue;
    }
    let hash = FNV_OFFSET;
    for (let j = 0; j < label.length; j++) {
      hash = Math.imul(hash ^ foldAsciiByte(label.charCodeAt(j)), FNV_PRIME);
    }
    return hash >>> 0;
  }
  return void 0;
}
class MdnsRelevanceFilter {
  // Per-owner footprint hashes (a multiset, so a name folding to an already-present label is reference-counted) or
  // the "all" sentinel. #footprints is the live union across owners (hash -> reference count).
  #owners = /* @__PURE__ */ new Map();
  #footprints = /* @__PURE__ */ new Map();
  #allOwnerCount = 0;
  /**
   * Declare (replacing any prior declaration) the full set of DNS names an owner cares about.
   *
   * **`"all"` disables the pre-filter for the ENTIRE socket, not just this owner.** Because the socket cannot know
   * which packets an unenumerated owner wants, a single `"all"` forces every inbound packet to be fully decoded
   * again — i.e. it reverts the whole optimization to "decode everything" for every consumer sharing this socket.
   * Use it only when an owner genuinely cannot enumerate its service types, and prefer listing concrete names.
   * (When no owner has registered, or every registered name folds away, the gate is likewise inactive.) Any
   * component consuming the socket's receipt must register here (or `"all"`) or another owner's footprints could
   * filter its packets away.
   *
   * For owners whose interest changes one name at a time (e.g. discovered hostnames), prefer {@link addRelevantName}
   * / {@link removeRelevantName} which update in O(1) instead of rehashing the whole set.
   */
  registerRelevantNames(owner, names) {
    this.#removeOwner(owner);
    if (names === "all") {
      this.#owners.set(owner, "all");
      this.#allOwnerCount++;
      return;
    }
    const counts = /* @__PURE__ */ new Map();
    for (const name of names) {
      const hash = footprintHash(name);
      if (hash === void 0) {
        continue;
      }
      counts.set(hash, (counts.get(hash) ?? 0) + 1);
    }
    this.#owners.set(owner, counts);
    for (const [hash, count] of counts) {
      this.#footprints.set(hash, (this.#footprints.get(hash) ?? 0) + count);
    }
  }
  /** Add a single name to an owner's footprints in O(1). No-op if the owner is registered as `"all"`. */
  addRelevantName(owner, name) {
    const hash = footprintHash(name);
    if (hash === void 0) {
      return;
    }
    let counts = this.#owners.get(owner);
    if (counts === "all") {
      return;
    }
    if (counts === void 0) {
      counts = /* @__PURE__ */ new Map();
      this.#owners.set(owner, counts);
    }
    counts.set(hash, (counts.get(hash) ?? 0) + 1);
    this.#footprints.set(hash, (this.#footprints.get(hash) ?? 0) + 1);
  }
  /** Remove a single previously-added name from an owner's footprints in O(1). */
  removeRelevantName(owner, name) {
    const hash = footprintHash(name);
    if (hash === void 0) {
      return;
    }
    const counts = this.#owners.get(owner);
    if (counts === void 0 || counts === "all") {
      return;
    }
    const ownerCount = counts.get(hash);
    if (ownerCount === void 0) {
      return;
    }
    if (ownerCount <= 1) {
      counts.delete(hash);
    } else {
      counts.set(hash, ownerCount - 1);
    }
    this.#decrement(hash);
  }
  unregisterRelevantNames(owner) {
    this.#removeOwner(owner);
  }
  /**
   * Scan a raw packet for a registered footprint, deciding whether it is worth a full decode. Walks the DNS name
   * structure only — header counts then each question/RR name — skipping RDATA via `rdlength`, and follows
   * compression pointers so every label is visited regardless of how the packet encoded the name. Returns true
   * (keep) on any structural uncertainty so the decoder makes the authoritative call.
   */
  isRelevant(message) {
    if (this.#allOwnerCount > 0 || this.#footprints.size === 0) {
      return true;
    }
    const footprints = this.#footprints;
    const bytes = Bytes.of(message);
    const length = bytes.length;
    if (length < DNS_HEADER_SIZE) {
      return true;
    }
    const questionCount = bytes[4] << 8 | bytes[5];
    const recordCount = questionCount + (bytes[6] << 8 | bytes[7]) + (bytes[8] << 8 | bytes[9]) + (bytes[10] << 8 | bytes[11]);
    let offset = DNS_HEADER_SIZE;
    for (let record = 0; record < recordCount; record++) {
      let cursor = offset;
      let sequenceEnd = -1;
      let hops = 0;
      for (; ; ) {
        if (cursor >= length) {
          return true;
        }
        const labelLength = bytes[cursor];
        if (labelLength === 0) {
          if (sequenceEnd < 0) {
            sequenceEnd = cursor + 1;
          }
          break;
        }
        if ((labelLength & 192) === 192) {
          if (cursor + 1 >= length) {
            return true;
          }
          const pointer = (labelLength & 63) << 8 | bytes[cursor + 1];
          if (sequenceEnd < 0) {
            sequenceEnd = cursor + 2;
          }
          if (pointer < DNS_HEADER_SIZE || pointer >= cursor || ++hops > MAX_NAME_POINTERS) {
            return true;
          }
          cursor = pointer;
          continue;
        }
        if ((labelLength & 192) !== 0) {
          return true;
        }
        cursor++;
        if (cursor + labelLength > length) {
          return true;
        }
        if (footprints.has(hashLabelBytes(bytes, cursor, labelLength))) {
          return true;
        }
        cursor += labelLength;
      }
      offset = sequenceEnd;
      if (record < questionCount) {
        if (offset + 4 > length) {
          return true;
        }
        offset += 4;
      } else {
        if (offset + 10 > length) {
          return true;
        }
        const rdlength = bytes[offset + 8] << 8 | bytes[offset + 9];
        if (offset + 10 + rdlength > length) {
          return true;
        }
        offset += 10 + rdlength;
      }
    }
    return false;
  }
  #removeOwner(owner) {
    const entry = this.#owners.get(owner);
    if (entry === void 0) {
      return;
    }
    this.#owners.delete(owner);
    if (entry === "all") {
      this.#allOwnerCount--;
      return;
    }
    for (const [hash, count] of entry) {
      const current = this.#footprints.get(hash);
      if (current === void 0) {
        continue;
      }
      if (current <= count) {
        this.#footprints.delete(hash);
      } else {
        this.#footprints.set(hash, current - count);
      }
    }
  }
  #decrement(hash) {
    const current = this.#footprints.get(hash);
    if (current === void 0) {
      return;
    }
    if (current <= 1) {
      this.#footprints.delete(hash);
    } else {
      this.#footprints.set(hash, current - 1);
    }
  }
}
export {
  MdnsRelevanceFilter
};
//# sourceMappingURL=MdnsRelevanceFilter.js.map
