/**
 * 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 log from "../../log";
import type {
  IAdaptation,
  IManifest,
  IPeriod,
  IRepresentation,
  ISegment,
} from "../../manifest";
import type {
  ObservationPosition,
  IReadOnlyPlaybackObserver,
} from "../../playback_observer";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import noop from "../../utils/noop";
import type { IRange } from "../../utils/ranges";
import { getLeftSizeOfRange } from "../../utils/ranges";
import type { IReadOnlySharedReference } from "../../utils/reference";
import SharedReference from "../../utils/reference";
import type { CancellationSignal } from "../../utils/task_canceller";
import TaskCanceller from "../../utils/task_canceller";
import type { IBufferType } from "../segment_sinks";
import BufferBasedChooser from "./buffer_based_chooser";
import GuessBasedChooser from "./guess_based_chooser";
import NetworkAnalyzer from "./network_analyzer";
import BandwidthEstimator from "./utils/bandwidth_estimator";
import filterByBitrate from "./utils/filter_by_bitrate";
import filterByResolution from "./utils/filter_by_resolution";
import type { IResolutionInfo } from "./utils/filter_by_resolution";
import LastEstimateStorage, { ABRAlgorithmType } from "./utils/last_estimate_storage";
import type {
  IPendingRequestStoreBegin,
  IPendingRequestStoreProgress,
} from "./utils/pending_requests_store";
import PendingRequestsStore from "./utils/pending_requests_store";
import RepresentationScoreCalculator from "./utils/representation_score_calculator";
import selectOptimalRepresentation from "./utils/select_optimal_representation";

// Create default shared references

const limitResolutionDefaultRef = new SharedReference<IResolutionInfo | undefined>(
  undefined,
);
limitResolutionDefaultRef.finish();

const throttleBitrateDefaultRef = new SharedReference(Infinity);
throttleBitrateDefaultRef.finish();

/**
 * Select the most adapted Representation according to the network and buffer
 * metrics it receives.
 *
 * @param {Object} options - Initial configuration (see type definition)
 * @returns {Object} - Interface allowing to select a Representation.
 * @see IRepresentationEstimator
 */
export default function createAdaptiveRepresentationSelector(
  options: IAdaptiveRepresentationSelectorArguments,
): IRepresentationEstimator {
  /**
   * Allows to estimate the current network bandwidth.
   * One per active media type.
   */
  const bandwidthEstimators: Partial<Record<IBufferType, BandwidthEstimator>> = {};
  const { initialBitrates, throttlers, lowLatencyMode } = options;

  /**
   * Returns Object emitting Representation estimates as well as callbacks
   * allowing to helping it produce them.
   *
   * @see IRepresentationEstimator
   * @param {Object} context
   * @param {Object} currentRepresentation
   * @param {Object} representations
   * @param {Object} playbackObserver
   * @param {Object} stopAllEstimates
   * @returns {Array.<Object>}
   */
  return function getEstimates(
    context: { manifest: IManifest; period: IPeriod; adaptation: IAdaptation },
    currentRepresentation: IReadOnlySharedReference<IRepresentation | null>,
    representations: IReadOnlySharedReference<IRepresentation[]>,
    playbackObserver: IReadOnlyPlaybackObserver<IRepresentationEstimatorPlaybackObservation>,
    stopAllEstimates: CancellationSignal,
  ): IRepresentationEstimatorResponse {
    const { type } = context.adaptation;
    const bandwidthEstimator = _getBandwidthEstimator(type);
    const initialBitrate = initialBitrates[type] ?? 0;
    const filters = {
      limitResolution: throttlers.limitResolution[type] ?? limitResolutionDefaultRef,
      throttleBitrate: throttlers.throttleBitrate[type] ?? throttleBitrateDefaultRef,
    };
    return getEstimateReference(
      {
        bandwidthEstimator,
        context,
        currentRepresentation,
        filters,
        initialBitrate,
        playbackObserver,
        representations,
        lowLatencyMode,
      },
      stopAllEstimates,
    );
  };

  /**
   * Returns interface allowing to estimate network throughtput for a given type.
   * @param {string} bufferType
   * @returns {Object}
   */
  function _getBandwidthEstimator(bufferType: IBufferType): BandwidthEstimator {
    const originalBandwidthEstimator = bandwidthEstimators[bufferType];
    if (isNullOrUndefined(originalBandwidthEstimator)) {
      log.debug("ABR", "Creating new BandwidthEstimator", { bufferType });
      const bandwidthEstimator = new BandwidthEstimator();
      bandwidthEstimators[bufferType] = bandwidthEstimator;
      return bandwidthEstimator;
    }
    return originalBandwidthEstimator;
  }
}

