/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  Bytes,
  ChannelType,
  createPromise,
  Diagnostic,
  Duration,
  Logger,
  Millis,
  Seconds,
  Time,
  Timespan
} from "@matter/general";
import { VendorId } from "@matter/types";
import { BleError } from "../ble/Ble.js";
import { BtpCodec } from "../codec/BtpCodec.js";
const logger = Logger.get("BleScanner");
class BleScanner {
  type = ChannelType.BLE;
  #client;
  #recordWaiters = /* @__PURE__ */ new Map();
  #discoveredMatterDevices = /* @__PURE__ */ new Map();
  constructor(client) {
    this.#client = client;
    this.#client.setDiscoveryCallback(
      (peripheral, manufacturerData) => this.#handleDiscoveredDevice(peripheral, manufacturerData)
    );
  }
  getDiscoveredDevice(address) {
    const device = this.#discoveredMatterDevices.get(address);
    if (device === void 0) {
      throw new BleError(`No device found for address ${address}`);
    }
    return device;
  }
  /**
   * Registers a deferred promise for a specific queryId together with a timeout and return the promise.
   * The promise will be resolved when the timer runs out latest.
   */
  async #registerWaiterPromise(queryId, timeout, resolveOnUpdatedRecords = true, cancelResolver) {
    const { promise, resolver } = createPromise();
    let timer;
    if (timeout) {
      timer = Time.getTimer("BLE query timeout", timeout, () => {
        cancelResolver?.();
        this.#finishWaiter(queryId, true);
      }).start();
    }
    this.#recordWaiters.set(queryId, { resolver, timer, resolveOnUpdatedRecords, cancelResolver });
    logger.debug(
      `Registered waiter for query ${queryId} with timeout ${timeout === void 0 ? "(none)" : Duration.format(timeout)} ${resolveOnUpdatedRecords ? "" : " (not resolving on updated records)"}`
    );
    await promise;
  }
  /**
   * Remove a waiter promise for a specific queryId and stop the connected timer. If required also resolve the
   * promise.
   */
  #finishWaiter(queryId, resolvePromise, isUpdatedRecord = false) {
    const waiter = this.#recordWaiters.get(queryId);
    if (waiter === void 0) return;
    const { timer, resolver, resolveOnUpdatedRecords } = waiter;
    if (isUpdatedRecord && !resolveOnUpdatedRecords) return;
    logger.debug(`Finishing waiter for query ${queryId}, resolving: ${resolvePromise}`);
    timer?.stop();
    if (resolvePromise) {
      resolver();
    }
    this.#recordWaiters.delete(queryId);
  }
  cancelCommissionableDeviceDiscovery(identifier, resolvePromise = true) {
    const queryKey = this.#buildCommissionableQueryIdentifier(identifier);
    if (queryKey === void 0) return;
    const { cancelResolver } = this.#recordWaiters.get(queryKey) ?? {};
    cancelResolver?.();
    this.#finishWaiter(queryKey, resolvePromise);
  }
  #handleDiscoveredDevice(peripheral, manufacturerServiceData) {
    const address = peripheral.address;
    try {
      const { discriminator, vendorId, productId, hasAdditionalAdvertisementData } = BtpCodec.decodeBleAdvertisementServiceData(manufacturerServiceData);
      const deviceData = {
        deviceIdentifier: address,
        D: discriminator,
        SD: discriminator >> 8 & 15,
        VP: `${vendorId}+${productId}`,
        CM: 1,
        // Can be no other mode,
        addresses: [{ type: "ble", peripheralAddress: address }]
      };
      const deviceExisting = this.#discoveredMatterDevices.has(address);
      logger.debug(
        `${deviceExisting ? "Re-" : ""}Discovered device ${address} data: ${Diagnostic.json(deviceData)}`
      );
      this.#discoveredMatterDevices.set(address, {
        deviceData,
        peripheral,
        hasAdditionalAdvertisementData
      });
      const queryKey = this.#findCommissionableQueryIdentifier(deviceData);
      if (queryKey !== void 0) {
        this.#finishWaiter(queryKey, true, deviceExisting);
      }
    } catch (error) {
      logger.debug(
        `Discovered device ${address} ${manufacturerServiceData === void 0 ? void 0 : Bytes.toHex(manufacturerServiceData)} does not seem to be a valid Matter device: ${error}`
      );
    }
  }
  #findCommissionableQueryIdentifier(record) {
    const longDiscriminatorQueryId = this.#buildCommissionableQueryIdentifier({ longDiscriminator: record.D });
    if (longDiscriminatorQueryId !== void 0 && this.#recordWaiters.has(longDiscriminatorQueryId)) {
      return longDiscriminatorQueryId;
    }
    const shortDiscriminatorQueryId = this.#buildCommissionableQueryIdentifier({ shortDiscriminator: record.SD });
    if (shortDiscriminatorQueryId !== void 0 && this.#recordWaiters.has(shortDiscriminatorQueryId)) {
      return shortDiscriminatorQueryId;
    }
    if (record.VP !== void 0) {
      const vpParts = record.VP.split("+");
      const vendorId = VendorId(parseInt(vpParts[0]));
      const productId = vpParts[1] !== void 0 ? parseInt(vpParts[1]) : void 0;
      if (productId !== void 0) {
        const vendorProductQueryId = this.#buildCommissionableQueryIdentifier({
          vendorId,
          productId
        });
        if (vendorProductQueryId !== void 0 && this.#recordWaiters.has(vendorProductQueryId)) {
          return vendorProductQueryId;
        }
      }
      const vendorIdQueryId = this.#buildCommissionableQueryIdentifier({ vendorId });
      if (vendorIdQueryId !== void 0 && this.#recordWaiters.has(vendorIdQueryId)) {
        return vendorIdQueryId;
      }
      if (productId !== void 0) {
        const productIdQueryId = this.#buildCommissionableQueryIdentifier({ productId });
        if (productIdQueryId !== void 0 && this.#recordWaiters.has(productIdQueryId)) {
          return productIdQueryId;
        }
      }
    }
    if (this.#recordWaiters.has("*")) {
      return "*";
    }
    return void 0;
  }
  /**
   * Builds an identifier string for commissionable queries based on the given identifier object.
   * Some identifiers are identical to the official DNS-SD identifiers, others are custom.
   */
  #buildCommissionableQueryIdentifier(identifier) {
    if ("instanceId" in identifier) {
      return void 0;
    } else if ("longDiscriminator" in identifier) {
      return `D:${identifier.longDiscriminator}`;
    } else if ("shortDiscriminator" in identifier) {
      return `SD:${identifier.shortDiscriminator}`;
    } else if ("vendorId" in identifier && "productId" in identifier) {
      return `VP:${identifier.vendorId}+${identifier.productId}`;
    } else if ("vendorId" in identifier) {
      return `V:${identifier.vendorId}`;
    } else if ("deviceType" in identifier) {
      return void 0;
    } else if ("productId" in identifier) {
      return `P:${identifier.productId}`;
    } else return "*";
  }
  #getCommissionableDevices(identifier) {
    const storedRecords = Array.from(this.#discoveredMatterDevices.values());
    const foundRecords = new Array();
    if ("instanceId" in identifier || "deviceType" in identifier) {
      return foundRecords;
    } else if ("longDiscriminator" in identifier) {
      foundRecords.push(...storedRecords.filter(({ deviceData: { D } }) => D === identifier.longDiscriminator));
    } else if ("shortDiscriminator" in identifier) {
      foundRecords.push(
        ...storedRecords.filter(({ deviceData: { SD } }) => SD === identifier.shortDiscriminator)
      );
    } else if ("vendorId" in identifier && "productId" in identifier) {
      foundRecords.push(
        ...storedRecords.filter(
          ({ deviceData: { VP } }) => VP === `${identifier.vendorId}+${identifier.productId}`
        )
      );
    } else if ("vendorId" in identifier) {
      foundRecords.push(
        ...storedRecords.filter(
          ({ deviceData: { VP } }) => VP === `${identifier.vendorId}` || VP?.startsWith(`${identifier.vendorId}+`)
        )
      );
    } else if ("productId" in identifier) {
      foundRecords.push(
        ...storedRecords.filter(({ deviceData: { VP } }) => VP?.endsWith(`+${identifier.productId}`))
      );
    } else {
      foundRecords.push(...storedRecords.filter(({ deviceData: { CM } }) => CM === 1 || CM === 2));
    }
    return foundRecords;
  }
  async findCommissionableDevices(identifier, timeout = Seconds(10), ignoreExistingRecords = false) {
    const queryKey = this.#buildCommissionableQueryIdentifier(identifier);
    if (queryKey === void 0) {
      return [];
    }
    let storedRecords = this.#getCommissionableDevices(identifier);
    if (ignoreExistingRecords) {
      for (const record of storedRecords) {
        this.#discoveredMatterDevices.delete(record.peripheral.address);
      }
      storedRecords = [];
    }
    if (storedRecords.length === 0) {
      await this.#client.startScanning();
      await this.#registerWaiterPromise(queryKey, timeout);
      storedRecords = this.#getCommissionableDevices(identifier);
      await this.#client.stopScanning();
    }
    return storedRecords.map(({ deviceData }) => deviceData);
  }
  async findCommissionableDevicesContinuously(identifier, callback, timeout, cancelSignal) {
    const queryKey = this.#buildCommissionableQueryIdentifier(identifier);
    if (queryKey === void 0) {
      return [];
    }
    const discoveredDevices = /* @__PURE__ */ new Set();
    const discoveryEndTime = timeout ? Time.nowMs + timeout : void 0;
    await this.#client.startScanning();
    let queryResolver;
    if (cancelSignal === void 0) {
      const { promise, resolver } = createPromise();
      cancelSignal = promise;
      queryResolver = resolver;
    }
    let canceled = false;
    cancelSignal?.then(
      () => {
        canceled = true;
        this.#finishWaiter(queryKey, true);
      },
      (cause) => {
        logger.error("Unexpected error canceling commissioning", cause);
      }
    );
    while (!canceled) {
      this.#getCommissionableDevices(identifier).forEach(({ deviceData }) => {
        const { deviceIdentifier } = deviceData;
        if (!discoveredDevices.has(deviceIdentifier)) {
          discoveredDevices.add(deviceIdentifier);
          callback(deviceData);
        }
      });
      let remainingTime;
      if (discoveryEndTime !== void 0) {
        remainingTime = Millis.ceil(Timespan(Time.nowMs, discoveryEndTime).duration);
        if (remainingTime <= 0) {
          break;
        }
      }
      await this.#registerWaiterPromise(queryKey, remainingTime, false, queryResolver);
    }
    await this.#client.stopScanning();
    return this.#getCommissionableDevices(identifier).map(({ deviceData }) => deviceData);
  }
  getDiscoveredCommissionableDevices(identifier) {
    return this.#getCommissionableDevices(identifier).map(({ deviceData }) => deviceData);
  }
  closeClient() {
    return this.#client.stopScanning();
  }
  async close() {
    await this.closeClient();
    [...this.#recordWaiters.keys()].forEach(
      (queryId) => this.#finishWaiter(queryId, !!this.#recordWaiters.get(queryId)?.timer)
    );
  }
}
export {
  BleScanner
};
//# sourceMappingURL=BleScanner.js.map
