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

goog.provide('shaka.hls.Attribute');
goog.provide('shaka.hls.Playlist');
goog.provide('shaka.hls.PlaylistType');
goog.provide('shaka.hls.Segment');
goog.provide('shaka.hls.Tag');

goog.require('goog.asserts');
goog.require('shaka.util.Error');


/**
 * HLS playlist class.
 */
shaka.hls.Playlist = class {
  /**
   * @param {!shaka.hls.PlaylistType} type
   * @param {!Array<shaka.hls.Tag>} tags
   * @param {!Array<shaka.hls.Segment>=} segments
   */
  constructor(type, tags, segments) {
    /** @const {shaka.hls.PlaylistType} */
    this.type = type;

    /** @const {!Array<!shaka.hls.Tag>} */
    this.tags = tags;

    /** @const {Array<!shaka.hls.Segment>} */
    this.segments = segments || null;
  }
};


/**
 * @enum {number}
 */
shaka.hls.PlaylistType = {
  MASTER: 0,
  MEDIA: 1,
};


/**
 * HLS tag class.
 */
shaka.hls.Tag = class {
  /**
   * @param {number} id
   * @param {string} name
   * @param {!Array<shaka.hls.Attribute>} attributes
   * @param {?string=} value
   */
  constructor(id, name, attributes, value = null) {
    /** @const {number} */
    this.id = id;

    /** @type {string} */
    this.name = name;

    /** @const {!Array<shaka.hls.Attribute>} */
    this.attributes = attributes;

    /** @const {?string} */
    this.value = value;
  }

  /**
   * Create the string representation of the tag.
   *
   * For the DRM system - the full tag needs to be passed down to the CDM.
   * There are two ways of doing this (1) save the original tag or (2) recreate
   * the tag.
   * As in some cases (like in tests) the tag never existed in string form, it
   * is far easier to recreate the tag from the parsed form.
   *
   * @param {?Set<string>=} attributesToSkip
   * @return {string}
   * @override
   */
  toString(attributesToSkip) {
    /**
     * @param {shaka.hls.Attribute} attr
     * @return {string}
     */
    const attrToStr = (attr) => {
      const isNumericAttr = !isNaN(Number(attr.value));
      const value = (isNumericAttr ? attr.value : '"' + attr.value + '"');
      return attr.name + '=' + value;
    };
    // A valid tag can only follow 1 of 4 patterns.
    //  1) <NAME>:<VALUE>
    //  2) <NAME>:<ATTRIBUTE LIST>
    //  3) <NAME>
    //  4) <NAME>:<VALUE>,<ATTRIBUTE_LIST>

    let tagStr = '#' + this.name;
    const appendages = this.attributes ? this.attributes.filter((attr) => {
      if (!attributesToSkip) {
        return true;
      }
      return !attributesToSkip.has(attr.name);
    }).map(attrToStr) : [];

    if (this.value) {
      appendages.unshift(this.value);
    }

    if (appendages.length > 0) {
      tagStr += ':' + appendages.join(',');
    }

    return tagStr;
  }

  /**
   * Create the string key of the tag.
   *
   * @param {boolean} keepAllAttributes
   * @return {string}
   */
  getTagKey(keepAllAttributes) {
    if (keepAllAttributes) {
      return this.toString();
    }
    const attributesToSkip = new Set()
        .add('AUDIO')
        .add('VIDEO')
        .add('SUBTITLES')
        .add('PATHWAY-ID')
        .add('GROUP-ID')
        .add('URI');
    return this.toString(attributesToSkip);
  }

  /**
   * Adds an attribute to an HLS Tag.
   *
   * @param {!shaka.hls.Attribute} attribute
   */
  addAttribute(attribute) {
    this.attributes.push(attribute);
  }


  /**
   * Gets the first attribute of the tag with a specified name.
   *
   * @param {string} name
   * @return {?shaka.hls.Attribute} attribute
   */
  getAttribute(name) {
    const attributes = this.attributes.filter((attr) => {
      return attr.name == name;
    });

    goog.asserts.assert(attributes.length < 2,
        'A tag should not have multiple attributes ' +
                        'with the same name!');

    if (attributes.length) {
      return attributes[0];
    } else {
      return null;
    }
  }

  /**
   * Gets the value of the first attribute of the tag with a specified name.
   * If not found, returns an optional default value.
   *
   * @param {string} name
   * @param {string=} defaultValue
   * @return {?string}
   */
  getAttributeValue(name, defaultValue) {
    const attribute = this.getAttribute(name);
    return attribute ? attribute.value : (defaultValue || null);
  }


  /**
   * Finds the attribute and returns its value.
   * Throws an error if attribute was not found.
   *
   * @param {string} name
   * @return {string}
   */
  getRequiredAttrValue(name) {
    const attribute = this.getAttribute(name);
    if (!attribute) {
      throw new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.MANIFEST,
          shaka.util.Error.Code.HLS_REQUIRED_ATTRIBUTE_MISSING,
          name);
    }

    return attribute.value;
  }

  /**
   * Set the name of the tag. Used only for Preload hinted MAP tag.
   * @param {string} name
   */
  setName(name) {
    this.name = name;
  }
};


/**
 * HLS segment class.
 */
shaka.hls.Segment = class {
  /**
   * Creates an HLS segment object.
   *
   * @param {string} verbatimSegmentUri verbatim segment URI.
   * @param {!Array<shaka.hls.Tag>} tags
   * @param {!Array<shaka.hls.Tag>=} partialSegments
   */
  constructor(verbatimSegmentUri, tags, partialSegments=[]) {
    /** @const {!Array<shaka.hls.Tag>} */
    this.tags = tags;

    /** @const {?string} */
    this.verbatimSegmentUri = verbatimSegmentUri;

    /** @type {!Array<shaka.hls.Tag>} */
    this.partialSegments = partialSegments;
  }
};


/**
 * HLS Attribute class.
 */
shaka.hls.Attribute = class {
  /**
   * Creates an HLS attribute object.
   *
   * @param {string} name
   * @param {string} value
   */
  constructor(name, value) {
    /** @const {string} */
    this.name = name;

    /** @const {string} */
    this.value = value;
  }
};