/**
 * Estimate regularly the current network bandwidth and the best Representation
 * that can be played according to the current network and playback conditions.
 *
 * `getEstimateReference` only does estimations for a given type (e.g.
 * "audio", "video" etc.) and Period.
 *
 * If estimates for multiple types and/or Periods are needed, you should
 * call `getEstimateReference` as many times.
 *
 * This function returns a tuple:
 *   - the first element being the object through which estimates will be produced
 *   - the second element being callbacks that have to be triggered at various
 *     events to help it doing those estimates.
 *
 * @param {Object} args
 * @param {Object} stopAllEstimates
 * @returns {Array.<Object>}
 */
function getEstimateReference(
  {
    bandwidthEstimator,
    context,
    currentRepresentation,
    filters,
    initialBitrate,
    lowLatencyMode,
    playbackObserver,
    representations: representationsRef,
  }: IRepresentationEstimatorArguments,
  stopAllEstimates: CancellationSignal,
): IRepresentationEstimatorResponse {
  const scoreCalculator = new RepresentationScoreCalculator();
  const networkAnalyzer = new NetworkAnalyzer(initialBitrate ?? 0, lowLatencyMode);
  const requestsStore = new PendingRequestsStore();

  /**
   * Callback called each time a new segment is pushed, with the information on the
   * new pushed segment.
   */
  let onAddedSegment: (val: IAddedSegmentCallbackPayload) => void = noop;

  const callbacks: IRepresentationEstimatorCallbacks = {
    metrics: onMetric,
    requestBegin: onRequestBegin,
    requestProgress: onRequestProgress,
    requestEnd: onRequestEnd,
    addedSegment(val) {
      onAddedSegment(val);
    },
  };

  /**
   * `TaskCanceller` allowing to stop producing estimate.
   * This TaskCanceller is used both for restarting estimates with a new
   * configuration and to cancel them altogether.
   */
  let currentEstimatesCanceller = new TaskCanceller("ABR " + context.adaptation.type);
  currentEstimatesCanceller.linkToSignal(stopAllEstimates);

  // Create `SharedReference` on which estimates will be emitted.
  const estimateRef = createEstimateReference(
    representationsRef.getValue(),
    currentEstimatesCanceller.signal,
  );
  representationsRef.onUpdate(restartEstimatesProductionFromCurrentConditions, {
    clearSignal: stopAllEstimates,
  });

  return { estimates: estimateRef, callbacks };

  /**
   * Emit the ABR estimates on various events through the returned
   * `SharedReference`.
   * @param {Array.<Object>} unsortedRepresentations - All `Representation` that
   * the ABR logic can choose from.
   * @param {Object} innerCancellationSignal - When this `CancellationSignal`
   * emits, all events registered to to emit new estimates will be unregistered.
   * Note that the returned reference may still be used to produce new estimates
   * in the future if you want to even after this signal emits.
   * @returns {Object} - `SharedReference` through which ABR estimates will be
   * produced.
   */
  function createEstimateReference(
    unsortedRepresentations: IRepresentation[],
    innerCancellationSignal: CancellationSignal,
  ): SharedReference<IABREstimate> {
    if (unsortedRepresentations.length <= 1) {
      // There's only a single Representation. Just choose it.
      return new SharedReference<IABREstimate>({
        bitrate: undefined,
        representation: unsortedRepresentations[0],
        urgent: true,
        knownStableBitrate: undefined,
      });
    }

    /** If true, Representation estimates based on the buffer health might be used. */
    let allowBufferBasedEstimates = false;

    /** Ensure `Representation` objects are sorted by bitrates and only rely on this. */
    const sortedRepresentations = unsortedRepresentations.sort(
      (ra, rb) => ra.bitrate - rb.bitrate,
    );

    /**
     * Module calculating the optimal Representation based on the current
     * buffer's health (i.e. whether enough data is buffered, history of
     * buffer size etc.).
     */
    const bufferBasedChooser = new BufferBasedChooser(
      sortedRepresentations.map((r) => r.bitrate),
    );

    /** Store the previous estimate made here. */
    const prevEstimate = new LastEstimateStorage();

    /**
     * Module calculating the optimal Representation by "guessing it" with a
     * step-by-step algorithm.
     * Only used in very specific scenarios.
     */
    const guessBasedChooser = new GuessBasedChooser(scoreCalculator, prevEstimate);

    // get initial observation for initial estimate
    let lastPlaybackObservation = playbackObserver.getReference().getValue();

    /** Reference through which estimates are emitted. */
    const innerEstimateRef = new SharedReference<IABREstimate>(getCurrentEstimate());

    // Listen to playback observations
    playbackObserver.listen(
      (obs) => {
        lastPlaybackObservation = obs;
        updateEstimate();
      },
      { includeLastObservation: false, clearSignal: innerCancellationSignal },
    );

    onAddedSegment = function (val: IAddedSegmentCallbackPayload) {
      if (lastPlaybackObservation === null) {
        return;
      }
      const { position, speed } = lastPlaybackObservation;
      const timeRanges = val.buffered;
      const bufferGap = getLeftSizeOfRange(timeRanges, position.getWanted());
      const { representation } = val.content;
      const currentScore = scoreCalculator.getEstimate(representation);
      const currentBitrate = representation.bitrate;
      const observation = { bufferGap, currentBitrate, currentScore, speed };
      bufferBasedChooser.onAddedSegment(observation);
      updateEstimate();
    };
    innerCancellationSignal.register(() => {
      onAddedSegment = noop;
    });

    filters.throttleBitrate.onUpdate(updateEstimate, {
      clearSignal: innerCancellationSignal,
    });
    filters.limitResolution.onUpdate(updateEstimate, {
      clearSignal: innerCancellationSignal,
    });

    return innerEstimateRef;

    function updateEstimate(): void {
      innerEstimateRef.setValue(getCurrentEstimate());
    }

    /** Returns the actual estimate based on all methods and algorithm available. */
    function getCurrentEstimate(): IABREstimate {
      const { bufferGap, position, maximumPosition } = lastPlaybackObservation;
      const resolutionLimit = filters.limitResolution.getValue();
      const bitrateThrottle = filters.throttleBitrate.getValue();
      const currentRepresentationVal = currentRepresentation.getValue();

      const filteredReps = getFilteredRepresentations(
        sortedRepresentations,
        resolutionLimit,
        bitrateThrottle,
      );
      const requests = requestsStore.getRequests();
      const { bandwidthEstimate, bitrateChosen } = networkAnalyzer.getBandwidthEstimate(
        lastPlaybackObservation,
        bandwidthEstimator,
        currentRepresentationVal,
        requests,
        prevEstimate.bandwidth,
      );

      const stableRepresentation = scoreCalculator.getLastStableRepresentation();
      const knownStableBitrate =
        stableRepresentation === null
          ? undefined
          : stableRepresentation.bitrate /
            (lastPlaybackObservation.speed > 0 ? lastPlaybackObservation.speed : 1);

      const { ABR_ENTER_BUFFER_BASED_ALGO, ABR_EXIT_BUFFER_BASED_ALGO } =
        config.getCurrent();

      if (allowBufferBasedEstimates && bufferGap <= ABR_EXIT_BUFFER_BASED_ALGO) {
        allowBufferBasedEstimates = false;
      } else if (
        !allowBufferBasedEstimates &&
        isFinite(bufferGap) &&
        bufferGap >= ABR_ENTER_BUFFER_BASED_ALGO
      ) {
        allowBufferBasedEstimates = true;
      }

      /**
       * Representation chosen when considering only [pessimist] bandwidth
       * calculation.
       * This is a safe enough choice but might be lower than what the user
       * could actually profit from.
       */
      const chosenRepFromBandwidth = selectOptimalRepresentation(
        filteredReps,
        bitrateChosen,
      );

      /**
       * Current optimal Representation's bandwidth choosen by a buffer-based
       * adaptive algorithm.
       */
      const currentBufferBasedEstimate = bufferBasedChooser.getLastEstimate();

      let currentBestBitrate = chosenRepFromBandwidth.bitrate;

      /**
       * Representation chosen when considering the current buffer size.
       * If defined, takes precedence over `chosenRepFromBandwidth`.
       *
       * This is a very safe choice, yet it is very slow and might not be
       * adapted to cases where a buffer cannot be build, such as live contents.
       *
       * `null` if this buffer size mode is not enabled or if we don't have a
       * choice from it yet.
       */
      let chosenRepFromBufferSize: IRepresentation | null = null;
      if (
        allowBufferBasedEstimates &&
        currentBufferBasedEstimate !== undefined &&
        currentBufferBasedEstimate > currentBestBitrate
      ) {
        chosenRepFromBufferSize = selectOptimalRepresentation(
          filteredReps,
          currentBufferBasedEstimate,
        );
        currentBestBitrate = chosenRepFromBufferSize.bitrate;
      }

      /**
       * Representation chosen by the more adventurous `GuessBasedChooser`,
       * which iterates through Representations one by one until finding one
       * that cannot be "maintained".
       *
       * If defined, takes precedence over both `chosenRepFromBandwidth` and
       * `chosenRepFromBufferSize`.
       *
       * This is the riskiest choice (in terms of rebuffering chances) but is
       * only enabled when no other solution is adapted (for now, this just
       * applies for low-latency contents when playing close to the live
       * edge).
       *
       * `null` if not enabled or if there's currently no guess.
       */
      let chosenRepFromGuessMode: IRepresentation | null = null;
      if (
        lowLatencyMode &&
        currentRepresentationVal !== null &&
        context.manifest.isDynamic &&
        maximumPosition - position.getWanted() < 40
      ) {
        chosenRepFromGuessMode = guessBasedChooser.getGuess(
          sortedRepresentations,
          lastPlaybackObservation,
          currentRepresentationVal,
          currentBestBitrate,
          requests,
        );
      }

      if (
        chosenRepFromGuessMode !== null &&
        chosenRepFromGuessMode.bitrate > currentBestBitrate
      ) {
        log.debug("ABR", "new guess-based estimate", {
          bitrate: chosenRepFromGuessMode.bitrate,
          representation: chosenRepFromGuessMode.id,
        });
        prevEstimate.update(
          chosenRepFromGuessMode,
          bandwidthEstimate,
          ABRAlgorithmType.GuessBased,
        );
        return {
          bitrate: bandwidthEstimate,
          representation: chosenRepFromGuessMode,
          urgent:
            currentRepresentationVal === null ||
            chosenRepFromGuessMode.bitrate < currentRepresentationVal.bitrate,
          knownStableBitrate,
        };
      } else if (chosenRepFromBufferSize !== null) {
        log.debug("ABR", "new buffer-based estimate", {
          bitrate: chosenRepFromBufferSize.bitrate,
          representation: chosenRepFromBufferSize.id,
        });
        prevEstimate.update(
          chosenRepFromBufferSize,
          bandwidthEstimate,
          ABRAlgorithmType.BufferBased,
        );
        return {
          bitrate: bandwidthEstimate,
          representation: chosenRepFromBufferSize,
          urgent: networkAnalyzer.isUrgent(
            chosenRepFromBufferSize.bitrate,
            currentRepresentationVal,
            requests,
            lastPlaybackObservation,
          ),
          knownStableBitrate,
        };
      } else {
        log.debug("ABR", "new bandwidth estimate", {
          bitrate: chosenRepFromBandwidth.bitrate,
          representation: chosenRepFromBandwidth.id,
        });
        prevEstimate.update(
          chosenRepFromBandwidth,
          bandwidthEstimate,
          ABRAlgorithmType.BandwidthBased,
        );
        return {
          bitrate: bandwidthEstimate,
          representation: chosenRepFromBandwidth,
          urgent: networkAnalyzer.isUrgent(
            chosenRepFromBandwidth.bitrate,
            currentRepresentationVal,
            requests,
            lastPlaybackObservation,
          ),
          knownStableBitrate,
        };
      }
    }
  }

  /**
   * Stop previous estimate production (if one) and restart it considering new
   * conditions (such as a new list of Representations).
   */
  function restartEstimatesProductionFromCurrentConditions(): void {
    const representations = representationsRef.getValue();
    currentEstimatesCanceller.cancel("restart");
    currentEstimatesCanceller = new TaskCanceller("ABR " + context.adaptation.type);
    currentEstimatesCanceller.linkToSignal(stopAllEstimates);
    const newRef = createEstimateReference(
      representations,
      currentEstimatesCanceller.signal,
    );

    newRef.onUpdate(
      function onNewEstimate(newEstimate: IABREstimate): void {
        estimateRef.setValue(newEstimate);
      },
      { clearSignal: currentEstimatesCanceller.signal, emitCurrentValue: true },
    );
  }

  /**
   * Callback to call when new metrics are available
   * @param {Object} value
   */
  function onMetric(value: IMetricsCallbackPayload): void {
    const { requestDuration, segmentDuration, size, content } = value;

    // calculate bandwidth
    bandwidthEstimator.addSample(requestDuration, size);

    if (!content.segment.isInit) {
      // calculate "maintainability score"
      const { segment, representation } = content;
      if (segmentDuration === undefined && !segment.complete) {
        // We cannot know the real duration of the segment
        return;
      }
      const segDur = segmentDuration ?? segment.duration;
      scoreCalculator.addSample(representation, requestDuration / 1000, segDur);
    }
  }

  /** Callback called when a new request begins. */
  function onRequestBegin(val: IRequestBeginCallbackPayload): void {
    requestsStore.add(val);
  }

  /** Callback called when progress information is known on a pending request. */
  function onRequestProgress(val: IRequestProgressCallbackPayload): void {
    requestsStore.addProgress(val);
  }

  /** Callback called when a pending request ends. */
  function onRequestEnd(val: IRequestEndCallbackPayload): void {
    requestsStore.remove(val.id);
  }
}

