/**
 * 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 type { ISegmentInformation } from "../../../../../transports";
import isNullOrUndefined from "../../../../../utils/is_null_or_undefined";
import type { IEMSG } from "../../../../containers/isobmff";
import type { IIndexSegment } from "../../../utils/index_helpers";
import {
  fromIndexTime,
  getIndexSegmentEnd,
  toIndexTime,
} from "../../../utils/index_helpers";
import type ManifestBoundsCalculator from "../manifest_bounds_calculator";
import getInitSegment from "./get_init_segment";
import getSegmentsFromTimeline from "./get_segments_from_timeline";
import { constructRepresentationUrl } from "./tokens";

/**
 * Index property defined for a SegmentBase RepresentationIndex
 * This object contains every property needed to generate an ISegment for a
 * given media time.
 */
export interface IBaseIndex {
  /** 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;
  /**
   * URL base to access any segment.
   * Can contain token to replace to convert it to real URLs.
   * `null` if no URL exists.
   */
  segmentUrlTemplate: string | null;
  /** Number from which the first segments in this index starts with. */
  startNumber?: number | undefined;
  /** Number associated to the last segment in this index. */
  endNumber?: number | undefined;
  /** Every segments defined in this index. */
  timeline: IIndexSegment[];
  /**
   * 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 SegmentBase RepresentationIndex.
 * Most of the properties here are already defined in IBaseIndex.
 */
export interface IBaseIndexIndexArgument {
  timeline?: IIndexSegment[];
  timescale?: number;
  media?: string;
  indexRange?: [number, number];
  initialization?: { media?: string; range?: [number, number] };
  startNumber?: number;
  endNumber?: number;
  /**
   * 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;
}

/** Aditional context needed by a SegmentBase RepresentationIndex. */
export interface IBaseIndexContextArgument {
  /** 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;
  /** Allows to obtain the minimum and maximum positions of a content. */
  manifestBoundsCalculator: ManifestBoundsCalculator;
  /* Function that tells if an EMSG is whitelisted by the manifest */
  isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean;
}

/**
 * Add a new segment to the index.
 *
 * /!\ Mutate the given index
 * @param {Object} index
 * @param {Object} segmentInfos
 * @returns {Boolean} - true if the segment has been added
 */
function _addSegmentInfos(
  index: IBaseIndex,
  segmentInfos: {
    time: number;
    duration: number;
    timescale: number;
    count?: number;
    range?: [number, number];
  },
): boolean {
  if (segmentInfos.timescale !== index.timescale) {
    const { timescale } = index;
    index.timeline.push({
      start: (segmentInfos.time / segmentInfos.timescale) * timescale,
      duration: (segmentInfos.duration / segmentInfos.timescale) * timescale,
      repeatCount: segmentInfos.count === undefined ? 0 : segmentInfos.count,
      range: segmentInfos.range,
    });
  } else {
    index.timeline.push({
      start: segmentInfos.time,
      duration: segmentInfos.duration,
      repeatCount: segmentInfos.count === undefined ? 0 : segmentInfos.count,
      range: segmentInfos.range,
    });
  }
  return true;
}

export default class BaseRepresentationIndex implements IRepresentationIndex {
  /**
   * `true` if the list of segments is already known.
   * `false` if the initialization segment should be loaded (and the segments
   * added) first.
   * @see isInitialized method
   */
  private _isInitialized: boolean;

  /** Underlying structure to retrieve segment information. */
  private _index: IBaseIndex;

  /** Absolute start of the period, timescaled and converted to index time. */
  private _scaledPeriodStart: number;

  /** Absolute end of the period, timescaled and converted to index time. */
  private _scaledPeriodEnd: number | undefined;

  /** Allows to obtain the minimum and maximum positions of a content. */
  private _manifestBoundsCalculator: ManifestBoundsCalculator;

  /* Function that tells if an EMSG is whitelisted by the manifest */
  private _isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean;

  /**
   * @param {Object} index
   * @param {Object} context
   */
  constructor(index: IBaseIndexIndexArgument, context: IBaseIndexContextArgument) {
    const {
      periodStart,
      periodEnd,
      representationId,
      representationBitrate,
      isEMSGWhitelisted,
    } = context;
    const timescale = index.timescale ?? 1;

    const presentationTimeOffset = index.presentationTimeOffset ?? 0;
    const indexTimeOffset = presentationTimeOffset - periodStart * timescale;

    const initializationUrl =
      index.initialization?.media === undefined
        ? null
        : constructRepresentationUrl(
            index.initialization.media,
            representationId,
            representationBitrate,
          );

    const segmentUrlTemplate =
      index.media === undefined
        ? null
        : constructRepresentationUrl(
            index.media,
            representationId,
            representationBitrate,
          );

    // TODO If indexRange is either undefined or behind the initialization segment
    // the following logic will not work.
    // However taking the nth first bytes like `dash.js` does (where n = 1500) is
    // not straightforward as we would need to clean-up the segment after that.
    // The following logic corresponds to 100% of tested cases, so good enough for
    // now.
    let range: [number, number] | undefined;
    if (index.initialization !== undefined) {
      range = index.initialization.range;
    } else if (index.indexRange !== undefined) {
      range = [0, index.indexRange[0] - 1];
    }

    this._index = {
      indexRange: index.indexRange,
      indexTimeOffset,
      initialization: { url: initializationUrl, range },
      segmentUrlTemplate,
      startNumber: index.startNumber,
      endNumber: index.endNumber,
      timeline: index.timeline ?? [],
      timescale,
    };
    this._manifestBoundsCalculator = context.manifestBoundsCalculator;
    this._scaledPeriodStart = toIndexTime(periodStart, this._index);
    this._scaledPeriodEnd = isNullOrUndefined(periodEnd)
      ? undefined
      : toIndexTime(periodEnd, this._index);
    this._isInitialized = this._index.timeline.length > 0;
    this._isEMSGWhitelisted = isEMSGWhitelisted;
  }

