/**
 * 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 { ISegment } from "../../manifest";
import { getDurationFromTrun, getTRAF } from "../../parsers/containers/isobmff";
import type { IChunkTimeInfo } from "../types";
import type { IISOBMFFBasicSegment } from "./isobmff";
import { parseTfrf, parseTfxd } from "./isobmff";

/** Information on future segments. */
export interface INextSegmentsInfos {
  /**
   * Difference between the latest and the earliest presentation time
   * available in that segment. The unit is in the corresponding timescale (see
   * `timescale` property).
   */
  duration: number;
  /**
   * Earliest presentation time available in that segment.
   * The unit is in the corresponding timescale (see `timescale` property).
   */
  time: number;
  /**
   * Allow to convert `duration` and `time` into seconds by dividing them to
   * that value.
   * e.g.:
   *   timeInSeconds = time / timescale
   *   durationInSeconds = duration / timescale
   *
   * Expressing those values relative to a "timescale" allows a greater
   * precision for the `time` and `duration` values.
   */
  timescale: number;
}

/**
 * Try to obtain time information from the given data.
 * @param {Uint8Array} data
 * @param {boolean} isChunked
 * @param {Object} segment
 * @param {boolean} isLive
 * @returns {Object}
 */
export default function extractTimingsInfos(
  data: Uint8Array,
  isChunked: boolean,
  initTimescale: number,
  segment: ISegment,
  isLive: boolean,
): {
  /** Possible information on segments after this one. */
  nextSegments: INextSegmentsInfos[];
  /** Time information on this segment */
  chunkInfos: IChunkTimeInfo | null;
  /**
   * Start time of this segment in the same timescale than the
   * initialization segment (so this segment can be patched with this
   * precize information).
   */
  scaledSegmentTime: number | undefined;
} {
  const nextSegments: INextSegmentsInfos[] = [];
  let chunkInfos: IChunkTimeInfo;

  let tfxdSegment: IISOBMFFBasicSegment | undefined;
  let tfrfSegments: IISOBMFFBasicSegment[] | undefined;
  if (isLive) {
    const traf = getTRAF(data);
    if (traf !== null) {
      tfrfSegments = parseTfrf(traf);
      tfxdSegment = parseTfxd(traf);
    } else {
      log.warn("smooth", "could not find traf atom");
    }
  }

  if (tfrfSegments !== undefined) {
    for (const tfrfSeg of tfrfSegments) {
      nextSegments.push({
        time: tfrfSeg.time,
        duration: tfrfSeg.duration,
        timescale: initTimescale,
      });
    }
  }

  if (tfxdSegment !== undefined) {
    chunkInfos = {
      time: tfxdSegment.time / initTimescale,
      duration: tfxdSegment.duration / initTimescale,
    };
    return { nextSegments, chunkInfos, scaledSegmentTime: tfxdSegment.time };
  }

  if (isChunked || !segment.complete) {
    return { nextSegments, chunkInfos: null, scaledSegmentTime: undefined };
  }

  const segmentDuration = segment.duration * initTimescale;

  // we could always make a mistake when reading a container.
  // If the estimate is too far from what the segment seems to imply, take
  // the segment infos instead.
  const maxDecodeTimeDelta = Math.min(initTimescale * 0.9, segmentDuration / 4);

  const trunDuration = getDurationFromTrun(data);
  const scaledSegmentTime =
    segment.privateInfos?.smoothMediaSegment !== undefined
      ? segment.privateInfos.smoothMediaSegment.time
      : Math.round(segment.time * initTimescale);
  if (
    trunDuration !== undefined &&
    Math.abs(trunDuration - segmentDuration) <= maxDecodeTimeDelta
  ) {
    chunkInfos = { time: segment.time, duration: trunDuration / initTimescale };
  } else {
    chunkInfos = { time: segment.time, duration: segment.duration };
  }
  return { nextSegments, chunkInfos, scaledSegmentTime };
}