/**
 * Filter representations given through filters options.
 * @param {Array.<Representation>} representations
 * @param {Object | undefined} resolutionLimit
 * @param {number | undefined} bitrateThrottle
 * @returns {Array.<Representation>}
 */
function getFilteredRepresentations(
  representations: IRepresentation[],
  resolutionLimit: IResolutionInfo | undefined,
  bitrateThrottle: number | undefined,
): IRepresentation[] {
  let filteredReps = representations;

  if (bitrateThrottle !== undefined && bitrateThrottle < Infinity) {
    filteredReps = filterByBitrate(filteredReps, bitrateThrottle);
  }

  if (resolutionLimit !== undefined) {
    filteredReps = filterByResolution(filteredReps, resolutionLimit);
  }

  return filteredReps;
}

/**
 * Adaptive BitRate estimate object.
 *
 * Helps the player to choose a Representation (i.e. a quality) by frequently
 * measuring the current network and playback conditions.
 *
 * At regular intervals, an `IABREstimate` will be sent to that end.
 */
export interface IABREstimate {
  /**
   * If defined, the last calculated available bitrate for that type of
   * content (e.g. "video, "audio"...).
   * Note that it can be very different from the "real" user's bandwidth.
   *
   * If `undefined`, we do not currently know the bitrate for the current type.
   */
  bitrate: undefined | number;
  /**
   * The Representation considered as the most adapted to the current network
   * and playback conditions.
   * `null` in the rare occurence where there is no `Representation` to choose
   * from.
   */
  representation: IRepresentation | null;
  /**
   * If `true`, the current `representation` suggested should be switched to as
   * soon as possible. For example, you might want to interrupt all pending
   * downloads for the current representation and replace it immediately by this
   * one.
   *
   * If `false`, the suggested `representation` can be switched to less
   * urgently. For example, pending segment requests for the current
   * Representation can be finished before switching to that new Representation.
   */
  urgent: boolean;
  /**
   * Last bitrate which was known to be "maintainable".
   *
   * The estimates provided though `IABREstimate` objects sometimes indicate
   * that you should switch to a Representation with a much higher bitrate than
   * what the network bandwidth can sustain.
   * Such Representation are said to not be "maintainable": at a regular
   * playback speed, we cannot maintain this Representation without buffering
   * after some time.
   *
   * `knownStableBitrate` communicates the bitrate of the last Representation
   * that was known to be maintaninable: it could reliably be played continually
   * without interruption.
   *
   * This can be for example useful in some optimizations such as
   * "fast-switching", where the player will load segments of higher quality to
   * replace segments of low quality.
   * Because in that case you're pushing segment on top of buffer you already
   * have, you will most likely only want to do it when the Representation is
   * known to be maintaninable.
   */
  knownStableBitrate?: number | undefined;
}

