"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var BlenoBleServer_exports = {};
__export(BlenoBleServer_exports, {
  BlenoBleServer: () => BlenoBleServer
});
module.exports = __toCommonJS(BlenoBleServer_exports);
var import_general = require("#general");
var import_protocol = require("#protocol");
var import_require = require("@matter/nodejs-ble/require");
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
const logger = import_general.Logger.get("BlenoBleServer");
let Bleno;
function initializeBleno(server, hciId) {
  if (hciId !== void 0) {
    process.env.BLENO_HCI_DEVICE_ID = hciId.toString();
  }
  Bleno = (0, import_require.require)("@stoprocent/bleno");
  class BtpWriteCharacteristicC1 extends Bleno.Characteristic {
    constructor() {
      super({
        uuid: import_protocol.BLE_MATTER_C1_CHARACTERISTIC_UUID,
        properties: ["write"],
        onWriteRequest: (_handle, data, offset, withoutResponse, callback) => this.#onWriteRequest(data, offset, withoutResponse, callback)
      });
    }
    #onWriteRequest(data, offset, withoutResponse, callback) {
      logger.debug(`C1 write request: ${data.toString("hex")} ${offset} ${withoutResponse}`);
      try {
        server.handleC1WriteRequest(data, offset, withoutResponse);
        callback(this.RESULT_SUCCESS);
      } catch (e) {
        logger.error(`C1 write request failed: ${e}`);
        callback(this.RESULT_UNLIKELY_ERROR);
      }
    }
  }
  class BtpIndicateCharacteristicC2 extends Bleno.Characteristic {
    constructor() {
      super({
        uuid: import_protocol.BLE_MATTER_C2_CHARACTERISTIC_UUID,
        properties: ["indicate"],
        onSubscribe: (_handle, maxValueSize, updateValueCallback) => this.#onSubscribe(maxValueSize, updateValueCallback),
        onUnsubscribe: () => this.#onUnsubscribe(),
        onIndicate: () => this.#onIndicate()
      });
    }
    #onSubscribe(maxValueSize, updateValueCallback) {
      logger.debug(`C2 subscribe ${maxValueSize}`);
      server.handleC2SubscribeRequest(maxValueSize, updateValueCallback).catch((e) => logger.warn("Error happened in when handling C2 subscribe", e));
    }
    #onUnsubscribe() {
      logger.debug("C2 unsubscribe");
      server.close().catch((e) => logger.warn("Error happened when closing server for C2 unsubscribe", e));
    }
    #onIndicate() {
      logger.debug("C2 indicate");
      server.handleC2Indicate();
    }
  }
  class BtpReadCharacteristicC3 extends Bleno.Characteristic {
    constructor() {
      super({
        uuid: import_protocol.BLE_MATTER_C3_CHARACTERISTIC_UUID,
        properties: ["read"],
        onReadRequest: (_handle, offset, callback) => this.#onReadRequest(offset, callback)
      });
    }
    #onReadRequest(offset, callback) {
      try {
        const data = server.handleC3ReadRequest(offset);
        logger.debug(`C3 read request: ${data.toString("hex")} ${offset}`);
        callback(this.RESULT_SUCCESS, data);
      } catch (e) {
        logger.debug(`C3 read request failed : ${e} ${offset}`);
        callback(this.RESULT_INVALID_OFFSET);
      }
    }
  }
  class BtpService extends Bleno.PrimaryService {
    constructor() {
      super({
        uuid: import_protocol.BLE_MATTER_SERVICE_UUID_SHORT,
        characteristics: [
          new BtpWriteCharacteristicC1(),
          new BtpIndicateCharacteristicC2(),
          new BtpReadCharacteristicC3()
        ]
      });
    }
  }
  return new BtpService();
}
class BlenoBleServer extends import_protocol.BleChannel {
  state = "unknown";
  isAdvertising = false;
  additionalAdvertisingData = Buffer.alloc(0);
  advertisingData;
  latestHandshakePayload;
  btpSession;
  onMatterMessageListener;
  writeConformationResolver;
  clientAddress;
  btpHandshakeTimeout = import_general.Time.getTimer(
    "BTP handshake timeout",
    import_protocol.BTP_CONN_RSP_TIMEOUT,
    () => this.btpHandshakeTimeoutTriggered()
  );
  #disconnected = false;
  #closing = false;
  matterBleService;
  constructor(options) {
    super();
    const { environment } = options ?? {};
    environment?.runtime.add(this);
    this.matterBleService = initializeBleno(this, options?.hciId);
    Bleno.on("stateChange", (state) => {
      if (state === this.state) return;
      this.state = state;
      logger.debug(`stateChange: ${state}, address = ${Bleno.address}`);
      if (state !== "poweredOn") {
        if (this.#disconnected || this.#closing) return;
        Bleno.stopAdvertising();
      } else if (this.advertisingData) {
        Bleno.startAdvertisingWithEIRData(this.advertisingData);
        this.isAdvertising = true;
      }
    });
    Bleno.on("accept", (clientAddress) => {
      logger.debug(`accept new connection, client: ${clientAddress}`);
      this.clientAddress = clientAddress;
      Bleno.updateRssi();
    });
    Bleno.on("disconnect", (clientAddress) => {
      logger.debug(`disconnect, client: ${clientAddress}`);
      this.isAdvertising = false;
      if (this.btpSession !== void 0) {
        this.btpSession.close().then(() => {
          this.btpSession = void 0;
        }).catch(() => {
          this.btpSession = void 0;
        });
      }
    });
    Bleno.on("rssiUpdate", (rssi) => {
      logger.debug(`rssiUpdate: ${rssi}`);
    });
    Bleno.on("mtuChange", (mtu) => {
      logger.debug(`mtuChange: ${mtu}`);
    });
    Bleno.on("advertisingStart", (error) => {
      logger.debug(`advertisingStart: ${error ? `error ${error}` : "success"}`);
      if (!error) {
        Bleno.setServices([this.matterBleService]);
      }
    });
    Bleno.on("advertisingStop", () => {
      logger.debug("advertisingStop");
    });
    Bleno.on("servicesSet", (error) => {
      logger.debug(`servicesSet: ${error ? `error ${error}` : "success"}`);
    });
  }
  /**
   * Process a Write request on characteristic C1 from the Matter service.
   * The data are checked if it might be a handshake request and stored until the subscribe request comes in.
   * Otherwise, the data are forwarded to the BTP session handler to be decoded and processed.
   *
   * @param data
   * @param offset
   * @param withoutResponse
   */
  handleC1WriteRequest(data, offset, withoutResponse) {
    if (offset !== 0 || withoutResponse) {
      throw new import_protocol.BleError(`Offset ${offset} or withoutResponse ${withoutResponse} not supported`);
    }
    if (data[0] === 101 && data[1] === 108 && data.length === 9) {
      this.btpHandshakeTimeout.start();
      logger.info(
        `Received Matter handshake request: ${data.toString("hex")}, store until subscribe request comes in.`
      );
      this.latestHandshakePayload = data;
    } else {
      if (this.btpSession !== void 0) {
        logger.debug(`Received Matter data for BTP Session: ${data.toString("hex")}`);
        void this.btpSession.handleIncomingBleData(new Uint8Array(data));
      } else {
        throw new import_protocol.BtpFlowError(
          `Received Matter data but no BTP session was initialized: ${data.toString("hex")}`
        );
      }
    }
  }
  /**
   * Process a Subscribe request on characteristic C2 from the Matter service.
   * This is expected directly after a handshake request and initializes the BTP session handler with the stored
   * handshake payload.
   * The BtpSessionHandler instance is wired with the bleno instance for sending data and disconnecting.
   *
   * @param maxValueSize
   * @param updateValueCallback
   */
  async handleC2SubscribeRequest(maxValueSize, updateValueCallback) {
    if (this.latestHandshakePayload === void 0) {
      throw new import_protocol.BtpFlowError(`Subscription request received before handshake Request`);
    }
    if (this.btpSession !== void 0) {
      throw new import_protocol.BtpFlowError(
        `Subscription request received but BTP session already initialized. Cannot handle two sessions!`
      );
    }
    this.btpHandshakeTimeout.stop();
    this.btpSession = await import_protocol.BtpSessionHandler.createFromHandshakeRequest(
      Math.min(Bleno.mtu - 3, maxValueSize),
      new Uint8Array(this.latestHandshakePayload),
      // callback to write data to characteristic C2
      async (data) => {
        updateValueCallback(Buffer.from(import_general.Bytes.of(data)));
        const { promise, resolver } = (0, import_general.createPromise)();
        this.writeConformationResolver = resolver;
        return promise;
      },
      // callback to disconnect the BLE connection
      async () => this.close(),
      // callback to forward decoded and de-assembled Matter messages to ExchangeManager
      async (data) => {
        if (this.onMatterMessageListener === void 0) {
          throw new import_general.InternalError(`No listener registered for Matter messages`);
        }
        this.onMatterMessageListener(this, data);
      }
    );
    this.latestHandshakePayload = void 0;
  }
  handleC2Indicate() {
    if (this.writeConformationResolver !== void 0) {
      this.writeConformationResolver();
      this.writeConformationResolver = void 0;
    } else {
      logger.warn(`Received C2 indication but no former write expected a confirmation`);
    }
  }
  /**
   * Process a Read request on characteristic C3 from the Matter service.
   * The relevant data needs optionally to be set before advertising, else empty data are used.
   *
   * @param offset
   */
  handleC3ReadRequest(offset) {
    if (offset > this.additionalAdvertisingData.length) {
      throw new import_protocol.BleError(`Offset ${offset} is larger than data ${this.additionalAdvertisingData.length}`);
    } else {
      return this.additionalAdvertisingData.subarray(offset);
    }
  }
  async advertise(advertiseData, additionalAdvertisementData, interval = (0, import_general.Millis)(100)) {
    process.env["BLENO_ADVERTISING_INTERVAL"] = interval.toString();
    this.advertisingData = Buffer.from(import_general.Bytes.of(advertiseData));
    if (additionalAdvertisementData) {
      this.additionalAdvertisingData = Buffer.from(import_general.Bytes.of(additionalAdvertisementData));
    } else {
      this.additionalAdvertisingData = Buffer.alloc(0);
    }
    if (this.isAdvertising) {
      await this.stopAdvertising();
      this.isAdvertising = false;
    }
    if (this.state === "poweredOn") {
      Bleno.startAdvertisingWithEIRData(this.advertisingData);
      this.isAdvertising = true;
    } else {
      logger.debug(`State is ${this.state}, advertise when powered on`);
    }
    return new Promise((resolve) => {
      Bleno.once("advertisingStart", () => resolve());
    });
  }
  async stopAdvertising() {
    if (this.isAdvertising) {
      return new Promise((resolve) => {
        Bleno.stopAdvertising();
        Bleno.once("advertisingStop", () => {
          this.isAdvertising = false;
          resolve();
        });
      });
    }
  }
  setMatterMessageListener(listener) {
    if (this.onMatterMessageListener !== void 0) {
      throw new import_general.InternalError(`onData listener already set`);
    }
    this.onMatterMessageListener = listener;
  }
  async btpHandshakeTimeoutTriggered() {
    await this.disconnect();
    logger.error("Timeout for handshake subscribe request on C2 reached, disconnecting.");
  }
  async close() {
    if (this.#closing) {
      return;
    }
    this.#closing = true;
    this.btpHandshakeTimeout.stop();
    await this.disconnect();
    if (this.btpSession !== void 0) {
      await this.btpSession.close();
      this.btpSession = void 0;
    }
    this.onMatterMessageListener = void 0;
  }
  async disconnect() {
    this.isAdvertising = false;
    if (this.#disconnected) {
      return;
    }
    this.#disconnected = true;
    logger.debug(`Disconnecting Bleno`);
    Bleno.disconnect();
    logger.debug(`Stopping Bleno`);
    Bleno.stop();
  }
  // Channel<Bytes>
  /**
   * Send a Matter message to the connected device - need to do BTP assembly first.
   *
   * @param data
   */
  async send(data) {
    if (this.btpSession === void 0) {
      throw new import_protocol.SessionClosedError(`Cannot send data, no BTP session initialized`);
    }
    await this.btpSession.sendMatterMessage(data);
  }
  // Channel<Bytes>
  get name() {
    return `${this.type}://${this.clientAddress}`;
  }
  [import_general.DiagnosticPresentation.name] = "BLE server";
}
//# sourceMappingURL=BlenoBleServer.js.map