  /**
   * Construct init Segment.
   * @returns {Object}
   */
  getInitSegment(): ISegment {
    return getInitSegment(this._index, this._isEMSGWhitelisted);
  }

  /**
   * Get the list of segments that are currently available from the `from`
   * position, in seconds, ending `dur` seconds after that position.
   *
   * Note that if not already done, you might need to "initialize" the
   * `BaseRepresentationIndex` first so that the list of available segments
   * is known.
   *
   * @see isInitialized for more information on `BaseRepresentationIndex`
   * initialization.
   * @param {Number} from
   * @param {Number} dur
   * @returns {Array.<Object>}
   */
  getSegments(from: number, dur: number): ISegment[] {
    return getSegmentsFromTimeline(
      this._index,
      from,
      dur,
      this._manifestBoundsCalculator,
      this._scaledPeriodEnd,
      this._isEMSGWhitelisted,
    );
  }

  /**
   * Returns false as no Segment-Base based index should need to be refreshed.
   * @returns {Boolean}
   */
  shouldRefresh(): false {
    return false;
  }

  /**
   * Returns first position in index.
   * @returns {Number|null}
   */
  getFirstAvailablePosition(): number | null {
    const index = this._index;
    if (index.timeline.length === 0) {
      return null;
    }
    return fromIndexTime(
      Math.max(this._scaledPeriodStart, index.timeline[0].start),
      index,
    );
  }

  /**
   * Returns last position in index.
   * @returns {Number|null}
   */
  getLastAvailablePosition(): number | null {
    const { timeline } = this._index;
    if (timeline.length === 0) {
      return null;
    }
    const lastTimelineElement = timeline[timeline.length - 1];
    const lastTime = Math.min(
      getIndexSegmentEnd(lastTimelineElement, null, this._scaledPeriodEnd),
      this._scaledPeriodEnd ?? Infinity,
    );
    return fromIndexTime(lastTime, this._index);
  }

  /**
   * 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 `BaseRepresentationIndex` because all segments should
   * be directly available.
   * @returns {boolean}
   */
  awaitSegmentBetween(): false {
    return false;
  }

  /**
   * Segments in a segmentBase scheme should stay available.
   * @returns {Boolean|undefined}
   */
  isSegmentStillAvailable(): true {
    return true;
  }

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

  /**
   * Returns `false` as a `BaseRepresentationIndex` should not be dynamic and as
   * such segments should never fall out-of-sync.
   * @returns {Boolean}
   */
  canBeOutOfSyncError(): false {
    return false;
  }

  /**
   * Returns `true` as SegmentBase are not dynamic and as such no new segment
   * should become available in the future.
   * @returns {Boolean}
   */
  isStillAwaitingFutureSegments(): false {
    return false;
  }

  /**
   * No segment in a `BaseRepresentationIndex` are known initially.
   * It is only defined generally in an "index segment" that will thus need to
   * be first loaded and parsed.
   *
   * Once the index segment or equivalent has been parsed, the `initializeIndex`
   * method have to be called with the corresponding segment information so the
   * `BaseRepresentationIndex` can be considered as "initialized" (and so this
   * method can return `true`).
   * Until then this method will return `false` and segments linked to that
   * Representation may be missing.
   * @returns {Boolean}
   */
  isInitialized(): boolean {
    return this._isInitialized;
  }

  /**
   * No segment in a `BaseRepresentationIndex` are known initially.
   *
   * It is only defined generally in an "index segment" that will thus need to
   * be first loaded and parsed.
   * Until then, this `BaseRepresentationIndex` is considered as `uninitialized`
   * (@see isInitialized).
   *
   * Once that those information are available, the present
   * `BaseRepresentationIndex` can be "initialized" by adding that parsed
   * segment information through this method.
   * @param {Array.<Object>} indexSegments
   * @returns {Array.<Object>}
   */
  initialize(indexSegments: ISegmentInformation[]): void {
    if (this._isInitialized) {
      return;
    }
    for (let i = 0; i < indexSegments.length; i++) {
      _addSegmentInfos(this._index, indexSegments[i]);
    }
    this._isInitialized = true;
  }

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

  /**
   * 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 { timeline, timescale } = this._index;
    const firstElementInTimeline = timeline[0];
    if (firstElementInTimeline === undefined) {
      return undefined;
    }
    return {
      duration: firstElementInTimeline.duration / timescale,
      isPrecize: false,
    };
  }

  /**
   * Replace in-place this `BaseRepresentationIndex` information by the
   * information from another one.
   * @param {Object} newIndex
   */
  _replace(newIndex: BaseRepresentationIndex): void {
    this._index = newIndex._index;
    this._isInitialized = newIndex._isInitialized;
    this._scaledPeriodEnd = newIndex._scaledPeriodEnd;
    this._isEMSGWhitelisted = newIndex._isEMSGWhitelisted;
  }

  _update(): void {
    log.error("Base RepresentationIndex: Cannot update a SegmentList");
  }
}
