/**
 * 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.
 */

// TODO: move that file somewhere else
import getBufferedDataPerMediaBuffer from "../../../core/entry/get_buffered_data_per_media_buffer";
import type { IPausedPlaybackObservation } from "../../../core/types";
import type { IManifestMetadata } from "../../../manifest";
import { getMaximumSafePosition } from "../../../manifest";
import type { IMediaSourceInterface } from "../../../mse";
import type {
  IPlaybackObservation,
  IReadOnlyPlaybackObserver,
  IMediaElementPlaybackObserver,
  ObservationPosition,
  IRebufferingStatus,
  IFreezingStatus,
} from "../../../playback_observer";
import type { ITrackType } from "../../../public_types";
import type { IRange } 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 { ITextDisplayer } from "../../text_displayer";

/** Arguments needed to create the core's version of the PlaybackObserver. */
export interface ICorePlaybackObserverArguments {
  /** If true, the player will auto-play when `initialPlayPerformed` becomes `true`. */
  autoPlay: boolean;
  /** Manifest of the content being played */
  manifest: IManifestMetadata;
  /** Becomes `true` after the initial play has been taken care of. */
  initialPlayPerformed: IReadOnlySharedReference<boolean>;
  /** The last speed requested by the user. */
  speed: IReadOnlySharedReference<number>;
  /**
   * Used abstraction to implement text track displaying.
   *
   * `null` if text tracks are disabled
   */
  textDisplayer: ITextDisplayer | null;
  /** Used abstraction for MSE API. */
  mediaSource: IMediaSourceInterface | null;
}

export interface ICorePlaybackObservation {
  /**
   * Information on whether the media element was paused at the time of the
   * Observation.
   */
  paused: IPausedPlaybackObservation;
  /**
   * Information on the current media position in seconds at the time of the
   * Observation.
   */
  position: ObservationPosition;
  /** `duration` property of the HTMLMediaElement. */
  duration: number;
  /** `readyState` property of the HTMLMediaElement. */
  readyState: number;
  /** Target playback rate at which we want to play the content. */
  speed: number;
  /** Theoretical maximum position on the content that can currently be played. */
  maximumPosition: number;
  /**
   * Ranges of buffered data per type of media.
   * `null` if no buffer exists for that type of media.
   */
  buffered: Record<ITrackType, IRange[] | null>;
  rebuffering: IRebufferingStatus | null;
  freezing: IFreezingStatus | null;
  bufferGap: number | undefined;
  /**
   * Indicates whether the user agent believes it has enough buffered data to ensure
   * uninterrupted playback for a meaningful period or needs more data.
   * It also reflects whether the user agent can retrieve and buffer data in an
   * energy-efficient manner while maintaining the desired memory usage.
   * `true` indicates that the buffer is low, and more data should be buffered.
   * `false` indicates that there is enough buffered data, and no additional data needs
   *  to be buffered at this time.
   */
  canStream: boolean;
  /** If `true` the content is loaded until its maximum position. */
  fullyLoaded: boolean;
}

/**
 * Create PlaybackObserver for the core part of the code.
 * @param {Object} srcPlaybackObserver - Base `PlaybackObserver` from which we
 * will derive information.
 * @param {Object} context - Various information linked to the current content
 * being played.
 * @param {Object} fnCancelSignal - Abort the created PlaybackObserver.
 * @returns {Object}
 */
export default function createCorePlaybackObserver(
  srcPlaybackObserver: IMediaElementPlaybackObserver,
  {
    autoPlay,
    initialPlayPerformed,
    manifest,
    mediaSource,
    speed,
    textDisplayer,
  }: ICorePlaybackObserverArguments,
  fnCancelSignal: CancellationSignal,
): IReadOnlyPlaybackObserver<ICorePlaybackObservation> {
  return srcPlaybackObserver.deriveReadOnlyObserver(function transform(
    observationRef: IReadOnlySharedReference<IPlaybackObservation>,
    parentObserverCancelSignal: CancellationSignal,
  ): IReadOnlySharedReference<ICorePlaybackObservation> {
    const canceller = new TaskCanceller("Core PlaybackObserver");
    canceller.linkToSignal(parentObserverCancelSignal);
    canceller.linkToSignal(fnCancelSignal);
    const newRef = new SharedReference(
      constructCorePlaybackObservation(),
      canceller.signal,
    );

    // TODO there might be subtle unexpected behavior here as updating the
    // speed will send observation which may be outdated at the time it is sent
    speed.onUpdate(emitCorePlaybackObservation, {
      clearSignal: canceller.signal,
      emitCurrentValue: false,
    });

    observationRef.onUpdate(emitCorePlaybackObservation, {
      clearSignal: canceller.signal,
      emitCurrentValue: false,
    });

    // TODO there might be subtle unexpected behavior here as listening to mediaSource
    // event will send observation which may be outdated at the time it is sent
    mediaSource?.addEventListener(
      "streamingChanged",
      () => {
        emitCorePlaybackObservation();
      },
      canceller.signal,
    );
    return newRef;

    function constructCorePlaybackObservation() {
      const observation = observationRef.getValue();
      const lastSpeed = speed.getValue();
      updateWantedPositionIfAfterManifest(observation, manifest);
      return {
        // TODO more exact according to the current Adaptation chosen?
        maximumPosition: getMaximumSafePosition(manifest),
        bufferGap: observation.bufferGap,
        position: observation.position,
        buffered: getBufferedDataPerMediaBuffer(mediaSource, textDisplayer),
        duration: observation.duration,
        rebuffering: observation.rebuffering,
        freezing: observation.freezing,
        paused: {
          last: observation.paused,
          pending: getPendingPaused(initialPlayPerformed, autoPlay),
        },
        readyState: observation.readyState,
        speed: lastSpeed,
        canStream: mediaSource?.streaming ?? true,
        fullyLoaded: observation.fullyLoaded,
      };
    }

    function emitCorePlaybackObservation() {
      newRef.setValue(constructCorePlaybackObservation());
    }
  });
}

export function updateWantedPositionIfAfterManifest(
  observation: IPlaybackObservation,
  manifest: IManifestMetadata,
): void {
  if (!manifest.isDynamic || manifest.isLastPeriodKnown) {
    // HACK: When the position is actually further than the maximum
    // position for a finished content, we actually want to be loading
    // the last segment before ending.
    // For now, this behavior is implicitely forced by making as if we
    // want to seek one second before the period's end (despite never
    // doing it).
    const lastPeriod = manifest.periods[manifest.periods.length - 1];
    if (lastPeriod !== undefined && lastPeriod.end !== undefined) {
      const wantedPosition = observation.position.getWanted();
      if (wantedPosition >= lastPeriod.start && wantedPosition >= lastPeriod.end - 1) {
        // We're after the end of the last Period, check if `buffered`
        // indicates that the last segment is probably not loaded, in which
        // case act as if we want to load one second before the end.
        const buffered = observation.buffered;
        if (
          buffered.length === 0 ||
          buffered.end(buffered.length - 1) < observation.duration - 1
        ) {
          observation.position.forceWantedPosition(lastPeriod.end - 1);
        }
      }
    }
  }
}

export function getPendingPaused(
  initialPlayPerformed: IReadOnlySharedReference<boolean>,
  autoPlay: boolean,
): boolean | undefined {
  return initialPlayPerformed.getValue() ? undefined : !autoPlay;
}
