/*
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import log from "../../../../../log";
import type { IRepresentationIndex, ISegment } from "../../../../../manifest";
import isNullOrUndefined from "../../../../../utils/is_null_or_undefined";
import type { IEMSG } from "../../../../containers/isobmff";
import { getTimescaledRange } from "../../../utils/index_helpers";
import getInitSegment from "./get_init_segment";
import { constructRepresentationUrl } from "./tokens";

/**
 * Index property defined for a SegmentList RepresentationIndex
 * This object contains every property needed to generate an ISegment for a
 * given media time.
 */
export interface IListIndex {
  /**
   * Duration of each element in the list, in the timescale given (see
   * timescale and list properties.)
   */
  duration: number;
  /** Byte range for a possible index of segments in the server. */
  indexRange?: [number, number] | undefined;
  /**
   * Temporal offset, in the current timescale (see timescale), to add to the
   * presentation time (time a segment has at decoding time) to obtain the
   * corresponding media time (original time of the media segment in the index
   * and on the media file).
   * For example, to look for a segment beginning at a second `T` on a
   * HTMLMediaElement, we actually will look for a segment in the index
   * beginning at:
   * ```
   * T * timescale + indexTimeOffset
   * ```
   */
  indexTimeOffset: number;
  /** Information on the initialization segment. */
  initialization?:
    | {
        /**
         * URL path, to add to the wanted CDN, to access the initialization segment.
         * `null` if no URL exists.
         */
        url: string | null;
        /** possible byte range to request it. */
        range?: [number, number] | undefined;
      }
    | undefined;
  /** Information on the list of segments for this index. */
  list: Array<{
    /**
     * URL path, to add to the wanted CDN, to access this media segment.
     * `null` if no URL exists.
     */
    url: string | null;
    /** Possible byte-range of the segment. */
    mediaRange?: [number, number] | undefined;
  }>;
  /**
   * Timescale to convert a time given here into seconds.
   * This is done by this simple operation:
   * ``timeInSeconds = timeInIndex * timescale``
   */
  timescale: number;
}

/**
 * `index` Argument for a SegmentList RepresentationIndex.
 * Most of the properties here are already defined in IListIndex.
 */
export interface IListIndexIndexArgument {
  duration?: number | undefined;
  indexRange?: [number, number] | undefined;
  initialization?: {
    media?: string | undefined;
    range?: [number, number] | undefined;
  };
  list: Array<{
    media?: string | undefined;
    mediaRange?: [number, number] | undefined;
  }>;
  /**
   * Offset present in the index to convert from the mediaTime (time declared in
   * the media segments and in this index) to the presentationTime (time wanted
   * when decoding the segment).  Basically by doing something along the line
   * of:
   * ```
   * presentationTimeInSeconds =
   *   mediaTimeInSeconds -
   *   presentationTimeOffsetInSeconds +
   *   periodStartInSeconds
   * ```
   * The time given here is in the current
   * timescale (see timescale)
   */
  presentationTimeOffset?: number | undefined;
  timescale?: number | undefined;
}

/** Aditional context needed by a SegmentList RepresentationIndex. */
export interface IListIndexContextArgument {
  /** Start of the period concerned by this RepresentationIndex, in seconds. */
  periodStart: number;
  /** End of the period concerned by this RepresentationIndex, in seconds. */
  periodEnd: number | undefined;
  /** ID of the Representation concerned. */
  representationId?: string | undefined;
  /** Bitrate of the Representation concerned. */
  representationBitrate?: number | undefined;
  /* Function that tells if an EMSG is whitelisted by the manifest */
  isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean;
}

export default class ListRepresentationIndex implements IRepresentationIndex {
  /** Underlying structure to retrieve segment information. */
  private _index: IListIndex;
  /** Start of the period concerned by this RepresentationIndex, in seconds. */
  protected _periodStart: number;
  /** End of the period concerned by this RepresentationIndex, in seconds. */
  protected _periodEnd: number | undefined;
  /* Function that tells if an EMSG is whitelisted by the manifest */
  private _isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean;

  /**
   * @param {Object} index
   * @param {Object} context
   */
  constructor(index: IListIndexIndexArgument, context: IListIndexContextArgument) {
    if (index.duration === undefined) {
      throw new Error("Invalid SegmentList: no duration");
    }

    const {
      periodStart,
      periodEnd,
      representationId,
      representationBitrate,
      isEMSGWhitelisted,
    } = context;
    this._isEMSGWhitelisted = isEMSGWhitelisted;
    this._periodStart = periodStart;
    this._periodEnd = periodEnd;
    const presentationTimeOffset = index.presentationTimeOffset ?? 0;
    const timescale = index.timescale ?? 1;
    const indexTimeOffset = presentationTimeOffset - periodStart * timescale;

    const initializationUrl =
      index.initialization?.media === undefined
        ? null
        : constructRepresentationUrl(
            index.initialization.media,
            representationId,
            representationBitrate,
          );
    const list = index.list.map((lItem) => ({
      url:
        lItem.media === undefined
          ? null
          : constructRepresentationUrl(
              lItem.media,
              representationId,
              representationBitrate,
            ),
      mediaRange: lItem.mediaRange,
    }));
    this._index = {
      list,
      timescale,
      duration: index.duration,
      indexTimeOffset,
      indexRange: index.indexRange,
      initialization: isNullOrUndefined(index.initialization)
        ? undefined
        : { url: initializationUrl, range: index.initialization.range },
    };
  }

