'use strict';

const {
  BigUint64Array,
  JSONStringify,
} = primordials;

const {
  isArrayBuffer,
} = require('util/types');

const {
  codes: {
    ERR_ILLEGAL_CONSTRUCTOR,
    ERR_INVALID_ARG_TYPE,
  },
} = require('internal/errors');

const { inspect } = require('internal/util/inspect');

const {
  kFinishClose,
  kInspect,
  kPrivateConstructor,
} = require('internal/quic/symbols');

// This file defines the helper objects for accessing statistics collected
// by various QUIC objects. Each of these wraps a BigUint64Array. Every
// stats object is read-only via the API, and the underlying buffer is
// only updated by the QUIC internals. When the stats object is no longer
// needed, it is closed to prevent further updates to the buffer.

const {
  // All of the IDX_STATS_* constants are the index positions of the stats
  // fields in the relevant BigUint64Array's that underlie the *Stats objects.
  // These are not exposed to end users.
  IDX_STATS_ENDPOINT_CREATED_AT,
  IDX_STATS_ENDPOINT_DESTROYED_AT,
  IDX_STATS_ENDPOINT_BYTES_RECEIVED,
  IDX_STATS_ENDPOINT_BYTES_SENT,
  IDX_STATS_ENDPOINT_PACKETS_RECEIVED,
  IDX_STATS_ENDPOINT_PACKETS_SENT,
  IDX_STATS_ENDPOINT_SERVER_SESSIONS,
  IDX_STATS_ENDPOINT_CLIENT_SESSIONS,
  IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT,
  IDX_STATS_ENDPOINT_RETRY_COUNT,
  IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT,
  IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT,
  IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,

  IDX_STATS_SESSION_CREATED_AT,
  IDX_STATS_SESSION_CLOSING_AT,
  IDX_STATS_SESSION_DESTROYED_AT,
  IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT,
  IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT,
  IDX_STATS_SESSION_GRACEFUL_CLOSING_AT,
  IDX_STATS_SESSION_BYTES_RECEIVED,
  IDX_STATS_SESSION_BYTES_SENT,
  IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT,
  IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT,
  IDX_STATS_SESSION_UNI_IN_STREAM_COUNT,
  IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT,
  IDX_STATS_SESSION_LOSS_RETRANSMIT_COUNT,
  IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT,
  IDX_STATS_SESSION_BYTES_IN_FLIGHT,
  IDX_STATS_SESSION_BLOCK_COUNT,
  IDX_STATS_SESSION_CWND,
  IDX_STATS_SESSION_LATEST_RTT,
  IDX_STATS_SESSION_MIN_RTT,
  IDX_STATS_SESSION_RTTVAR,
  IDX_STATS_SESSION_SMOOTHED_RTT,
  IDX_STATS_SESSION_SSTHRESH,
  IDX_STATS_SESSION_DATAGRAMS_RECEIVED,
  IDX_STATS_SESSION_DATAGRAMS_SENT,
  IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED,
  IDX_STATS_SESSION_DATAGRAMS_LOST,

  IDX_STATS_STREAM_CREATED_AT,
  IDX_STATS_STREAM_RECEIVED_AT,
  IDX_STATS_STREAM_ACKED_AT,
  IDX_STATS_STREAM_CLOSING_AT,
  IDX_STATS_STREAM_DESTROYED_AT,
  IDX_STATS_STREAM_BYTES_RECEIVED,
  IDX_STATS_STREAM_BYTES_SENT,
  IDX_STATS_STREAM_MAX_OFFSET,
  IDX_STATS_STREAM_MAX_OFFSET_ACK,
  IDX_STATS_STREAM_MAX_OFFSET_RECV,
  IDX_STATS_STREAM_FINAL_SIZE,
} = internalBinding('quic');

class QuicEndpointStats {
  /** @type {BigUint64Array} */
  #handle;
  /** @type {boolean} */
  #disconnected = false;

