/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.cea.DtvccPacketBuilder');
goog.provide('shaka.cea.DtvccPacket');
goog.require('shaka.util.Error');
goog.requireType('shaka.cea.Cea708Service');


/**
 * CEA-708 DTVCC Packet Builder.
 * Builds packets based on Figure 5 CCP State Table in 5.2 of CEA-708-E.
 * Initially, there is no packet. When a DTVCC_PACKET_START payload is received,
 * a packet begins construction. The packet is considered "built" once all bytes
 * indicated in the header are read, and ignored if a new packet starts building
 * before the current packet is finished being built.
 */
shaka.cea.DtvccPacketBuilder = class {
  constructor() {
    /**
     * An array containing built DTVCC packets that are ready to be processed.
     * @private {!Array<!shaka.cea.DtvccPacket>}
     */
    this.builtPackets_ = [];

    /**
     * Stores the packet data for the current packet being processed, if any.
     * @private {?Array<!shaka.cea.Cea708Service.Cea708Byte>}
     */
    this.currentPacketBeingBuilt_ = null;

    /**
     * Keeps track of the number of bytes left to add in the current packet.
     * @private {number}
     */
    this.bytesLeftToAddInCurrentPacket_ = 0;
  }

  /**
   * @param {!shaka.cea.Cea708Service.Cea708Byte} cea708Byte
   */
  addByte(cea708Byte) {
    if (cea708Byte.type === shaka.cea.DtvccPacketBuilder.DTVCC_PACKET_START) {
      // If there was a packet being built that finished, it would have
      // already been added to the built packets when it finished. So if
      // there's an open packet at this point, it must be unfinished. As
      // per the spec, we don't deal with unfinished packets. So we ignore them.

      // A new packet should be opened.
      const packetSize = cea708Byte.value & 0x3f;

      // As per spec, number of packet data bytes to follow is packetSize*2-1.
      this.bytesLeftToAddInCurrentPacket_ = packetSize * 2 - 1;
      this.currentPacketBeingBuilt_ = [];
      return;
    }

    if (!this.currentPacketBeingBuilt_) {
      // There is no packet open. Then an incoming byte should not
      // have come in at all. Ignore it.
      return;
    }

    if (this.bytesLeftToAddInCurrentPacket_ > 0) {
      this.currentPacketBeingBuilt_.push(cea708Byte);
      this.bytesLeftToAddInCurrentPacket_--;
    }

    if (this.bytesLeftToAddInCurrentPacket_ === 0) {
      // Current packet is complete and ready for processing.
      const packet = new shaka.cea.DtvccPacket(this.currentPacketBeingBuilt_);
      this.builtPackets_.push(packet);
      this.currentPacketBeingBuilt_ = null;
      this.bytesLeftToAddInCurrentPacket_ = 0;
    }
  }

  /**
   * @return {!Array<!shaka.cea.DtvccPacket>}
   */
  getBuiltPackets() {
    return this.builtPackets_;
  }

  /** Clear built packets. */
  clearBuiltPackets() {
    this.builtPackets_ = [];
  }

  /** Clear built packets and packets in progress. */
  clear() {
    this.builtPackets_ = [];
    this.currentPacketBeingBuilt_ = [];
    this.bytesLeftToAddInCurrentPacket_ = 0;
  }
};


shaka.cea.DtvccPacket = class {
  /**
   * @param {!Array<!shaka.cea.Cea708Service.Cea708Byte>} packetData
   */
  constructor(packetData) {
    /**
     * Keeps track of the position to read the next byte from in the packet.
     * @private {number}
     */
    this.pos_ = 0;

    /**
     * Bytes that represent the data in the DTVCC packet.
     * @private {!Array<!shaka.cea.Cea708Service.Cea708Byte>}
     */
    this.packetData_ = packetData;
  }

  /**
   * @return {boolean}
   */
  hasMoreData() {
    return this.pos_ < this.packetData_.length;
  }

  /**
   * @return {number}
   */
  getPosition() {
    return this.pos_;
  }

  /**
   * Reads a byte from the packet. TODO CONSIDER RENAMING THIS TO BLOCK
   * @return {!shaka.cea.Cea708Service.Cea708Byte}
   * @throws {!shaka.util.Error}
   */
  readByte() {
    if (!this.hasMoreData()) {
      throw new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.TEXT,
          shaka.util.Error.Code.BUFFER_READ_OUT_OF_BOUNDS);
    }
    return this.packetData_[this.pos_++];
  }

  /**
   * Skips the provided number of blocks in the buffer.
   * @param {number} numBlocks
   * @throws {!shaka.util.Error}
   */
  skip(numBlocks) {
    if (this.pos_ + numBlocks > this.packetData_.length) {
      throw new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.TEXT,
          shaka.util.Error.Code.BUFFER_READ_OUT_OF_BOUNDS);
    }
    this.pos_ += numBlocks;
  }

  /**
   * Rewinds the provided number of blocks in the buffer.
   * @param {number} numBlocks
   * @throws {!shaka.util.Error}
   */
  rewind(numBlocks) {
    if (this.pos_ - numBlocks < 0) {
      throw new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.TEXT,
          shaka.util.Error.Code.BUFFER_READ_OUT_OF_BOUNDS);
    }
    this.pos_ -= numBlocks;
  }
};

/**
 * @const {number}
 */
shaka.cea.DtvccPacketBuilder.DTVCC_PACKET_DATA = 2;

/**
 * @const {number}
 */
shaka.cea.DtvccPacketBuilder.DTVCC_PACKET_START = 3;