/** Media properties `getEstimateReference` will need to keep track of. */
export interface IRepresentationEstimatorPlaybackObservation {
  /**
   * For the concerned media buffer, difference in seconds between the next
   * position where no segment data is available and the current position.
   */
  bufferGap: number;
  /**
   * Information on the current media position in seconds at the time of a
   * Playback Observation.
   */
  position: ObservationPosition;
  /**
   * Last "playback rate" set by the user. This is the ideal "playback rate" at
   * which the media should play.
   */
  speed: number;
  /** `duration` property of the HTMLMediaElement on which the content plays. */
  duration: number;
  /** Theoretical maximum position on the content that can currently be played. */
  maximumPosition: number;
}

/** Content of the `IABRMetricsEvent` event's payload. */
export interface IMetricsCallbackPayload {
  /** Time the request took to perform the request, in milliseconds. */
  requestDuration: number;
  /** Amount of bytes downloaded with this request. */
  size: number;
  /** Duration of the loaded segment, in seconds. */
  segmentDuration: number | undefined;
  /** Context about the segment downloaded. */
  content: {
    representation: IRepresentation;
    adaptation: IAdaptation;
    segment: ISegment;
  };
}

export type IRequestBeginCallbackPayload = IPendingRequestStoreBegin;
export type IRequestProgressCallbackPayload = IPendingRequestStoreProgress;
export interface IRequestEndCallbackPayload {
  /**
   * Same `id` value used to identify that request at the time the corresponding
   * callback was called.
   */
  id: string;
}