  /**
   * @param {symbol} privateSymbol
   * @param {ArrayBuffer} buffer
   */
  constructor(privateSymbol, buffer) {
    // We use the kPrivateConstructor symbol to restrict the ability to
    // create new instances of QuicEndpointStats to internal code.
    if (privateSymbol !== kPrivateConstructor) {
      throw new ERR_ILLEGAL_CONSTRUCTOR();
    }
    if (!isArrayBuffer(buffer)) {
      throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer);
    }
    this.#handle = new BigUint64Array(buffer);
  }

  /** @type {bigint} */
  get createdAt() {
    return this.#handle[IDX_STATS_ENDPOINT_CREATED_AT];
  }

  /** @type {bigint} */
  get destroyedAt() {
    return this.#handle[IDX_STATS_ENDPOINT_DESTROYED_AT];
  }

  /** @type {bigint} */
  get bytesReceived() {
    return this.#handle[IDX_STATS_ENDPOINT_BYTES_RECEIVED];
  }

  /** @type {bigint} */
  get bytesSent() {
    return this.#handle[IDX_STATS_ENDPOINT_BYTES_SENT];
  }

  /** @type {bigint} */
  get packetsReceived() {
    return this.#handle[IDX_STATS_ENDPOINT_PACKETS_RECEIVED];
  }

  /** @type {bigint} */
  get packetsSent() {
    return this.#handle[IDX_STATS_ENDPOINT_PACKETS_SENT];
  }

  /** @type {bigint} */
  get serverSessions() {
    return this.#handle[IDX_STATS_ENDPOINT_SERVER_SESSIONS];
  }

  /** @type {bigint} */
  get clientSessions() {
    return this.#handle[IDX_STATS_ENDPOINT_CLIENT_SESSIONS];
  }

  /** @type {bigint} */
  get serverBusyCount() {
    return this.#handle[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT];
  }

  /** @type {bigint} */
  get retryCount() {
    return this.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT];
  }

  /** @type {bigint} */
  get versionNegotiationCount() {
    return this.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT];
  }

  /** @type {bigint} */
  get statelessResetCount() {
    return this.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT];
  }

  /** @type {bigint} */
  get immediateCloseCount() {
    return this.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT];
  }

  toString() {
    return JSONStringify(this.toJSON());
  }

  toJSON() {
    return {
      __proto__: null,
      connected: this.isConnected,
      // We need to convert the values to strings because JSON does not
      // support BigInts.
      createdAt: `${this.createdAt}`,
      destroyedAt: `${this.destroyedAt}`,
      bytesReceived: `${this.bytesReceived}`,
      bytesSent: `${this.bytesSent}`,
      packetsReceived: `${this.packetsReceived}`,
      packetsSent: `${this.packetsSent}`,
      serverSessions: `${this.serverSessions}`,
      clientSessions: `${this.clientSessions}`,
      serverBusyCount: `${this.serverBusyCount}`,
      retryCount: `${this.retryCount}`,
      versionNegotiationCount: `${this.versionNegotiationCount}`,
      statelessResetCount: `${this.statelessResetCount}`,
      immediateCloseCount: `${this.immediateCloseCount}`,
    };
  }

  [kInspect](depth, options) {
    if (depth < 0)
      return this;

    const opts = {
      ...options,
      depth: options.depth == null ? null : options.depth - 1,
    };

    return `QuicEndpointStats ${inspect({
      connected: this.isConnected,
      createdAt: this.createdAt,
      destroyedAt: this.destroyedAt,
      bytesReceived: this.bytesReceived,
      bytesSent: this.bytesSent,
      packetsReceived: this.packetsReceived,
      packetsSent: this.packetsSent,
      serverSessions: this.serverSessions,
      clientSessions: this.clientSessions,
      serverBusyCount: this.serverBusyCount,
      retryCount: this.retryCount,
      versionNegotiationCount: this.versionNegotiationCount,
      statelessResetCount: this.statelessResetCount,
      immediateCloseCount: this.immediateCloseCount,
    }, opts)}`;
  }

  /**
   * True if this QuicEndpointStats object is still connected to the underlying
   * Endpoint stats source. If this returns false, then the stats object is
   * no longer being updated and should be considered stale.
   * @returns {boolean}
   */
  get isConnected() {
    return !this.#disconnected;
  }

  [kFinishClose]() {
    // Snapshot the stats into a new BigUint64Array since the underlying
    // buffer will be destroyed.
    this.#handle = new BigUint64Array(this.#handle);
    this.#disconnected = true;
  }
}

