/**
 * 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 config from "../../../../config";
import type { IAdaptation, IPeriod } from "../../../../manifest";
import type { IReadOnlyPlaybackObserver } from "../../../../playback_observer";
import areCodecsCompatible from "../../../../utils/are_codecs_compatible";
import type { IRange } from "../../../../utils/ranges";
import { excludeFromRanges, insertInto } from "../../../../utils/ranges";
import type { SegmentSink } from "../../../segment_sinks";
import {
  getFirstSegmentAfterPeriod,
  getLastSegmentBeforePeriod,
  SegmentSinkOperation,
} from "../../../segment_sinks";
import type { ITrackSwitchingMode } from "../../adaptation";
import type { IPeriodStreamPlaybackObservation } from "../types";

export type IAdaptationSwitchStrategy =
  /** Do nothing special. */
  | { type: "continue"; value: undefined }
  /**
   * Clean the given ranges of time from the buffer, preferably avoiding time
   * around the current position to continue playback smoothly.
   */
  | { type: "clean-buffer"; value: Array<{ start: number; end: number }> }
  /**
   * Clean the given ranges of time from the buffer and try to flush the buffer
   * so that it is taken in account directly.
   */
  | { type: "flush-buffer"; value: Array<{ start: number; end: number }> }
  /** Reload completely the media buffers. */
  | { type: "needs-reload"; value: undefined };

export interface IAdaptationSwitchOptions {
  /** Behavior when a new video and/or audio codec is encountered. */
  onCodecSwitch: "continue" | "reload";
}

/**
 * Find out what to do when switching Adaptation, based on the current
 * situation.
 * @param {Object} segmentSink
 * @param {Object} period
 * @param {Object} adaptation
 * @param {Object} playbackObserver
 * @returns {Object}
 */
export default function getAdaptationSwitchStrategy(
  segmentSink: SegmentSink,
  period: IPeriod,
  adaptation: IAdaptation,
  switchingMode: ITrackSwitchingMode,
  playbackObserver: IReadOnlyPlaybackObserver<IPeriodStreamPlaybackObservation>,
  options: IAdaptationSwitchOptions,
): IAdaptationSwitchStrategy {
  if (
    segmentSink.codec !== undefined &&
    options.onCodecSwitch === "reload" &&
    !hasCompatibleCodec(adaptation, segmentSink.codec)
  ) {
    return { type: "needs-reload", value: undefined };
  }

  const inventory = segmentSink.getLastKnownInventory();

  const unwantedRange: IRange[] = [];
  for (const elt of inventory) {
    if (elt.infos.period.id === period.id && elt.infos.adaptation.id !== adaptation.id) {
      insertInto(unwantedRange, {
        start: elt.bufferedStart ?? elt.start,
        end: elt.bufferedEnd ?? elt.end,
      });
    }
  }

  const pendingOperations = segmentSink.getPendingOperations();
  for (const operation of pendingOperations) {
    if (operation.type === SegmentSinkOperation.Push) {
      const info = operation.value.inventoryInfos;
      if (info.period.id === period.id && info.adaptation.id !== adaptation.id) {
        const start = info.segment.time;
        const end = start + info.segment.duration;
        insertInto(unwantedRange, { start, end });
      }
    }
  }

  // Continue if we have no other Adaptation buffered in the current Period
  if (unwantedRange.length === 0) {
    return { type: "continue", value: undefined };
  }

  if (switchingMode === "reload") {
    const readyState = playbackObserver.getReadyState();
    if (readyState === undefined || readyState > 1) {
      return { type: "needs-reload", value: undefined };
    }
  }

  // From here, clean-up data from the previous Adaptation, if one

  const shouldCleanAll = switchingMode === "direct";

  const rangesToExclude = [];

  // First, we don't want to accidentally remove some segments from the previous
  // Period (which overlap a little with this one)

  /** Last segment before one for the current period. */
  const lastSegmentBefore = getLastSegmentBeforePeriod(inventory, period);
  if (
    lastSegmentBefore !== null &&
    (lastSegmentBefore.bufferedEnd === undefined ||
      period.start - lastSegmentBefore.bufferedEnd < 1)
  ) {
    // Close to Period's start
    // Exclude data close to the period's start to avoid cleaning
    // to much
    rangesToExclude.push({ start: 0, end: period.start + 1 });
  }

  if (!shouldCleanAll) {
    // Exclude data around current position to avoid decoding issues
    const bufferType = adaptation.type;
    const { ADAP_REP_SWITCH_BUFFER_PADDINGS } = config.getCurrent();
    /** Ranges that won't be cleaned from the current buffer. */
    const paddingBefore = ADAP_REP_SWITCH_BUFFER_PADDINGS[bufferType].before ?? 0;
    const paddingAfter = ADAP_REP_SWITCH_BUFFER_PADDINGS[bufferType].after ?? 0;

    let currentTime = playbackObserver.getCurrentTime();
    if (currentTime === undefined) {
      // TODO current position might be old. A better solution should be found.
      const lastObservation = playbackObserver.getReference().getValue();
      currentTime = lastObservation.position.getPolled();
    }

    rangesToExclude.push({
      start: currentTime - paddingBefore,
      end: currentTime + paddingAfter,
    });
  }

  // Now remove possible small range from the end if there is a segment from the
  // next Period
  if (period.end !== undefined) {
    /** first segment after for the current period. */
    const firstSegmentAfter = getFirstSegmentAfterPeriod(inventory, period);
    if (
      firstSegmentAfter !== null &&
      (firstSegmentAfter.bufferedStart === undefined ||
        firstSegmentAfter.bufferedStart - period.end < 1)
    ) {
      // Close to Period's end
      rangesToExclude.push({ start: period.end - 1, end: Number.MAX_VALUE });
    }
  }
  const toRemove = excludeFromRanges(unwantedRange, rangesToExclude);

  if (toRemove.length === 0) {
    return { type: "continue", value: undefined };
  }

  return shouldCleanAll && adaptation.type !== "text"
    ? { type: "flush-buffer", value: toRemove }
    : { type: "clean-buffer", value: toRemove };
}

/**
 * Returns `true` if at least one codec of the Representations in the given
 * Adaptation has a codec compatible with the given SegmentSink's codec.
 * @param {Object} adaptation
 * @param {string} segmentSinkCodec
 * @returns {boolean}
 */
function hasCompatibleCodec(adaptation: IAdaptation, segmentSinkCodec: string): boolean {
  return adaptation.representations.some(
    (rep) =>
      rep.isPlayable() === true &&
      areCodecsCompatible(rep.getMimeTypeString(), segmentSinkCodec),
  );
}