/** Object allowing to filter some Representations out based on different attributes. */
export interface IABRFiltersObject {
  /**
   * Filters out all Representations with a bitrate higher than this value.
   * If all Representations have a bitrate higher than this value, still
   * consider the Representation with the lowest bitrate.
   */
  bitrate?: number;
  /**
   * Consider only Representations with a width either unknown or lower or equal
   * to that value.
   *
   * As a special case, if no Representation has a width exactly equal to that
   * value, the Representation(s) with the `width` immediately higher will also
   * be considered.
   *
   * _This is usually used to filter out Representations for which the width
   * is much higher than the maximum width of the screen. In such cases, there
   * would be no difference between those higher-quality Representations._
   */
  width?: number;
}

/** Callbacks returned by `getEstimateReference`. */
export interface IRepresentationEstimatorCallbacks {
  /** Callback to call when a segment has been completely pushed to the buffer. */
  addedSegment(val: IAddedSegmentCallbackPayload): void;
  /** Callback to call when network metrics are available. */
  metrics(val: IMetricsCallbackPayload): void;
  /** Callback to call when an HTTP(S) request begins. */
  requestBegin(val: IRequestBeginCallbackPayload): void;
  /**
   * Callback to call when an HTTP(S) request ends.
   * Important: `requestEnd` should only be called for requests for which the
   * `requestBegin` callback has been called. It should be called at most once.
   */
  requestEnd(val: IRequestEndCallbackPayload): void;
  /**
   * Callback to call when progress information is available on a pending
   * request.
   * Important: `requestProgress` should only be called for requests for which
   * the `requestBegin` callback has been called but the `requestEnd` method has
   * not yet been called.
   */
  requestProgress(val: IRequestProgressCallbackPayload): void;
}