class QuicSessionStats {
  /** @type {BigUint64Array} */
  #handle;
  /** @type {boolean} */
  #disconnected = false;

  /**
   * @param {symbol} privateSynbol
   * @param {BigUint64Array} buffer
   */
  constructor(privateSynbol, buffer) {
    // We use the kPrivateConstructor symbol to restrict the ability to
    // create new instances of QuicSessionStats to internal code.
    if (privateSynbol !== kPrivateConstructor) {
      throw new ERR_ILLEGAL_CONSTRUCTOR();
    }
    if (!isArrayBuffer(buffer)) {
      throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer);
    }
    this.#handle = new BigUint64Array(buffer);
  }

  /** @type {bigint} */
  get createdAt() {
    return this.#handle[IDX_STATS_SESSION_CREATED_AT];
  }

  /** @type {bigint} */
  get closingAt() {
    return this.#handle[IDX_STATS_SESSION_CLOSING_AT];
  }

  /** @type {bigint} */
  get destroyedAt() {
    return this.#handle[IDX_STATS_SESSION_DESTROYED_AT];
  }

  /** @type {bigint} */
  get handshakeCompletedAt() {
    return this.#handle[IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT];
  }

  /** @type {bigint} */
  get handshakeConfirmedAt() {
    return this.#handle[IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT];
  }

  /** @type {bigint} */
  get gracefulClosingAt() {
    return this.#handle[IDX_STATS_SESSION_GRACEFUL_CLOSING_AT];
  }

  /** @type {bigint} */
  get bytesReceived() {
    return this.#handle[IDX_STATS_SESSION_BYTES_RECEIVED];
  }

  /** @type {bigint} */
  get bytesSent() {
    return this.#handle[IDX_STATS_SESSION_BYTES_SENT];
  }

  /** @type {bigint} */
  get bidiInStreamCount() {
    return this.#handle[IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT];
  }

  /** @type {bigint} */
  get bidiOutStreamCount() {
    return this.#handle[IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT];
  }

  /** @type {bigint} */
  get uniInStreamCount() {
    return this.#handle[IDX_STATS_SESSION_UNI_IN_STREAM_COUNT];
  }

  /** @type {bigint} */
  get uniOutStreamCount() {
    return this.#handle[IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT];
  }

  /** @type {bigint} */
  get lossRetransmitCount() {
    return this.#handle[IDX_STATS_SESSION_LOSS_RETRANSMIT_COUNT];
  }

  /** @type {bigint} */
  get maxBytesInFlights() {
    return this.#handle[IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT];
  }

  /** @type {bigint} */
  get bytesInFlight() {
    return this.#handle[IDX_STATS_SESSION_BYTES_IN_FLIGHT];
  }

  /** @type {bigint} */
  get blockCount() {
    return this.#handle[IDX_STATS_SESSION_BLOCK_COUNT];
  }

  /** @type {bigint} */
  get cwnd() {
    return this.#handle[IDX_STATS_SESSION_CWND];
  }

  /** @type {bigint} */
  get latestRtt() {
    return this.#handle[IDX_STATS_SESSION_LATEST_RTT];
  }

  /** @type {bigint} */
  get minRtt() {
    return this.#handle[IDX_STATS_SESSION_MIN_RTT];
  }

  /** @type {bigint} */
  get rttVar() {
    return this.#handle[IDX_STATS_SESSION_RTTVAR];
  }

  /** @type {bigint} */
  get smoothedRtt() {
    return this.#handle[IDX_STATS_SESSION_SMOOTHED_RTT];
  }

  /** @type {bigint} */
  get ssthresh() {
    return this.#handle[IDX_STATS_SESSION_SSTHRESH];
  }

  /** @type {bigint} */
  get datagramsReceived() {
    return this.#handle[IDX_STATS_SESSION_DATAGRAMS_RECEIVED];
  }

  /** @type {bigint} */
  get datagramsSent() {
    return this.#handle[IDX_STATS_SESSION_DATAGRAMS_SENT];
  }

  /** @type {bigint} */
  get datagramsAcknowledged() {
    return this.#handle[IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED];
  }

  /** @type {bigint} */
  get datagramsLost() {
    return this.#handle[IDX_STATS_SESSION_DATAGRAMS_LOST];
  }

  toString() {
    return JSONStringify(this.toJSON());
  }

  toJSON() {
    return {
      __proto__: null,
      connected: this.isConnected,
      // We need to convert the values to strings because JSON does not
      // support BigInts.
      createdAt: `${this.createdAt}`,
      closingAt: `${this.closingAt}`,
      destroyedAt: `${this.destroyedAt}`,
      handshakeCompletedAt: `${this.handshakeCompletedAt}`,
      handshakeConfirmedAt: `${this.handshakeConfirmedAt}`,
      gracefulClosingAt: `${this.gracefulClosingAt}`,
      bytesReceived: `${this.bytesReceived}`,
      bytesSent: `${this.bytesSent}`,
      bidiInStreamCount: `${this.bidiInStreamCount}`,
      bidiOutStreamCount: `${this.bidiOutStreamCount}`,
      uniInStreamCount: `${this.uniInStreamCount}`,
      uniOutStreamCount: `${this.uniOutStreamCount}`,
      lossRetransmitCount: `${this.lossRetransmitCount}`,
      maxBytesInFlights: `${this.maxBytesInFlights}`,
      bytesInFlight: `${this.bytesInFlight}`,
      blockCount: `${this.blockCount}`,
      cwnd: `${this.cwnd}`,
      latestRtt: `${this.latestRtt}`,
      minRtt: `${this.minRtt}`,
      rttVar: `${this.rttVar}`,
      smoothedRtt: `${this.smoothedRtt}`,
      ssthresh: `${this.ssthresh}`,
      datagramsReceived: `${this.datagramsReceived}`,
      datagramsSent: `${this.datagramsSent}`,
      datagramsAcknowledged: `${this.datagramsAcknowledged}`,
      datagramsLost: `${this.datagramsLost}`,
    };
  }

  [kInspect](depth, options) {
    if (depth < 0)
      return this;

    const opts = {
      ...options,
      depth: options.depth == null ? null : options.depth - 1,
    };

    return `QuicSessionStats ${inspect({
      connected: this.isConnected,
      createdAt: this.createdAt,
      closingAt: this.closingAt,
      destroyedAt: this.destroyedAt,
      handshakeCompletedAt: this.handshakeCompletedAt,
      handshakeConfirmedAt: this.handshakeConfirmedAt,
      gracefulClosingAt: this.gracefulClosingAt,
      bytesReceived: this.bytesReceived,
      bytesSent: this.bytesSent,
      bidiInStreamCount: this.bidiInStreamCount,
      bidiOutStreamCount: this.bidiOutStreamCount,
      uniInStreamCount: this.uniInStreamCount,
      uniOutStreamCount: this.uniOutStreamCount,
      lossRetransmitCount: this.lossRetransmitCount,
      maxBytesInFlights: this.maxBytesInFlights,
      bytesInFlight: this.bytesInFlight,
      blockCount: this.blockCount,
      cwnd: this.cwnd,
      latestRtt: this.latestRtt,
      minRtt: this.minRtt,
      rttVar: this.rttVar,
      smoothedRtt: this.smoothedRtt,
      ssthresh: this.ssthresh,
      datagramsReceived: this.datagramsReceived,
      datagramsSent: this.datagramsSent,
      datagramsAcknowledged: this.datagramsAcknowledged,
      datagramsLost: this.datagramsLost,
    }, opts)}`;
  }

  /**
   * True if this QuicSessionStats object is still connected to the underlying
   * Session stats source. If this returns false, then the stats object is
   * no longer being updated and should be considered stale.
   * @returns {boolean}
   */
  get isConnected() {
    return !this.#disconnected;
  }

  [kFinishClose]() {
    // Snapshot the stats into a new BigUint64Array since the underlying
    // buffer will be destroyed.
    this.#handle = new BigUint64Array(this.#handle);
    this.#disconnected = true;
  }
}

