/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Bytes, DataReader, Diagnostic, Endian, Logger, MatterError, Observable, Time } from "@matter/general";
import { BtpCodec } from "../codec/BtpCodec.js";
import { BleDisconnectedError } from "./Ble.js";
import { MatterBle } from "./BleConsts.js";
class BtpMatterError extends MatterError {
}
class BtpProtocolError extends BtpMatterError {
}
class BtpFlowError extends BtpMatterError {
}
const MAXIMUM_SEQUENCE_NUMBER = 255;
const logger = 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.prevAckedSequenceNumber = this.prevIncomingSequenceNumber;
    } else {
      this.sendAckTimer.start();
      this.prevIncomingSequenceNumber = 0;
      this.prevIncomingAckNumber = -1;
      this.sequenceNumber = -1;
    }
  }
  role;
  fragmentSize;
  clientWindowSize;
  writeBleCallback;
  disconnectBleCallback;
  handleMatterMessagePayload;
  currentIncomingSegmentedMsgLength;
  currentIncomingSegmentedPayload;
  prevIncomingSequenceNumber = 255;
  // Incoming Sequence Number received. Set to 255 to start at 0
  prevIncomingAckNumber = -1;
  // Previous ackNumber received
  ackReceiveTimer = Time.getTimer(
    "BTP ack timeout",
    MatterBle.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 = Time.getTimer(
    "BTP send timeout",
    MatterBle.BTP_SEND_ACK_TIMEOUT,
    () => this.btpSendAckTimeoutTriggered()
  );
  isActive = true;
  idleTimeout = Time.getTimer("Central Device Idle Timer", MatterBle.BTP_CONN_IDLE_TIMEOUT, async () => {
    logger.info("Central Device Connection Idle Timer expired, closing BTP session");
    await this.close();
  });
  #closed = Observable();
  /** Emitted exactly once when the session transitions to closed. */
  get closed() {
    return this.#closed;
  }
  /** Factory method to create a new BTPSessionHandler from a received handshake request */
  static async createFromHandshakeRequest(maxDataSize, handshakeRequestPayload, writeBleCallback, disconnectBleCallback, handleMatterMessagePayload) {
    const handshakeRequest = BtpCodec.decodeBtpHandshakeRequest(handshakeRequestPayload);
    const {
      versions,
      attMtu: handshakeMtu,
      // Number excluding 3 header bytes
      clientWindowSize
    } = handshakeRequest;
    logger.debug(`Received BTP handshake request:`, Diagnostic.dict({ maxDataSize, ...handshakeRequest }));
    const version = MatterBle.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 = MatterBle.MINIMUM_ATT_MTU;
    if (maxDataSize !== void 0) {
      if (maxDataSize > MatterBle.MINIMUM_ATT_MTU) {
        if (handshakeMtu <= MatterBle.MINIMUM_ATT_MTU) {
          attMtu = Math.min(maxDataSize, MatterBle.MAXIMUM_BTP_MTU);
        } else {
          attMtu = Math.min(handshakeMtu, maxDataSize, MatterBle.MAXIMUM_BTP_MTU);
        }
      }
    }
    const fragmentSize = attMtu;
    const windowSize = Math.min(MatterBle.BTP_MAXIMUM_WINDOW_SIZE, clientWindowSize);
    const handshakeResponse = BtpCodec.encodeBtpHandshakeResponse({
      version,
      attMtu,
      windowSize
    });
    logger.debug(
      `Sending BTP handshake response:`,
      Diagnostic.dict({
        version,
        attMtu,
        windowSize
      })
    );
    const btpSession = new BtpSessionHandler(
      "peripheral",
      version,
      fragmentSize,
      windowSize,
      writeBleCallback,
      disconnectBleCallback,
      handleMatterMessagePayload
    );
    await writeBleCallback(handshakeResponse);
    btpSession.ackReceiveTimer.start();
    return btpSession;
  }
  static async createAsCentral(handshakeResponsePayload, writeBleCallback, disconnectBleCallback, handleMatterMessagePayload) {
    const handshakeRequest = BtpCodec.decodeBtpHandshakeResponsePayload(handshakeResponsePayload);
    logger.debug("Handshake request", Diagnostic.dict(handshakeRequest));
    const { version, attMtu: handshakeMtu, windowSize } = handshakeRequest;
    const fragmentSize = Math.min(handshakeMtu, MatterBle.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 = BtpCodec.decodeBtpPacket(data);
      logger.debug(`Received BTP packet: ${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) {
        const outstanding = this.modularDistance(this.prevIncomingAckNumber, this.sequenceNumber);
        if (this.modularDistance(this.prevIncomingAckNumber, ackNumber) > outstanding) {
          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.`);
        }
        const reassembledLength = this.currentIncomingSegmentedPayload.byteLength + segmentPayload.byteLength;
        if (this.currentIncomingSegmentedMsgLength !== void 0 && reassembledLength > this.currentIncomingSegmentedMsgLength) {
          throw new BtpProtocolError(
            `BTP reassembled length ${reassembledLength} exceeds declared message length ${this.currentIncomingSegmentedMsgLength}`
          );
        }
        this.currentIncomingSegmentedPayload = 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);
      }
      await this.processSendQueue();
      await this.sendImmediateAckIfWindowLow();
    } catch (error) {
      logger.warn(`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: ${Bytes.toHex(data)}`);
    if (data.byteLength === 0) {
      throw new BtpFlowError("BTP packet must not be empty");
    }
    const dataReader = new DataReader(data, Endian.Little);
    this.queuedOutgoingMatterMessages.push(dataReader);
    await this.processSendQueue();
  }
  async processSendQueue() {
    if (this.sendInProgress) return;
    if (this.queuedOutgoingMatterMessages.length === 0) return;
    this.sendInProgress = true;
    while (this.queuedOutgoingMatterMessages.length > 0) {
      const hasAckNumber = this.prevIncomingSequenceNumber !== this.prevAckedSequenceNumber;
      if (!this.canSend(hasAckNumber)) {
        break;
      }
      if (hasAckNumber) {
        this.sendAckTimer.stop();
      }
      const currentProcessedMessage = this.queuedOutgoingMatterMessages[0];
      const remainingMessageLength = currentProcessedMessage.remainingBytesCount;
      logger.debug(
        "Sending BTP fragment: ",
        Diagnostic.dict({
          fullMessageLength: currentProcessedMessage.length,
          remainingLengthInBytes: remainingMessageLength
        })
      );
      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 ackNumberToSend = hasAckNumber ? this.prevIncomingSequenceNumber : void 0;
      const btpPacket = {
        header: packetHeader,
        payload: {
          ackNumber: ackNumberToSend,
          sequenceNumber: this.getNextSequenceNumber(),
          messageLength: packetHeader.isBeginningSegment ? remainingMessageLength : void 0,
          // remainingMessageLength if the fill length on beginning packet
          segmentPayload
        }
      };
      logger.debug(`Sending BTP packet: ${Diagnostic.json(btpPacket)}`);
      const packet = BtpCodec.encodeBtpPacket(btpPacket);
      logger.debug(`Sending BTP packet raw: ${Bytes.toHex(packet)}`);
      const previousAckedSequenceNumber = this.prevAckedSequenceNumber;
      if (ackNumberToSend !== void 0) {
        this.prevAckedSequenceNumber = ackNumberToSend;
      }
      try {
        await this.writeBleCallback(packet);
      } catch (error) {
        this.prevAckedSequenceNumber = previousAckedSequenceNumber;
        this.queuedOutgoingMatterMessages.length = 0;
        this.sendInProgress = false;
        BleDisconnectedError.accept(error);
        logger.debug(
          `BTP packet (seq ${btpPacket.payload.sequenceNumber}) send failed because BLE is disconnected`,
          Diagnostic.errorMessage(error)
        );
        return;
      }
      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();
      }
    }
    this.sendInProgress = false;
    await this.sendStandaloneAck();
  }
  /**
   * 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;
      try {
        await this.disconnectBleCallback();
      } finally {
        this.#closed.emit();
      }
    }
  }
  /**
   * If this timer expires and the peer has a pending acknowledgement, the peer SHALL immediately send that
   * acknowledgement
   */
  async btpSendAckTimeoutTriggered() {
    if (!this.isActive) return;
    await this.sendStandaloneAck();
  }
  /** §4.19.4.8: ack proactively once our receive window runs low; skipped if queued data will piggyback it. */
  async sendImmediateAckIfWindowLow() {
    if (this.queuedOutgoingMatterMessages.length > 0 || this.sendInProgress) return;
    if (this.localWindowFreeSlots() > MatterBle.BTP_IMMEDIATE_ACK_WINDOW_THRESHOLD) return;
    await this.sendStandaloneAck();
  }
  /** Send a stand-alone acknowledgement packet if one is pending. */
  async sendStandaloneAck() {
    if (this.sendInProgress) return;
    const ackNumberToSend = this.prevIncomingSequenceNumber;
    if (ackNumberToSend === this.prevAckedSequenceNumber) return;
    if (!this.canSend(true)) return;
    this.sendInProgress = true;
    try {
      logger.debug(`Sending BTP ACK for sequence number ${ackNumberToSend}`);
      const btpPacket = {
        header: {
          isHandshakeRequest: false,
          hasManagementOpcode: false,
          hasAckNumber: true,
          isBeginningSegment: false,
          isContinuingSegment: false,
          isEndingSegment: false
        },
        payload: {
          ackNumber: ackNumberToSend,
          sequenceNumber: this.getNextSequenceNumber()
        }
      };
      const packet = BtpCodec.encodeBtpPacket(btpPacket);
      const previousAckedSequenceNumber = this.prevAckedSequenceNumber;
      this.prevAckedSequenceNumber = ackNumberToSend;
      try {
        await this.writeBleCallback(packet);
      } catch (error) {
        this.prevAckedSequenceNumber = previousAckedSequenceNumber;
        BleDisconnectedError.accept(error);
        logger.debug(
          `BTP ACK (seq ${btpPacket.payload.sequenceNumber}, ack ${ackNumberToSend}) send failed because BLE is disconnected`,
          Diagnostic.errorMessage(error)
        );
        return;
      }
      this.sendAckTimer.stop();
      if (!this.ackReceiveTimer.isRunning) {
        this.ackReceiveTimer.start();
      }
    } finally {
      this.sendInProgress = false;
    }
    await this.processSendQueue();
  }
  /**
   * 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;
  }
  /** Wrap-safe forward distance between two sequence numbers (modulo 256); the -1 sentinels read as 255. */
  modularDistance(from, to) {
    return ((to - from) % 256 + 256) % 256;
  }
  /** Free slots in the remote peer's receive window; gates our sending (§4.19.4.7). */
  remoteWindowFreeSlots() {
    return this.clientWindowSize - this.modularDistance(this.prevIncomingAckNumber, this.sequenceNumber);
  }
  /** Free slots in our own receive window; drives proactive acknowledgement (§4.19.4.8). */
  localWindowFreeSlots() {
    return this.clientWindowSize - this.modularDistance(this.prevAckedSequenceNumber, this.prevIncomingSequenceNumber);
  }
  /** §4.19.4.7: a data-only packet must leave the last window slot free; an ack-bearing packet may use it. */
  canSend(carriesAck) {
    const free = this.remoteWindowFreeSlots();
    if (free <= 0) return false;
    if (free <= MatterBle.BTP_WINDOW_NO_ACK_SEND_THRESHOLD && !carriesAck) return false;
    return true;
  }
}
export {
  BtpFlowError,
  BtpMatterError,
  BtpProtocolError,
  BtpSessionHandler
};
//# sourceMappingURL=BtpSessionHandler.js.map
