/**
 * 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 arrayFindIndex from "../../utils/array_find_index";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import getMonotonicTimeStamp from "../../utils/monotonic_timestamp";
import getBufferLevels from "./utils/get_buffer_levels";
import type { IRepresentationMaintainabilityScore } from "./utils/representation_score_calculator";
import { ScoreConfidenceLevel } from "./utils/representation_score_calculator";

/**
 * Minimum amount of time, in milliseconds, during which we are blocked from
 * raising in quality after it had been considered as too high.
 */
const MINIMUM_BLOCK_RAISE_DELAY = 6000;

/**
 * Maximum amount of time, in milliseconds, during which we are blocked from
 * raising in quality after it had been considered as too high.
 */
const MAXIMUM_BLOCK_RAISE_DELAY = 15000;

/**
 * Amount of time, in milliseconds, with which the blocking time in raising
 * the quality will be incremented if the current quality estimate is seen
 * as too unstable.
 */
const RAISE_BLOCKING_DELAY_INCREMENT = 3000;

/**
 * Amount of time, in milliseconds, with which the blocking time in raising
 * the quality will be dcremented if the current quality estimate is seen
 * as relatively stable, until `MINIMUM_BLOCK_RAISE_DELAY` is reached.
 */
const RAISE_BLOCKING_DELAY_DECREMENT = 1000;

/**
 * Amount of time, in milliseconds, after the "raise blocking delay" currently
 * in place (during which it is forbidden to raise up in quality), during which
 * we might want to raise the "raise blocking delay" if the last chosen quality
 * seems unsuitable.
 *
 * For example, let's consider that the current raise blocking delay is at
 * `4000`, or 4 seconds, and that this `STABILITY_CHECK_DELAY` is at `5000`, or
 * 5 seconds.
 * Here it means that if the estimated quality is found to be unsuitable less
 * than 4+5 = 9 seconds after it last was, we will increment the raise blocking
 * delay by `RAISE_BLOCKING_DELAY_INCREMENT` (unless `MAXIMUM_BLOCK_RAISE_DELAY`
 * is reached).
 * Else, if takes more than 9 seconds, the raise blocking delay might be
 * decremented.
 */
const STABILITY_CHECK_DELAY = 9000;

/**
 * Choose a bitrate based on the currently available buffer.
 *
 * This algorithm is based on a deviation of the BOLA algorithm.
 * It is a hybrid solution that also relies on a given bitrate's
 * "maintainability".
 * Each time a chunk is downloaded, from the ratio between the chunk duration
 * and chunk's request time, we can assume that the representation is
 * "maintanable" or not.
 * If so, we may switch to a better quality, or conversely to a worse quality.
 *
 * It also rely on mechanisms to avoid fluctuating too much between qualities.
 *
 * @class BufferBasedChooser
 */
export default class BufferBasedChooser {
  private _levelsMap: number[];
  private _bitrates: number[];

  /**
   * Current last best Representation's bitrate estimate made by the
   * `BufferBasedChooser` or `undefined` if it has no such guess for now.
   *
   * Might be updated each time `onAddedSegment` is called.
   */
  private _currentEstimate: number | undefined;

  /**
   * Last monotonically-raising timestamp (as defined by the RxPlayer), at which
   * the current quality was seen as too high by this algorithm.
   * Begins at `undefined`.
   */
  private _lastUnsuitableQualityTimestamp: number | undefined;

  /**
   * After lowering in quality, we forbid raising during a set amount of time.
   * This amount is adaptive may continue to raise if it seems that quality
   * is switching too much between low and high qualities.
   *
   * `_blockRaiseDelay` represents this time in milliseconds.
   */
  private _blockRaiseDelay: number;

  /**
   * @param {Array.<number>} bitrates
   */
  constructor(bitrates: number[]) {
    this._levelsMap = getBufferLevels(bitrates).map((bl) => {
      return bl + 4; // Add some buffer security as it will be used conjointly with
      // other algorithms anyway
    });
    this._bitrates = bitrates;
    this._lastUnsuitableQualityTimestamp = undefined;
    this._blockRaiseDelay = MINIMUM_BLOCK_RAISE_DELAY;
    log.debug(
      "ABR",
      "Steps for buffer based chooser.",
      this._levelsMap
        .map((l, i) => `bufferLevel: ${l}, bitrate: ${bitrates[i]}`)
        .join(" ,"),
    );
  }