class QuicStreamStats {
  /** @type {BigUint64Array} */
  #handle;
  /** type {boolean} */
  #disconnected = false;

  /**
   * @param {symbol} privateSymbol
   * @param {ArrayBuffer} buffer
   */
  constructor(privateSymbol, buffer) {
    // We use the kPrivateConstructor symbol to restrict the ability to
    // create new instances of QuicStreamStats to internal code.
    if (privateSymbol !== kPrivateConstructor) {
      throw new ERR_ILLEGAL_CONSTRUCTOR();
    }
    if (!isArrayBuffer(buffer)) {
      throw new ERR_INVALID_ARG_TYPE('buffer', ['ArrayBuffer'], buffer);
    }
    this.#handle = new BigUint64Array(buffer);
  }

  /** @type {bigint} */
  get createdAt() {
    return this.#handle[IDX_STATS_STREAM_CREATED_AT];
  }

  /** @type {bigint} */
  get receivedAt() {
    return this.#handle[IDX_STATS_STREAM_RECEIVED_AT];
  }

  /** @type {bigint} */
  get ackedAt() {
    return this.#handle[IDX_STATS_STREAM_ACKED_AT];
  }

  /** @type {bigint} */
  get closingAt() {
    return this.#handle[IDX_STATS_STREAM_CLOSING_AT];
  }