  /**
   * Construct init Segment.
   * @returns {Object}
   */
  getInitSegment(): ISegment {
    const initSegment = getInitSegment(this._index);
    if (initSegment.privateInfos === undefined) {
      initSegment.privateInfos = {};
    }
    initSegment.privateInfos.isEMSGWhitelisted = this._isEMSGWhitelisted;
    return initSegment;
  }

  /**
   * @param {Number} fromTime
   * @param {Number} dur
   * @returns {Array.<Object>}
   */
  getSegments(fromTime: number, dur: number): ISegment[] {
    const index = this._index;
    const { duration, list, timescale } = index;
    const durationInSeconds = duration / timescale;
    const fromTimeInPeriod = fromTime - this._periodStart;
    const [up, to] = getTimescaledRange(fromTimeInPeriod, dur, timescale);

    const length = Math.min(list.length - 1, Math.floor(to / duration));
    const segments: ISegment[] = [];
    let i = Math.floor(up / duration);
    while (i <= length) {
      const range = list[i].mediaRange;
      const url = list[i].url;
      const time = i * durationInSeconds + this._periodStart;
      const segment = {
        id: String(i),
        time,
        isInit: false,
        range,
        duration: durationInSeconds,
        timescale: 1 as const,
        end: time + durationInSeconds,
        url,
        timestampOffset: -(index.indexTimeOffset / timescale),
        complete: true,
        privateInfos: { isEMSGWhitelisted: this._isEMSGWhitelisted },
      };
      segments.push(segment);
      i++;
    }
    return segments;
  }

  /**
   * Returns whether the Manifest should be refreshed based on the
   * `ListRepresentationIndex`'s state and the time range the player is
   * currently considering.
   * @param {Number} _fromTime
   * @param {Number} _toTime
   * @returns {Boolean}
   */
  shouldRefresh(_fromTime: number, _toTime: number): boolean {
    // DASH Manifests are usually refreshed through other means, i.e. thanks to
    // the `minimumUpdatePeriod` attribute.
    // Moreover, SegmentList are usually only found in static MPDs.
    return false;
  }

  /**
   * Returns first position in this index, in seconds.
   * @returns {Number}
   */
  getFirstAvailablePosition(): number {
    return this._periodStart;
  }

  /**
   * Returns last position in this index, in seconds.
   * @returns {Number}
   */
  getLastAvailablePosition(): number {
    const index = this._index;
    const { duration, list } = index;
    return Math.min(
      (list.length * duration) / index.timescale + this._periodStart,
      this._periodEnd ?? Infinity,
    );
  }

  /**
   * Returns the absolute end in seconds this RepresentationIndex can reach once
   * all segments are available.
   * @returns {number|null|undefined}
   */
  getEnd(): number | null {
    return this.getLastAvailablePosition();
  }

  /**
   * Returns:
   *   - `true` if in the given time interval, at least one new segment is
   *     expected to be available in the future.
   *   - `false` either if all segments in that time interval are already
   *     available for download or if none will ever be available for it.
   *   - `undefined` when it is not possible to tell.
   *
   * Always `false` in a `ListRepresentationIndex` because all segments should
   * be directly available.
   * @returns {boolean}
   */
  awaitSegmentBetween(): false {
    return false;
  }

  /**
   * Returns true if a Segment returned by this index is still considered
   * available.
   * @returns {Boolean}
   */
  isSegmentStillAvailable(): true {
    return true;
  }

  /**
   * We do not check for discontinuity in SegmentList-based indexes.
   * @returns {null}
   */
  checkDiscontinuity(): null {
    return null;
  }

  /**
   * SegmentList should not be updated.
   * @returns {Boolean}
   */
  canBeOutOfSyncError(): false {
    return false;
  }

  /**
   * @returns {Boolean}
   */
  isStillAwaitingFutureSegments(): false {
    return false;
  }

  /**
   * @returns {Boolean}
   */
  isInitialized(): true {
    return true;
  }

  initialize(): void {
    log.error("dash", "A `ListRepresentationIndex` does not need to be initialized");
  }

  addPredictedSegments(): void {
    log.warn("dash", "Cannot add predicted segments to a `ListRepresentationIndex`");
  }

  /**
   * Returns the `duration` of each segment in the context of its Manifest (i.e.
   * as the Manifest anounces them, actual segment duration may be different due
   * to approximations), in seconds.
   *
   * NOTE: we could here do a median or a mean but I chose to be lazy (and
   * more performant) by returning the duration of the first element instead.
   * As `isPrecize` is `false`, the rest of the code should be notified that
   * this is only an approximation.
   * @returns {number}
   */
  getTargetSegmentDuration(): { duration: number; isPrecize: boolean } | undefined {
    const { duration, timescale } = this._index;
    return {
      duration: duration / timescale,
      isPrecize: true,
    };
  }

  /**
   * @param {Object} newIndex
   */
  _replace(newIndex: ListRepresentationIndex): void {
    this._index = newIndex._index;
  }

  _update(): void {
    log.error("dash", "A `ListRepresentationIndex` cannot be updated");
  }
}