  /**
   * @param {Object} playbackObservation
   * @returns {number|undefined}
   */
  public onAddedSegment(
    playbackObservation: IBufferBasedChooserPlaybackObservation,
  ): void {
    const bufferLevels = this._levelsMap;
    const bitrates = this._bitrates;
    const { bufferGap, currentBitrate, currentScore, speed } = playbackObservation;
    if (isNullOrUndefined(currentBitrate)) {
      this._currentEstimate = bitrates[0];
      return;
    }

    let currentBitrateIndex = -1;
    for (let i = 0; i < bitrates.length; i++) {
      // There could be bitrate duplicates. Only take the last one to simplify
      const bitrate = bitrates[i];
      if (bitrate === currentBitrate) {
        currentBitrateIndex = i;
      } else if (bitrate > currentBitrate) {
        break;
      }
    }

    if (currentBitrateIndex < 0 || bitrates.length !== bufferLevels.length) {
      log.info("ABR", "Current Bitrate not found in the calculated levels");
      this._currentEstimate = bitrates[0];
      return;
    }

    let scaledScore: number | undefined;
    if (currentScore !== undefined) {
      scaledScore = speed === 0 ? currentScore.score : currentScore.score / speed;
    }

    const actualBufferGap = isFinite(bufferGap) ? bufferGap : 0;

    const now = getMonotonicTimeStamp();

    if (
      actualBufferGap < bufferLevels[currentBitrateIndex] ||
      (scaledScore !== undefined &&
        scaledScore < 1 &&
        currentScore?.confidenceLevel === ScoreConfidenceLevel.HIGH)
    ) {
      const timeSincePrev =
        this._lastUnsuitableQualityTimestamp === undefined
          ? -1
          : now - this._lastUnsuitableQualityTimestamp;
      if (timeSincePrev < this._blockRaiseDelay + STABILITY_CHECK_DELAY) {
        const newDelay = this._blockRaiseDelay + RAISE_BLOCKING_DELAY_INCREMENT;
        log.debug(
          "ABR",
          "Incrementing blocking raise in BufferBasedChooser due " +
            "to unstable quality",
          { prevDelay: this._blockRaiseDelay, newDelay },
        );
        this._blockRaiseDelay = Math.min(newDelay, MAXIMUM_BLOCK_RAISE_DELAY);
      } else {
        const newDelay = this._blockRaiseDelay - RAISE_BLOCKING_DELAY_DECREMENT;
        log.debug("ABR", "Lowering quality in BufferBasedChooser", {
          prevDelay: this._blockRaiseDelay,
          newDelay,
        });
        this._blockRaiseDelay = Math.max(MINIMUM_BLOCK_RAISE_DELAY, newDelay);
      }
      this._lastUnsuitableQualityTimestamp = now;
      // Security if multiple bitrates are equal, we now take the first one
      const baseIndex = arrayFindIndex(bitrates, (b) => b === currentBitrate);
      for (let i = baseIndex - 1; i >= 0; i--) {
        if (actualBufferGap >= bufferLevels[i]) {
          this._currentEstimate = bitrates[i];
          return;
        }
      }
      this._currentEstimate = bitrates[0];
      return;
    }

    if (
      (this._lastUnsuitableQualityTimestamp !== undefined &&
        now - this._lastUnsuitableQualityTimestamp < this._blockRaiseDelay) ||
      scaledScore === undefined ||
      scaledScore < 1.15 ||
      currentScore?.confidenceLevel !== ScoreConfidenceLevel.HIGH
    ) {
      this._currentEstimate = currentBitrate;
      return;
    }

    const currentBufferLevel = bufferLevels[currentBitrateIndex];
    const nextIndex = (() => {
      for (let i = currentBitrateIndex + 1; i < bufferLevels.length; i++) {
        if (bufferLevels[i] > currentBufferLevel) {
          return i;
        }
      }
    })();
    if (nextIndex !== undefined) {
      const nextBufferLevel = bufferLevels[nextIndex];
      if (bufferGap >= nextBufferLevel) {
        log.debug("ABR", "Raising quality in BufferBasedChooser", {
          bitrate: bitrates[nextIndex],
        });
        this._currentEstimate = bitrates[nextIndex];
        return;
      }
    }
    this._currentEstimate = currentBitrate;
    return;
  }

  /**
   * Returns the last best Representation's bitrate estimate made by the
   * `BufferBasedChooser` or `undefined` if it has no such guess for now.
   *
   * Might be updated after `onAddedSegment` is called.
   *
   * @returns {number|undefined}
   */
  public getLastEstimate(): number | undefined {
    return this._currentEstimate;
  }
}

/** Playback observation needed by the `BufferBasedChooser`. */
export interface IBufferBasedChooserPlaybackObservation {
  /**
   * Difference in seconds between the current position and the next
   * non-buffered position in the buffer for the currently-considered
   * media type.
   */
  bufferGap: number;
  /** The bitrate of the currently downloaded segments, in bps. */
  currentBitrate?: number | undefined;
  /** The "maintainability score" of the currently downloaded segments. */
  currentScore?: IRepresentationMaintainabilityScore | undefined;
  /** Playback rate wanted (e.g. `1` is regular playback, `2` is double speed etc.). */
  speed: number;
}