  /** @type {bigint} */
  get destroyedAt() {
    return this.#handle[IDX_STATS_STREAM_DESTROYED_AT];
  }

  /** @type {bigint} */
  get bytesReceived() {
    return this.#handle[IDX_STATS_STREAM_BYTES_RECEIVED];
  }

  /** @type {bigint} */
  get bytesSent() {
    return this.#handle[IDX_STATS_STREAM_BYTES_SENT];
  }

  /** @type {bigint} */
  get maxOffset() {
    return this.#handle[IDX_STATS_STREAM_MAX_OFFSET];
  }

  /** @type {bigint} */
  get maxOffsetAcknowledged() {
    return this.#handle[IDX_STATS_STREAM_MAX_OFFSET_ACK];
  }

  /** @type {bigint} */
  get maxOffsetReceived() {
    return this.#handle[IDX_STATS_STREAM_MAX_OFFSET_RECV];
  }

  /** @type {bigint} */
  get finalSize() {
    return this.#handle[IDX_STATS_STREAM_FINAL_SIZE];
  }

  toString() {
    return JSONStringify(this.toJSON());
  }

  toJSON() {
    return {
      __proto__: null,
      connected: this.isConnected,
      // We need to convert the values to strings because JSON does not
      // support BigInts.
      createdAt: `${this.createdAt}`,
      receivedAt: `${this.receivedAt}`,
      ackedAt: `${this.ackedAt}`,
      closingAt: `${this.closingAt}`,
      destroyedAt: `${this.destroyedAt}`,
      bytesReceived: `${this.bytesReceived}`,
      bytesSent: `${this.bytesSent}`,
      maxOffset: `${this.maxOffset}`,
      maxOffsetAcknowledged: `${this.maxOffsetAcknowledged}`,
      maxOffsetReceived: `${this.maxOffsetReceived}`,
      finalSize: `${this.finalSize}`,
    };
  }

  [kInspect](depth, options) {
    if (depth < 0)
      return this;

    const opts = {
      ...options,
      depth: options.depth == null ? null : options.depth - 1,
    };

    return `StreamStats ${inspect({
      connected: this.isConnected,
      createdAt: this.createdAt,
      receivedAt: this.receivedAt,
      ackedAt: this.ackedAt,
      closingAt: this.closingAt,
      destroyedAt: this.destroyedAt,
      bytesReceived: this.bytesReceived,
      bytesSent: this.bytesSent,
      maxOffset: this.maxOffset,
      maxOffsetAcknowledged: this.maxOffsetAcknowledged,
      maxOffsetReceived: this.maxOffsetReceived,
      finalSize: this.finalSize,
    }, opts)}`;
  }

  /**
   * True if this QuicStreamStats object is still connected to the underlying
   * Stream stats source. If this returns false, then the stats object is
   * no longer being updated and should be considered stale.
   * @returns {boolean}
   */
  get isConnected() {
    return !this.#disconnected;
  }

  [kFinishClose]() {
    // Snapshot the stats into a new BigUint64Array since the underlying
    // buffer will be destroyed.
    this.#handle = new BigUint64Array(this.#handle);
    this.#disconnected = true;
  }
}

module.exports = {
  QuicEndpointStats,
  QuicSessionStats,
  QuicStreamStats,
};
