"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 BtpSessionHandler_exports = {};
__export(BtpSessionHandler_exports, {
  BTP_SUPPORTED_VERSIONS: () => BTP_SUPPORTED_VERSIONS,
  BtpFlowError: () => BtpFlowError,
  BtpMatterError: () => BtpMatterError,
  BtpProtocolError: () => BtpProtocolError,
  BtpSessionHandler: () => BtpSessionHandler
});
module.exports = __toCommonJS(BtpSessionHandler_exports);
var import_general = require("#general");
var import_BtpCodec = require("../codec/BtpCodec.js");
var import_BleConsts = require("./BleConsts.js");
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
class BtpMatterError extends import_general.MatterError {
}
class BtpProtocolError extends BtpMatterError {
}
class BtpFlowError extends BtpMatterError {
}
const BTP_SUPPORTED_VERSIONS = [4];
const MAXIMUM_SEQUENCE_NUMBER = 255;
const logger = import_general.Logger.get("BtpSessionHandler");
class BtpSessionHandler {
  /**
   * Creates a new BTP session handler
   *
   * @param role The role of the BTP session handler
   * @param btpVersion The BTP protocol version to use
   * @param fragmentSize The fragment size to use for the messages
   * @param clientWindowSize The client window size to use
   * @param writeBleCallback Callback to write data to the BLE transport
   * @param disconnectBleCallback Callback to disconnect the BLE transport
   * @param handleMatterMessagePayload Callback to handle a Matter message payload
   */
  constructor(role, btpVersion, fragmentSize, clientWindowSize, writeBleCallback, disconnectBleCallback, handleMatterMessagePayload) {
    this.role = role;
    this.fragmentSize = fragmentSize;
    this.clientWindowSize = clientWindowSize;
    this.writeBleCallback = writeBleCallback;
    this.disconnectBleCallback = disconnectBleCallback;
    this.handleMatterMessagePayload = handleMatterMessagePayload;
    if (btpVersion !== 4) {
      throw new BtpProtocolError(`Unsupported BTP version ${btpVersion}`);
    }
    if (role === "peripheral") {
      this.ackReceiveTimer.start();
    } else {
      this.sendAckTimer.start();
      this.prevIncomingSequenceNumber = 0;
      this.prevIncomingAckNumber = -1;
      this.sequenceNumber = -1;
    }
  }
  currentIncomingSegmentedMsgLength;
  currentIncomingSegmentedPayload;
  prevIncomingSequenceNumber = 255;
  // Incoming Sequence Number received. Set to 255 to start at 0
  prevIncomingAckNumber = -1;
  // Previous ackNumber received
  ackReceiveTimer = import_general.Time.getTimer(
    "BTP ack timeout",
    import_BleConsts.BTP_ACK_TIMEOUT,
    () => this.btpAckTimeoutTriggered()
  );
  sequenceNumber = 0;
  // Sequence number is set to 0 already for the handshake, next sequence number is 1
  prevAckedSequenceNumber = -1;
  // Previous (outgoing) Acked Sequence Number
  queuedOutgoingMatterMessages = new Array();
  sendInProgress = false;
  sendAckTimer = import_general.Time.getTimer(
    "BTP send timeout",
    import_BleConsts.BTP_SEND_ACK_TIMEOUT,
    () => this.btpSendAckTimeoutTriggered()
  );
  isActive = true;
  idleTimeout = import_general.Time.getTimer("Central Device Idle Timer", import_BleConsts.BTP_CONN_IDLE_TIMEOUT, async () => {
    logger.info("Central Device Connection Idle Timer expired, closing BTP session");
    await this.close();
  });
  /** Factory method to create a new BTPSessionHandler from a received handshake request */
  static async createFromHandshakeRequest(maxDataSize, handshakeRequestPayload, writeBleCallback, disconnectBleCallback, handleMatterMessagePayload) {
    const handshakeRequest = import_BtpCodec.BtpCodec.decodeBtpHandshakeRequest(handshakeRequestPayload);
    const {
      versions,
      attMtu: handshakeMtu,
      // Number excluding 3 header bytes
      clientWindowSize
    } = handshakeRequest;
    logger.debug(`Received BTP handshake request:`, import_general.Diagnostic.dict({ maxDataSize, ...handshakeRequest }));
    const version = BTP_SUPPORTED_VERSIONS.find((version2) => versions.includes(version2));
    if (version === void 0) {
      await disconnectBleCallback();
      throw new BtpProtocolError(`No supported BTP version found in ${versions}`);
    }
    let attMtu = import_BleConsts.BLE_MINIMUM_ATT_MTU;
    if (maxDataSize !== void 0) {
      if (maxDataSize > import_BleConsts.BLE_MINIMUM_ATT_MTU) {
        if (handshakeMtu <= import_BleConsts.BLE_MINIMUM_ATT_MTU) {
          attMtu = Math.min(maxDataSize, import_BleConsts.BLE_MAXIMUM_BTP_MTU);
        } else {
          attMtu = Math.min(handshakeMtu, maxDataSize, import_BleConsts.BLE_MAXIMUM_BTP_MTU);
        }
      }
    }
    const fragmentSize = attMtu;
    const windowSize = Math.min(import_BleConsts.BTP_MAXIMUM_WINDOW_SIZE, clientWindowSize);
    const handshakeResponse = import_BtpCodec.BtpCodec.encodeBtpHandshakeResponse({
      version,
      attMtu,
      windowSize
    });
    logger.debug(
      `Sending BTP handshake response:`,
      import_general.Diagnostic.dict({
        version,
        attMtu,
        windowSize
      })
    );
    const btpSession = new BtpSessionHandler(
      "peripheral",
      version,
      fragmentSize,
      windowSize,
      writeBleCallback,
      disconnectBleCallback,
      handleMatterMessagePayload
    );
    await writeBleCallback(handshakeResponse);
    return btpSession;
  }
  static async createAsCentral(handshakeResponsePayload, writeBleCallback, disconnectBleCallback, handleMatterMessagePayload) {
    const handshakeRequest = import_BtpCodec.BtpCodec.decodeBtpHandshakeResponsePayload(handshakeResponsePayload);
    logger.debug("Handshake request", import_general.Diagnostic.dict(handshakeRequest));
    const { version, attMtu: handshakeMtu, windowSize } = handshakeRequest;
    const fragmentSize = Math.min(handshakeMtu, import_BleConsts.BLE_MAXIMUM_BTP_MTU);
    return new BtpSessionHandler(
      "central",
      version,
      fragmentSize,
      windowSize,
      writeBleCallback,
      disconnectBleCallback,
      handleMatterMessagePayload
    );
  }
  /**
   * Handle incoming data from the transport layer and hand over completely received matter messages to the
   * ExchangeManager layer
   *
   * @param data ByteArray containing the data
   */
  async handleIncomingBleData(data) {
    if (!this.isActive) {
      logger.debug(`BTP session is not active, ignoring incoming BLE data`);
      return;
    }
    try {
      if (data.byteLength > this.fragmentSize) {
        if (data.byteLength > this.fragmentSize + 3) {
          throw new BtpProtocolError(
            `Received data ${data.byteLength} bytes exceeds fragment size of ${this.fragmentSize} bytes`
          );
        } else {
          logger.warn(
            `Received data ${data.byteLength} bytes exceeds fragment size of ${this.fragmentSize} bytes, still accepting`
          );
        }
      }
      const btpPacket = import_BtpCodec.BtpCodec.decodeBtpPacket(data);
      logger.debug(`Received BTP packet: ${import_general.Diagnostic.json(btpPacket)}`);
      const {
        header: {
          hasAckNumber,
          isHandshakeRequest,
          hasManagementOpcode,
          isEndingSegment,
          isBeginningSegment,
          isContinuingSegment
        },
        payload: { ackNumber, sequenceNumber, messageLength, segmentPayload }
      } = btpPacket;
      if (isHandshakeRequest || hasManagementOpcode) {
        throw new BtpProtocolError("BTP packet must not be a handshake request or have a management opcode.");
      }
      if (segmentPayload.byteLength === 0 && !hasAckNumber) {
        throw new BtpProtocolError("BTP packet must have a segment payload or an ack number.");
      }
      if (sequenceNumber !== (this.prevIncomingSequenceNumber + 1) % 256) {
        logger.debug(
          `sequenceNumber : ${sequenceNumber}, prevClientSequenceNumber : ${this.prevIncomingSequenceNumber}`
        );
        throw new BtpProtocolError("Expected and actual BTP packets sequence number does not match");
      }
      this.prevIncomingSequenceNumber = sequenceNumber;
      if (!this.sendAckTimer.isRunning) {
        this.sendAckTimer.start();
      }
      if (hasAckNumber && ackNumber !== void 0) {
        if (ackNumber > this.sequenceNumber || this.exceedsWindowSize(this.prevIncomingAckNumber, ackNumber)) {
          throw new BtpProtocolError(
            `Invalid Ack Number, Ack Number: ${ackNumber}, Sequence Number: ${this.sequenceNumber}, Previous AckNumber: ${this.prevIncomingAckNumber}`
          );
        }
        this.ackReceiveTimer.stop();
        this.prevIncomingAckNumber = ackNumber;
        if (ackNumber !== this.sequenceNumber) {
          this.ackReceiveTimer.start();
        }
      }
      if (isBeginningSegment) {
        if (this.currentIncomingSegmentedPayload !== void 0) {
          throw new BtpProtocolError(
            `BTP message flow error! New beginning packet was received without previous message being completed.`
          );
        }
        this.currentIncomingSegmentedMsgLength = messageLength;
        this.currentIncomingSegmentedPayload = segmentPayload;
      } else if (isContinuingSegment || isEndingSegment) {
        if (this.currentIncomingSegmentedPayload === void 0) {
          throw new BtpProtocolError(`BTP Continuing or ending packet received without beginning packet.`);
        }
        if (segmentPayload.byteLength === 0) {
          throw new BtpProtocolError(`BTP Continuing or ending packet received without payload.`);
        }
        this.currentIncomingSegmentedPayload = import_general.Bytes.concat(
          this.currentIncomingSegmentedPayload,
          segmentPayload
        );
      }
      if (isEndingSegment) {
        if (this.currentIncomingSegmentedMsgLength === void 0 || this.currentIncomingSegmentedPayload === void 0) {
          throw new BtpProtocolError("BTP beginning packet missing but ending packet received.");
        }
        if (this.currentIncomingSegmentedPayload.byteLength !== this.currentIncomingSegmentedMsgLength) {
          throw new BtpProtocolError(
            `BTP packet payload length does not match message length: ${this.currentIncomingSegmentedPayload.byteLength} !== ${this.currentIncomingSegmentedMsgLength}`
          );
        }
        const payloadToProcess = this.currentIncomingSegmentedPayload;
        this.currentIncomingSegmentedMsgLength = void 0;
        this.currentIncomingSegmentedPayload = void 0;
        await this.handleMatterMessagePayload(payloadToProcess);
      }
    } catch (error) {
      logger.error(`Error while handling incoming BTP data: ${error}`);
      await this.close();
      BtpProtocolError.accept(error);
    }
  }
  /**
   * Send a Matter message to the transport layer, but before that encode it into a BTP packet and potentially split
   * it into multiple segments. This Method is indirectly called by the ExchangeManager layer when a Matter message
   * should be sent.
   *
   * @param data ByteArray containing the Matter message
   */
  async sendMatterMessage(data) {
    if (!this.isActive) {
      throw new BtpFlowError("BTP session is not active");
    }
    logger.debug(`Got Matter message to send via BLE transport: ${import_general.Bytes.toHex(data)}`);
    if (data.byteLength === 0) {
      throw new BtpFlowError("BTP packet must not be empty");
    }
    const dataReader = new import_general.DataReader(data, import_general.Endian.Little);
    this.queuedOutgoingMatterMessages.push(dataReader);
    await this.processSendQueue();
  }
  async processSendQueue() {
    if (this.sendInProgress) return;
    if (this.exceedsWindowSize(this.prevIncomingAckNumber, this.sequenceNumber)) return;
    if (this.queuedOutgoingMatterMessages.length === 0) return;
    this.sendInProgress = true;
    while (this.queuedOutgoingMatterMessages.length > 0) {
      const currentProcessedMessage = this.queuedOutgoingMatterMessages[0];
      const remainingMessageLength = currentProcessedMessage.remainingBytesCount;
      logger.debug(
        "Sending BTP fragment: ",
        import_general.Diagnostic.dict({
          fullMessageLength: currentProcessedMessage.length,
          remainingLengthInBytes: remainingMessageLength
        })
      );
      const hasAckNumber = this.prevIncomingSequenceNumber !== this.prevAckedSequenceNumber;
      if (hasAckNumber) {
        this.prevAckedSequenceNumber = this.prevIncomingSequenceNumber;
        this.sendAckTimer.stop();
      }
      const isBeginningSegment = remainingMessageLength === currentProcessedMessage.length;
      const btpHeaderLength = 2 + (isBeginningSegment ? 2 : 0) + (hasAckNumber ? 1 : 0);
      const isEndingSegment = remainingMessageLength <= this.fragmentSize - btpHeaderLength;
      const packetHeader = {
        isHandshakeRequest: false,
        hasManagementOpcode: false,
        hasAckNumber,
        isBeginningSegment,
        isContinuingSegment: !isBeginningSegment,
        isEndingSegment
      };
      logger.debug(
        `Take up to ${this.fragmentSize - btpHeaderLength} bytes from Rest of message: ${remainingMessageLength}`
      );
      const segmentPayload = currentProcessedMessage.readByteArray(this.fragmentSize - btpHeaderLength);
      const btpPacket = {
        header: packetHeader,
        payload: {
          ackNumber: hasAckNumber ? this.prevIncomingSequenceNumber : void 0,
          sequenceNumber: this.getNextSequenceNumber(),
          messageLength: packetHeader.isBeginningSegment ? remainingMessageLength : void 0,
          // remainingMessageLength if the fill length on beginning packet
          segmentPayload
        }
      };
      logger.debug(`Sending BTP packet: ${import_general.Diagnostic.json(btpPacket)}`);
      const packet = import_BtpCodec.BtpCodec.encodeBtpPacket(btpPacket);
      logger.debug(`Sending BTP packet raw: ${import_general.Bytes.toHex(packet)}`);
      await this.writeBleCallback(packet);
      if (!this.ackReceiveTimer.isRunning) {
        this.ackReceiveTimer.start();
      }
      if (this.role === "central") {
        if (this.idleTimeout.isRunning) {
          this.idleTimeout.stop();
        }
        this.idleTimeout.start();
      }
      if (isEndingSegment) {
        this.queuedOutgoingMatterMessages.shift();
      }
      if (this.exceedsWindowSize(this.prevIncomingAckNumber, this.sequenceNumber)) {
        break;
      }
    }
    this.sendInProgress = false;
  }
  /**
   * Close the BTP session. This method is called when the BLE transport is disconnected and so the BTP session gets closed.
   */
  async close() {
    this.sendAckTimer.stop();
    this.ackReceiveTimer.stop();
    this.idleTimeout.stop();
    if (this.isActive) {
      logger.debug(`Closing BTP session`);
      this.isActive = false;
      await this.disconnectBleCallback();
    }
  }
  /**
   * If this timer expires and the peer has a pending acknowledgement, the peer SHALL immediately send that
   * acknowledgement
   */
  async btpSendAckTimeoutTriggered() {
    if (this.prevIncomingSequenceNumber > this.prevAckedSequenceNumber) {
      logger.debug(`Sending BTP ACK for sequence number ${this.prevIncomingSequenceNumber}`);
      const btpPacket = {
        header: {
          isHandshakeRequest: false,
          hasManagementOpcode: false,
          hasAckNumber: true,
          isBeginningSegment: false,
          isContinuingSegment: false,
          isEndingSegment: false
        },
        payload: {
          ackNumber: this.prevIncomingSequenceNumber,
          sequenceNumber: this.getNextSequenceNumber()
        }
      };
      this.prevAckedSequenceNumber = this.prevIncomingSequenceNumber;
      const packet = import_BtpCodec.BtpCodec.encodeBtpPacket(btpPacket);
      await this.writeBleCallback(packet);
      if (!this.ackReceiveTimer.isRunning) {
        this.ackReceiveTimer.start();
      }
    }
  }
  /**
   * If a peer’s acknowledgement-received timer expires, or if a peer receives an invalid acknowledgement,
   * the peer SHALL close the BTP session and report an error to the application.
   */
  async btpAckTimeoutTriggered() {
    if (this.prevIncomingAckNumber !== this.sequenceNumber) {
      logger.warn("Acknowledgement for the sent sequence number was not received ... disconnect");
      await this.close();
    }
  }
  /**
   * Increments sequence number for the packets and round it off to 0 when it reaches the maximum limit.
   */
  getNextSequenceNumber() {
    this.sequenceNumber++;
    if (this.sequenceNumber > MAXIMUM_SEQUENCE_NUMBER) {
      this.sequenceNumber = 0;
    }
    return this.sequenceNumber;
  }
  /**
   * Checks if incoming ackNumber and sent sequence number exceeds the client window size or not.
   */
  exceedsWindowSize(prevIncomingAckNumber, currentSequenceNumber) {
    if (prevIncomingAckNumber > currentSequenceNumber) {
      prevIncomingAckNumber = prevIncomingAckNumber % MAXIMUM_SEQUENCE_NUMBER - 1;
    }
    return currentSequenceNumber - prevIncomingAckNumber > this.clientWindowSize - 1;
  }
}
//# sourceMappingURL=BtpSessionHandler.js.map
