/*! @license
 * MSS Transmuxer
 * Copyright 2015 Dash Industry Forum
 * SPDX-License-Identifier: BSD-3-Clause
 */

/*
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * - Neither the name of the Dash Industry Forum nor the names of its
 *   contributors may be used to endorse or promote products derived from this
 *   software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS”
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

// cspell:words crypted usertype

goog.provide('shaka.transmuxer.MssTransmuxer');

goog.require('shaka.media.Capabilities');
goog.require('shaka.transmuxer.TransmuxerEngine');
goog.require('shaka.util.BufferUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.dependencies');

goog.requireType('shaka.media.SegmentReference');


/**
 * @implements {shaka.extern.Transmuxer}
 * @export
 */
shaka.transmuxer.MssTransmuxer = class {
  /**
   * @param {string} mimeType
   */
  constructor(mimeType) {
    /** @private {string} */
    this.originalMimeType_ = mimeType;

    /** @private {?ISOBoxer} */
    this.isoBoxer_ = shaka.dependencies.isoBoxer();

    if (this.isoBoxer_) {
      this.addSpecificBoxProcessor_();
    }
  }

  /**
   * Add specific box processor for codem-isoboxer
   *
   * @private
   */
  addSpecificBoxProcessor_() {
    // eslint-disable-next-line no-restricted-syntax
    this.isoBoxer_.addBoxProcessor('saio', function() {
      // eslint-disable-next-line no-invalid-this
      const box = /** @type {!ISOBox} */(this);
      box._procFullBox();
      if (box.flags & 1) {
        box._procField('aux_info_type', 'uint', 32);
        box._procField('aux_info_type_parameter', 'uint', 32);
      }
      box._procField('entry_count', 'uint', 32);
      box._procFieldArray('offset', box.entry_count, 'uint',
          (box.version === 1) ? 64 : 32);
    });
    // eslint-disable-next-line no-restricted-syntax
    this.isoBoxer_.addBoxProcessor('saiz', function() {
      // eslint-disable-next-line no-invalid-this
      const box = /** @type {!ISOBox} */(this);
      box._procFullBox();
      if (box.flags & 1) {
        box._procField('aux_info_type', 'uint', 32);
        box._procField('aux_info_type_parameter', 'uint', 32);
      }
      box._procField('default_sample_info_size', 'uint', 8);
      box._procField('sample_count', 'uint', 32);
      if (box.default_sample_info_size === 0) {
        box._procFieldArray('sample_info_size',
            box.sample_count, 'uint', 8);
      }
    });
    // eslint-disable-next-line no-restricted-syntax
    const sencProcessor = function() {
      // eslint-disable-next-line no-invalid-this
      const box = /** @type {!ISOBox} */(this);
      box._procFullBox();
      if (box.flags & 1) {
        box._procField('AlgorithmID', 'uint', 24);
        box._procField('IV_size', 'uint', 8);
        box._procFieldArray('KID', 16, 'uint', 8);
      }
      box._procField('sample_count', 'uint', 32);
      // eslint-disable-next-line no-restricted-syntax
      box._procEntries('entry', box.sample_count, function(entry) {
        // eslint-disable-next-line no-invalid-this
        const boxEntry = /** @type {!ISOBox} */(this);
        boxEntry._procEntryField(entry, 'InitializationVector', 'data', 8);
        if (boxEntry.flags & 2) {
          boxEntry._procEntryField(entry, 'NumberOfEntries', 'uint', 16);
          boxEntry._procSubEntries(entry, 'clearAndCryptedData',
              // eslint-disable-next-line no-restricted-syntax
              entry.NumberOfEntries, function(clearAndCryptedData) {
                // eslint-disable-next-line no-invalid-this
                const subBoxEntry = /** @type {!ISOBox} */(this);
                subBoxEntry._procEntryField(clearAndCryptedData,
                    'BytesOfClearData', 'uint', 16);
                subBoxEntry._procEntryField(clearAndCryptedData,
                    'BytesOfEncryptedData', 'uint', 32);
              });
        }
      });
    };
    this.isoBoxer_.addBoxProcessor('senc', sencProcessor);
    // eslint-disable-next-line no-restricted-syntax
    this.isoBoxer_.addBoxProcessor('uuid', function() {
      const MssTransmuxer = shaka.transmuxer.MssTransmuxer;
      // eslint-disable-next-line no-invalid-this
      const box = /** @type {!ISOBox} */(this);
      let isSENC = true;
      for (let i = 0; i < 16; i++) {
        if (box.usertype[i] !== MssTransmuxer.UUID_SENC_[i]) {
          isSENC = false;
        }
        // Add support for other user types here
      }

      if (isSENC) {
        if (box._parsing) {
          // Convert this box to sepiff for later processing.
          // See processMediaSegment_ function.
          box.type = 'sepiff';
        }
        // eslint-disable-next-line no-restricted-syntax, no-invalid-this
        sencProcessor.call(/** @type {!ISOBox} */(this));
      }
    });
  }


  /**
   * @override
   * @export
   */
  destroy() {
    // Nothing
  }


  /**
   * Check if the mime type and the content type is supported.
   * @param {string} mimeType
   * @param {string=} contentType
   * @return {boolean}
   * @override
   * @export
   */
  isSupported(mimeType, contentType) {
    const Capabilities = shaka.media.Capabilities;

    const isMss = mimeType.startsWith('mss/');

    if (!this.isoBoxer_ || !isMss) {
      return false;
    }

    if (contentType) {
      return Capabilities.isTypeSupported(
          this.convertCodecs(contentType, mimeType));
    }

    const ContentType = shaka.util.ManifestParserUtils.ContentType;

    const audioMime = this.convertCodecs(ContentType.AUDIO, mimeType);
    const videoMime = this.convertCodecs(ContentType.VIDEO, mimeType);
    return Capabilities.isTypeSupported(audioMime) ||
        Capabilities.isTypeSupported(videoMime);
  }


  /**
   * @override
   * @export
   */
  convertCodecs(contentType, mimeType) {
    return mimeType.replace('mss/', '');
  }


  /**
   * @override
   * @export
   */
  getOriginalMimeType() {
    return this.originalMimeType_;
  }


  /**
   * @override
   * @export
   */
  transmux(data, stream, reference) {
    if (!reference) {
      // Init segment doesn't need transmux
      return Promise.resolve(shaka.util.BufferUtils.toUint8(data));
    }
    if (!stream.mssPrivateData) {
      return Promise.reject(new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.MEDIA,
          shaka.util.Error.Code.MSS_MISSING_DATA_FOR_TRANSMUXING,
          reference ? reference.getUris()[0] : null));
    }
    try {
      const transmuxedData = this.processMediaSegment_(
          data, stream, reference);
      return Promise.resolve(transmuxedData);
    } catch (exception) {
      if (exception instanceof shaka.util.Error) {
        return Promise.reject(exception);
      }
      return Promise.reject(new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.MEDIA,
          shaka.util.Error.Code.MSS_TRANSMUXING_FAILED,
          reference ? reference.getUris()[0] : null));
    }
  }

  /**
   * Process a media segment from a data and stream.
   * @param {BufferSource} data
   * @param {shaka.extern.Stream} stream
   * @param {shaka.media.SegmentReference} reference
   * @return {!Uint8Array}
   * @private
   */
  processMediaSegment_(data, stream, reference) {
    let i;
    /** @type {!ISOFile} */
    const isoFile = this.isoBoxer_.parseBuffer(data);
    // Update track_Id in tfhd box
    const tfhd = isoFile.fetch('tfhd');
    tfhd.track_ID = stream.id + 1;
    // Add tfdt box
    let tfdt = isoFile.fetch('tfdt');
    const traf = isoFile.fetch('traf');
    if (tfdt === null) {
      tfdt = this.isoBoxer_.createFullBox('tfdt', traf, tfhd);
      tfdt.version = 1;
      tfdt.flags = 0;
      const timescale = stream.mssPrivateData.timescale;
      const startTime = reference.startTime;
      tfdt.baseMediaDecodeTime = Math.floor(startTime * timescale);
    }
    const trun = isoFile.fetch('trun');
    // Process tfxd boxes
    // This box provide absolute timestamp but we take the segment start
    // time for tfdt
    let tfxd = isoFile.fetch('tfxd');
    if (tfxd) {
      tfxd._parent.boxes.splice(tfxd._parent.boxes.indexOf(tfxd), 1);
      tfxd = null;
    }
    let tfrf = isoFile.fetch('tfrf');
    if (tfrf) {
      tfrf._parent.boxes.splice(tfrf._parent.boxes.indexOf(tfrf), 1);
      tfrf = null;
    }

    // If protected content in PIFF1.1 format
    // (sepiff box = Sample Encryption PIFF)
    // => convert sepiff box it into a senc box
    // => create saio and saiz boxes (if not already present)
    const sepiff = isoFile.fetch('sepiff');
    if (sepiff !== null) {
      sepiff.type = 'senc';
      sepiff.usertype = undefined;

      let saio = isoFile.fetch('saio');
      if (saio === null) {
        // Create Sample Auxiliary Information Offsets Box box (saio)
        saio = this.isoBoxer_.createFullBox('saio', traf);
        saio.version = 0;
        saio.flags = 0;
        saio.entry_count = 1;
        saio.offset = [0];
        const saiz = this.isoBoxer_.createFullBox('saiz', traf);
        saiz.version = 0;
        saiz.flags = 0;
        saiz.sample_count = sepiff.sample_count;
        saiz.default_sample_info_size = 0;
        saiz.sample_info_size = [];
        if (sepiff.flags & 0x02) {
          // Sub-sample encryption => set sample_info_size for each sample
          for (i = 0; i < sepiff.sample_count; i += 1) {
            // 10 = 8 (InitializationVector field size) + 2
            // (subsample_count field size)
            // 6 = 2 (BytesOfClearData field size) + 4
            // (BytesOfEncryptedData field size)
            const entry = /** @type {!ISOEntry} */ (sepiff.entry[i]);
            saiz.sample_info_size[i] = 10 + (6 * entry.NumberOfEntries);
          }
        } else {
          // No sub-sample encryption => set default
          // sample_info_size = InitializationVector field size (8)
          saiz.default_sample_info_size = 8;
        }
      }
    }

    // set tfhd.base-data-offset-present to false
    tfhd.flags &= 0xFFFFFE;
    // set tfhd.default-base-is-moof to true
    tfhd.flags |= 0x020000;
    // set trun.data-offset-present to true
    trun.flags |= 0x000001;

    // Update trun.data_offset field that corresponds to first data byte
    // (inside mdat box)
    const moof = isoFile.fetch('moof');
    const length = moof.getLength();
    trun.data_offset = length + 8;

    // Update saio box offset field according to new senc box offset
    const saio = isoFile.fetch('saio');
    if (saio !== null) {
      const trafPosInMoof = this.getBoxOffset_(moof, 'traf');
      const sencPosInTraf = this.getBoxOffset_(traf, 'senc');
      // Set offset from begin fragment to the first IV field in senc box
      // 16 = box header (12) + sample_count field size (4)
      saio.offset[0] = trafPosInMoof + sencPosInTraf + 16;
    }

    return shaka.util.BufferUtils.toUint8(isoFile.write());
  }

  /**
   * This function returns the offset of the 1st byte of a child box within
   * a container box.
   *
   * @param {ISOBox} parent
   * @param {string} type
   * @return {number}
   * @private
   */
  getBoxOffset_(parent, type) {
    let offset = 8;
    for (let i = 0; i < parent.boxes.length; i++) {
      if (parent.boxes[i].type === type) {
        return offset;
      }
      offset += parent.boxes[i].size;
    }
    return offset;
  }
};

/**
 * @private {!Uint8Array}
 */
shaka.transmuxer.MssTransmuxer.UUID_SENC_ = new Uint8Array([
  0xA2, 0x39, 0x4F, 0x52, 0x5A, 0x9B, 0x4F, 0x14,
  0xA2, 0x44, 0x6C, 0x42, 0x7C, 0x64, 0x8D, 0xF4,
]);

shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
    'mss/audio/mp4',
    () => new shaka.transmuxer.MssTransmuxer('mss/audio/mp4'),
    shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);
shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
    'mss/video/mp4',
    () => new shaka.transmuxer.MssTransmuxer('mss/video/mp4'),
    shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);