export interface IAddedSegmentCallbackPayload {
  /**
   * The buffered ranges of the related media buffer after that segment has
   * been pushed.
   */
  buffered: IRange[];
  /** The context for the segment that has been pushed. */
  content: { representation: IRepresentation };
}

/** Arguments to give to `getEstimateReference`. */
export interface IRepresentationEstimatorArguments {
  /** Class allowing to estimate the current network bandwidth. */
  bandwidthEstimator: BandwidthEstimator;
  /** Emit regular playback information. */
  playbackObserver: IReadOnlyPlaybackObserver<IRepresentationEstimatorPlaybackObservation>;
  /**
   * The Representation currently loaded.
   * `null` if no Representation is currently loaded.
   */
  currentRepresentation: IReadOnlySharedReference<IRepresentation | null>;
  /** Throttle Representation pool according to filters. */
  filters: {
    limitResolution: IReadOnlySharedReference<IResolutionInfo | undefined>;
    throttleBitrate: IReadOnlySharedReference<number>;
  };
  /**
   * The initial bitrate you want to start with.
   *
   * The highest-quality Representation with a bitrate lower-or-equal to that
   * value will be chosen.
   * If no Representation has bitrate lower or equal to that value, the
   * Representation with the lowest bitrate will be chosen instead.
   */
  initialBitrate?: number;
  /**
   * If `true` the content is a "low-latency" content.
   * Such contents have specific settings.
   */
  lowLatencyMode: boolean;
  /** The list of Representations `getEstimateReference` can choose from. */
  representations: IReadOnlySharedReference<IRepresentation[]>;
  /** Context for the list of Representations to choose. */
  context: {
    /** In which Manifest the Representations are. */
    manifest: IManifest;
    /** In which Period the Representations are. */
    period: IPeriod;
    /** In which Adaptation the Representations are. */
    adaptation: IAdaptation;
  };
}

/**
 * Type of the function returned by `createAdaptiveRepresentationSelector`,
 * allowing to estimate the most adapted `Representation`.
 */
export type IRepresentationEstimator = (
  /** Information on the content for which a Representation will be chosen */
  context: { manifest: IManifest; period: IPeriod; adaptation: IAdaptation },
  /** Reference emitting the Representation currently loaded. */
  currentRepresentation: IReadOnlySharedReference<IRepresentation | null>,
  /** Reference emitting the list of available Representations to choose from. */
  representations: IReadOnlySharedReference<IRepresentation[]>,
  /** Regularly emits playback conditions */
  playbackObserver: IReadOnlyPlaybackObserver<IRepresentationEstimatorPlaybackObservation>,
  /**
   * After this `CancellationSignal` emits, resources will be disposed and
   * estimates will stop to be emitted.
   */
  stopAllEstimates: CancellationSignal,
) => IRepresentationEstimatorResponse;

/** Value returned by an `IRepresentationEstimator` */
export interface IRepresentationEstimatorResponse {
  /**
   * Regularly produces estimates of the best Representation to play (from the
   * list given).
   */
  estimates: IReadOnlySharedReference<IABREstimate>;
  /**
   * Callback which need to be called as soon as the corresponding events to
   * obtain accurate Representation estimates.
   */
  callbacks: IRepresentationEstimatorCallbacks;
}

/** Arguments received by `createAdaptiveRepresentationSelector`. */
export interface IAdaptiveRepresentationSelectorArguments {
  /** Initial bitrate chosen, per type (minimum if not set) */
  initialBitrates: Partial<Record<IBufferType, number>>;

  /**
   * Some settings can depend on wether you're playing a low-latency content.
   * Set it to `true` if you're playing such content.
   */
  lowLatencyMode: boolean;

  /** Allows to filter which Representations can be choosen. */
  throttlers: IRepresentationEstimatorThrottlers;
}

/**
 * Throttlers are interfaces allowing to limit the pool of Representations
 * to choose from.
 */
export interface IRepresentationEstimatorThrottlers {
  /** Limit Representations based on maximum authorized resolution. */
  limitResolution: Partial<
    Record<IBufferType, IReadOnlySharedReference<IResolutionInfo>>
  >;
  /** Limit Representations based on maximum authorized bitrate. */
  throttleBitrate: Partial<Record<IBufferType, IReadOnlySharedReference<number>>>;
}

export type { IResolutionInfo };
