import { FakeEventTarget } from '../../event/fake-event-target';
import { FakeEvent } from '../../event/fake-event';
import { EventManager } from '../../event/event-manager';
import { CustomEventType, Html5EventType } from '../../event/event-type';
import MediaSourceProvider from './media-source/media-source-provider';
import VideoTrack from '../../track/video-track';
import AudioTrack from '../../track/audio-track';
import { PKTextTrack, getActiveCues } from '../../track/text-track';
import ImageTrack from '../../track/image-track';
import { createTimedMetadata } from '../../track/timed-metadata';
import * as Utils from '../../utils/util';
import Html5AutoPlayCapability from './capabilities/html5-autoplay';
import Error from '../../error/error';
import getLogger from '../../utils/logger';
import { DroppedFramesWatcher } from '../dropped-frames-watcher';
import { ThumbnailInfo } from '../../thumbnail/thumbnail-info';
import { IMediaSourceAdapter } from '../../types';
import { CapabilityResult, ICapability } from '../../types';
import { PKABRRestrictionObject, PKDrmConfigObject, PKDrmDataObject, PKMediaSourceObject, PKVideoElementStore } from '../../types';
import { IEngine } from '../../types';
import Track from '../../track/track';
import Env from '../../../src/utils/env';

const SHORT_BUFFERING_TIMEOUT: number = 200;

/**
 * Html5 engine for playback.
 * @classdesc
 */
export default class Html5 extends FakeEventTarget implements IEngine {
  /**
   * The video element.
   * @type {HTMLVideoElement}
   * @private
   */
  private _el!: HTMLVideoElement;
  /**
   * The event manager of the engine.
   * @type {EventManager}
   * @private
   */
  private _eventManager: EventManager;
  /**
   * The selected media source adapter of the engine.
   * @type {?IMediaSourceAdapter}
   * @private
   */
  private _mediaSourceAdapter!: IMediaSourceAdapter | null;
  /**
   * The player config object.
   * @type {Object}
   * @private
   */
  private _config: any;
  /**
   * Promise to indicate when a media source adapter can be loaded.
   * @type {Promise<*>}
   * @private
   */
  private _canLoadMediaSourceAdapterPromise: Promise<void>;
  private _droppedFramesWatcher: DroppedFramesWatcher | undefined;
  private _reset: boolean = false;

  private _cachedUrls: string[] = [];

  /**
   * The html5 class logger.
   * @type {any}
   * @static
   * @private
   */
  private static _logger: any = getLogger('Html5');

  /**
   * The html5 capabilities handlers.
   * @private
   * @static
   */
  private static _capabilities: Array<ICapability> = [Html5AutoPlayCapability];

  /**
   * @type {string} - The engine id.
   * @public
   * @static
   */
  public static id: string = 'html5';

  /**
   * @type {PKVideoElementStore} - Store object which maps between playerId to its video element.
   */
  public static videoElementStore: PKVideoElementStore = {};

  /**
   * Checks if html5 is supported.
   * @returns {boolean} - Whether the html5 is supported.
   */
  public static isSupported(): boolean {
    try {
      const el = Utils.Dom.createElement('video');
      el.volume = 0.5;
      return !!el.canPlayType;
    } catch (e) {
      return false;
    }
  }

  /**
   * Factory method to create an engine.
   * @param {PKMediaSourceObject} source - The selected source object.
   * @param {Object} config - The player configuration.
   * @param {string} playerId - The player id.
   * @returns {IEngine} - New instance of the run time engine.
   * @public
   * @static
   */
  public static createEngine(source: PKMediaSourceObject, config: any, playerId: string): IEngine {
    return new this(source, config, playerId);
  }

  /**
   * Checks if the engine can play a given source.
   * @param {PKMediaSourceObject} source - The source object to check.
   * @param {boolean} preferNative - prefer native flag.
   * @param {PKDrmConfigObject} drmConfig - The drm config.
   * @returns {boolean} - Whether the engine can play the source.
   * @public
   * @static
   */
  public static canPlaySource(source: PKMediaSourceObject, preferNative: boolean, drmConfig: PKDrmConfigObject): boolean {
    return MediaSourceProvider.canPlaySource(source, preferNative, drmConfig);
  }

  /**
   * Runs the html5 capabilities tests.
   * @returns {void}
   * @public
   * @static
   */
  public static runCapabilities(): void {
    Html5._capabilities.forEach((capability) => capability.runCapability());
  }

  /**
   * Gets the html5 capabilities.
   * @return {Promise<Object>} - The html5 capabilities object.
   * @public
   * @static
   */
  public static getCapabilities(): Promise<any> {
    const promises: CapabilityResult[] = [];
    Html5._capabilities.forEach((capability) => promises.push(capability.getCapability()));
    return Promise.all(promises).then((arrayOfResults) => {
      const mergedResults: CapabilityResult = {};
      arrayOfResults.forEach((res) => Object.assign(mergedResults, res));
      return { [Html5.id]: mergedResults };
    });
  }

  /**
   * Sets an engine capabilities.
   * @param {Object} capabilities - The engine capabilities.
   * @returns {void}
   * @public
   * @static
   */
  public static setCapabilities(capabilities: { [name: string]: any }): void {
    Html5._capabilities.forEach((capability) => capability.setCapabilities(capabilities));
  }

  /**
   * For browsers which block auto play, use the user gesture to open the video element and enable playing via API.
   * @returns {void}
   * @param {string} playerId - the id to be set as the key of the video element
   * @private
   * @public
   */
  public static prepareVideoElement(playerId: string): void {
    if (!Html5.videoElementStore[playerId]) {
      Html5._logger.debug(`Create the video element for playing ${playerId}`);
      const videoElement = Utils.Dom.createElement('video');
      Html5.videoElementStore[playerId] = videoElement;
    }
    Html5._logger.debug(`Prepare the video element for playing ${playerId}`);
    Html5.videoElementStore[playerId].load();
  }

  /**
   * The player playback rates.
   * @type {Array<number>}
   */
  public static PLAYBACK_RATES: Array<number> = [0.5, 1, 1.5, 2];

  /**
   * @constructor
   * @param {PKMediaSourceObject} source - The selected source object.
   * @param {Object} config - The player configuration.
   * @param {string} playerId - The player id.
   */
  constructor(source: PKMediaSourceObject, config: any, playerId: string) {
    super();
    this._eventManager = new EventManager();
    this._canLoadMediaSourceAdapterPromise = Promise.resolve();
    this._createVideoElement(playerId);
    this._init(source, config);
  }

  /**
   * Restores the engine.
   * @param {PKMediaSourceObject} source - The selected source object.
   * @param {Object} config - The player configuration.
   * @returns {void}
   */
  public restore(source: PKMediaSourceObject, config: any): void {
    this.reset();
    this._init(source, config);
  }

  /**
   * Resets the engine.
   * @returns {void}
   */
  public reset(): void {
    if (this._reset) return;
    this._reset = true;
    this._eventManager.removeAll();
    if (this._droppedFramesWatcher) {
      this._droppedFramesWatcher.destroy();
      this._droppedFramesWatcher = undefined;
    }
    this._canLoadMediaSourceAdapterPromise = new Promise((resolve, reject) => {
      const mediaSourceAdapterDestroyed = this._mediaSourceAdapter ? this._mediaSourceAdapter.destroy() : Promise.resolve();
      if (this._el && this._el.src) {
        mediaSourceAdapterDestroyed.then(() => {
          Utils.Dom.setAttribute(this._el, 'src', '');
          Utils.Dom.removeAttribute(this._el, 'src');
          resolve();
        }, reject);
      } else {
        mediaSourceAdapterDestroyed.then(resolve, reject);
      }
    });
    this._mediaSourceAdapter = null;
  }

  /**
   * Destroys the engine.
   * @public
   * @returns {void}
   */
  public destroy(): void {
    this.detach();
    if (this._el) {
      this.pause();
      Utils.Dom.removeAttribute(this._el, 'src');
      Utils.Dom.removeChild(this._el.parentNode, this._el);
    }
    this._eventManager.destroy();
    MediaSourceProvider.destroy();
    if (this._droppedFramesWatcher) {
      this._droppedFramesWatcher.destroy();
      this._droppedFramesWatcher = undefined;
    }
    if (this._mediaSourceAdapter) {
      this._mediaSourceAdapter.setCachedUrls([]);
      this._mediaSourceAdapter.destroy();
      this._mediaSourceAdapter = null;
    }
  }

  /**
   * Get the engine mediaSourceAdapter
   * @public
   * @returns {IMediaSourceAdapter | null}
   */
  public get mediaSourceAdapter(): IMediaSourceAdapter | null {
    return this._mediaSourceAdapter;
  }

  /**
   * Get the engine's id
   * @public
   * @returns {string} the engine's id
   */
  public get id(): string {
    return Html5.id;
  }

  /**
   * attach media - return the media source to handle the video tag
   * @public
   * @returns {void}
   */
  public attachMediaSource(): void {
    if (this._mediaSourceAdapter) {
      this._mediaSourceAdapter.attachMediaSource();
    }
  }

  /**
   * detach media - will remove the media source from handling the video
   * @public
   * @returns {void}
   */
  public detachMediaSource(): void {
    if (this._mediaSourceAdapter) {
      this._mediaSourceAdapter.detachMediaSource();
    }
  }

  /**
   * Listen to the video element events and triggers them from the engine.
   * @public
   * @returns {void}
   */
  public attach(): void {
    Object.keys(Html5EventType).forEach((html5Event) => {
      if (![Html5EventType.ERROR, Html5EventType.WAITING].includes(Html5EventType[html5Event])) {
        this._eventManager.listen(this._el, Html5EventType[html5Event], () => {
          return this.dispatchEvent(new FakeEvent(Html5EventType[html5Event]));
        });
      }
    });
    this._eventManager.listen(this._el, Html5EventType.ERROR, () => this._handleVideoError());
    this._eventManager.listen(this._el, Html5EventType.WAITING, () => this._handleWaiting());
    this._handleMetadataTrackEvents();
    this._eventManager.listen(this._el.textTracks, 'addtrack', (event: any) => {
      if (PKTextTrack.isNativeTextTrack(event.track)) {
        this.dispatchEvent(new FakeEvent(CustomEventType.TEXT_TRACK_ADDED, { track: event.track }));
      }
    });
    const mediaSourceAdapter = this._mediaSourceAdapter;
    if (mediaSourceAdapter) {
      this._eventManager.listen(mediaSourceAdapter, CustomEventType.VIDEO_TRACK_CHANGED, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, CustomEventType.AUDIO_TRACK_CHANGED, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, CustomEventType.TEXT_TRACK_CHANGED, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, CustomEventType.IMAGE_TRACK_CHANGED, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, CustomEventType.ABR_MODE_CHANGED, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, CustomEventType.TEXT_CUE_CHANGED, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, CustomEventType.TRACKS_CHANGED, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, CustomEventType.FRAG_LOADED, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, CustomEventType.DRM_LICENSE_LOADED, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, CustomEventType.MANIFEST_LOADED, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, Html5EventType.ERROR, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, Html5EventType.TIME_UPDATE, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, Html5EventType.PLAYING, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, Html5EventType.WAITING, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, CustomEventType.MEDIA_RECOVERED, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, CustomEventType.TIMED_METADATA_ADDED, (event: FakeEvent) => this.dispatchEvent(event));
      this._eventManager.listen(mediaSourceAdapter, 'hlsFragParsingMetadata', (event: FakeEvent) => this.dispatchEvent(event));
      if (this._droppedFramesWatcher) {
        this._eventManager.listen(this._droppedFramesWatcher, CustomEventType.FPS_DROP, (event: FakeEvent) => this.dispatchEvent(event));
      }
    }
  }

  /**
   * Remove the listeners of the video element events.
   * @public
   * @returns {void}
   */
  public detach(): void {
    Object.keys(Html5EventType).forEach((html5Event) => {
      this._eventManager.unlisten(this._el, Html5EventType[html5Event]);
    });
    if (this._mediaSourceAdapter) {
      this._eventManager.unlisten(this._mediaSourceAdapter, CustomEventType.VIDEO_TRACK_CHANGED);
      this._eventManager.unlisten(this._mediaSourceAdapter, CustomEventType.AUDIO_TRACK_CHANGED);
      this._eventManager.unlisten(this._mediaSourceAdapter, CustomEventType.TEXT_TRACK_CHANGED);
      this._eventManager.unlisten(this._mediaSourceAdapter, CustomEventType.TEXT_CUE_CHANGED);
    }
  }

  /**
   * @returns {HTMLVideoElement} - The video element.
   * @public
   */
  public getVideoElement(): HTMLVideoElement {
    return this._el;
  }

  /**
   * Select a new video track.
   * @param {VideoTrack} videoTrack - The video track object to set.
   * @returns {void}
   */
  public selectVideoTrack(videoTrack: VideoTrack): void {
    if (this._mediaSourceAdapter) {
      this._mediaSourceAdapter.selectVideoTrack(videoTrack);
    }
  }

  /**
   * Select a new audio track.
   * @param {AudioTrack} audioTrack - The video track object to set.
   * @returns {void}
   */
  public selectAudioTrack(audioTrack: AudioTrack): void {
    if (this._mediaSourceAdapter) {
      this._mediaSourceAdapter.selectAudioTrack(audioTrack);
    }
  }

  /**
   * Select a new text track.
   * @param {PKTextTrack} textTrack - The playkit text track object to set.
   * @returns {void}
   */
  public selectTextTrack(textTrack: PKTextTrack): void {
    this._removeCueChangeListeners();
    if (this._mediaSourceAdapter) {
      this._mediaSourceAdapter.selectTextTrack(textTrack);
    }
    this.resetAllCues();
    this._addCueChangeListener();
  }

  /**
   * Select a new image track.
   * @param {ImageTrack} imageTrack - The image track object to set.
   * @returns {void}
   */
  public selectImageTrack(imageTrack: ImageTrack): void {
    if (this._mediaSourceAdapter) {
      this._mediaSourceAdapter.selectImageTrack(imageTrack);
    }
  }

  /**
   * Hide the text track
   * @function hideTextTrack
   * @returns {void}
   * @public
   */
  public hideTextTrack(): void {
    if (this._mediaSourceAdapter) {
      this._mediaSourceAdapter.hideTextTrack();
    }
    this._removeCueChangeListeners();
  }

  /**
   * Enables adaptive bitrate switching according to the media source extension logic.
   * @function enableAdaptiveBitrate
   * @returns {void}
   * @public
   */
  public enableAdaptiveBitrate(): void {
    if (this._mediaSourceAdapter) {
      this._mediaSourceAdapter.enableAdaptiveBitrate();
    }
  }

  /**
   * Checking if adaptive bitrate switching is enabled.
   * @function isAdaptiveBitrateEnabled
   * @returns {boolean} - Whether adaptive bitrate is enabled.
   * @public
   */
  public isAdaptiveBitrateEnabled(): boolean {
    if (this._mediaSourceAdapter) {
      return this._mediaSourceAdapter.isAdaptiveBitrateEnabled();
    }
    return false;
  }

  /**
   * Apply ABR restriction
   * @function applyABRRestriction
   * @param {PKABRRestrictionObject} restriction - abr restriction config
   * @returns {void}
   * @public
   */
  public applyABRRestriction(restriction: PKABRRestrictionObject): void {
    if (this._mediaSourceAdapter) {
      return this._mediaSourceAdapter.applyABRRestriction(restriction);
    }
  }

  /**
   * Seeking to live edge.
   * @function seekToLiveEdge
   * @returns {void}
   * @public
   */
  public seekToLiveEdge(): void {
    if (this._mediaSourceAdapter) {
      this._mediaSourceAdapter.seekToLiveEdge();
    }
  }

  public isOnLiveEdge(): boolean {
    if (this._mediaSourceAdapter) {
      return this._mediaSourceAdapter.isOnLiveEdge();
    }
    return false;
  }

  /**
   * Get the start time of DVR window in live playback in seconds.
   * @returns {Number} - start time of DVR window.
   * @public
   */
  public getStartTimeOfDvrWindow(): number {
    return this._mediaSourceAdapter ? this._mediaSourceAdapter.getStartTimeOfDvrWindow() : 0;
  }

  /**
   * Checking if the current playback is live.
   * @function isLive
   * @returns {boolean} - Whether playback is live.
   * @public
   */
  public isLive(): boolean {
    return this._mediaSourceAdapter ? this._mediaSourceAdapter.isLive() : false;
  }

  /**
   * Checks if the video playback has ended and resets it if necessary.
   * Specifically handles Safari in Picture-in-Picture mode for non-live streams.
   *
   * @private
   * @returns {void}
   */
  private resetIfPlaybackEnded(): void {
    const currentTimeMs = Math.round(this._el.currentTime * 1000);
    const durationMs = Math.round(this._el.duration * 1000);

    if (currentTimeMs >= durationMs || this._el.ended) {
      this._el.currentTime = 0;
    }
  }

  /**
   * Start/resume playback.
   * @public
   * @returns {?Promise<*>} - play promise
   */
  public play(): Promise<void> {
    if (Env.isSafari && this.isInPictureInPicture && !this.isLive()) {
      this.resetIfPlaybackEnded();
    }

    const playPromise = this._el.play();
    if (playPromise) {
      playPromise.catch((err) => this.dispatchEvent(new FakeEvent(CustomEventType.PLAY_FAILED, { error: err })));
    }
    return playPromise;
  }

  /**
   * Pause playback.
   * @public
   * @returns {void}
   */
  public pause(): void {
    return this._el.pause();
  }

  /**
   * Load media.
   * @param {number} startTime - Optional time to start the video from.
   * @public
   * @returns {Promise<Object>} - The loaded data
   */
  public load(startTime?: number): Promise<{ tracks: Track[] }> {
    this._el.load();
    return this._canLoadMediaSourceAdapterPromise
      .then(() => {
        return this._mediaSourceAdapter ? this._mediaSourceAdapter.load(startTime) : Promise.resolve({} as { tracks: Track[] });
      })
      .catch((error) => {
        this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, error));
        return Promise.reject(error);
      });
  }

  /**
   * Request the engine to enter picture in picture mode
   * @public
   * @returns {void}
   */
  public enterPictureInPicture(): void {
    try {
      // Currently it's supported in chrome and in safari. So if we consider checking support before,
      // we can use this flag to distinguish between the two. In the future we might need a different method.
      // Second condition is because flow does not support this API yet
      if (document.pictureInPictureEnabled && typeof this._el.requestPictureInPicture === 'function' && !this._el.disablePictureInPicture) {
        this._el.requestPictureInPicture().catch((error) => {
          this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, new Error(Error.Severity.RECOVERABLE, Error.Category.PLAYER, Error.Code.ENTER_PICTURE_IN_PICTURE_FAILED, error)));
        });
        // @ts-expect-error -  Property 'webkitSetPresentationMode' does not exist on type 'HTMLVideoElement'
      } else if (typeof this._el.webkitSetPresentationMode === 'function') {
        // @ts-expect-error -  Property 'webkitSetPresentationMode' does not exist on type 'HTMLVideoElement'
        this._el.webkitSetPresentationMode('picture-in-picture');
        // Safari does not fire this event but Chrome does, normalizing the behaviour
        setTimeout(() => this.dispatchEvent(new FakeEvent(Html5EventType.ENTER_PICTURE_IN_PICTURE)), 0);
      }
    } catch (error) {
      this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, new Error(Error.Severity.RECOVERABLE, Error.Category.PLAYER, Error.Code.ENTER_PICTURE_IN_PICTURE_FAILED, error)));
    }
  }

  /**
   * Request the engine to exit picture in picture mode
   * @public
   * @returns {void}
   */
  public exitPictureInPicture(): void {
    try {
      // Currently it's supported in chrome and in safari. So if we consider checking support before,
      // we can use this flag to distinguish between the two. In the future we might need a different method.
      // Second condition is because flow does not support this API yet
      if (document.pictureInPictureEnabled && typeof document.exitPictureInPicture === 'function' && this._el === document.pictureInPictureElement) {
        document.exitPictureInPicture().catch((error) => {
          this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, new Error(Error.Severity.RECOVERABLE, Error.Category.PLAYER, Error.Code.EXIT_PICTURE_IN_PICTURE_FAILED, error)));
        });
      } else if (typeof this._el['webkitSetPresentationMode'] === 'function') {
        //@ts-expect-error - Property 'webkitSetPresentationMode' does not exist on type 'HTMLVideoElement'.
        this._el.webkitSetPresentationMode('inline');
      }
    } catch (error) {
      this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, new Error(Error.Severity.RECOVERABLE, Error.Category.PLAYER, Error.Code.EXIT_PICTURE_IN_PICTURE_FAILED, error)));
    }
  }

  /**
   * Check if the engine is in picture in picture mode
   * @public
   * @return {boolean} if the engine is in picture in picture mode or not
   */
  public isPictureInPictureSupported(): boolean {
    // due to a bug in shaka pip_webkit which sets pictureInPictureEnabled to true in unsupported devices like iphones we will
    // first rely on the response of webkitSupportsPresentationMode (if exists) and only if not on the pictureInPictureEnabled property
    //@ts-expect-error - Property 'webkitSupportsPresentationMode' does not exist on type 'HTMLVideoElement'.
    if (typeof this._el.webkitSupportsPresentationMode === 'function') {
      //@ts-expect-error - Property 'webkitSupportsPresentationMode' does not exist on type 'HTMLVideoElement'.
      return this._el.webkitSupportsPresentationMode('picture-in-picture');
    } else {
      return !!document.pictureInPictureEnabled;
    }
  }

  /**
   *  Returns in-stream thumbnail for a chosen time.
   * @param {number} time - playback time.
   * @public
   * @return {?ThumbnailInfo} - Thumbnail info
   */
  public getThumbnail(time: number): ThumbnailInfo | null {
    if (this._mediaSourceAdapter) {
      return this._mediaSourceAdapter.getThumbnail(time);
    }
    return null;
  }

  /**
   * Set a source.
   * @param {string} source - Source to set.
   * @public
   * @returns {void}
   */
  public set src(source: string) {
    if (this._mediaSourceAdapter) {
      this._mediaSourceAdapter.src = source;
    }
  }

  /**
   * Get the source url.
   * @returns {string} - The source url.
   * @public
   */
  public get src(): string {
    if (this._mediaSourceAdapter) {
      return this._mediaSourceAdapter.src;
    }
    return '';
  }

  /**
   * Get the current time in seconds.
   * @returns {Number} - The current playback time.
   * @public
   */
  public get currentTime(): number {
    return this._el ? this._el.currentTime : 0;
  }

  /**
   * Set the current time in seconds.
   * @param {Number} to - The number to set in seconds.
   * @public
   * @returns {void}
   */
  public set currentTime(to: number) {
    if (this._el) {
      this._el.currentTime = to;
    }
  }

  /**
   * Get the duration in seconds.
   * @returns {Number} - The playback duration.
   * @public
   */
  public get duration(): number {
    return this._el.duration;
  }

  public get liveDuration(): number {
    return this._mediaSourceAdapter ? this._mediaSourceAdapter.liveDuration : -1;
  }

  /**
   * Set playback volume.
   * @param {Number} vol - The volume to set.
   * @public
   * @returns {void}
   */
  public set volume(vol: number) {
    this._el.volume = vol;
  }

  /**
   * Get playback volume.
   * @returns {Number} - The volume value of the video element.
   * @public
   */
  public get volume(): number {
    return this._el.volume;
  }

  /**
   * Get paused state.
   * @returns {boolean} - The paused value of the video element.
   * @public
   */
  public get paused(): boolean {
    return this._el.paused;
  }

  /**
   * Get seeking state.
   * @returns {boolean} - The seeking value of the video element.
   * @public
   */
  public get seeking(): boolean {
    return this._el.seeking;
  }

  /**
   * Get the first seekable range (part) of the video in seconds.
   * @returns {TimeRanges} - First seekable range (part) of the video in seconds.
   * @public
   */
  public get seekable(): TimeRanges {
    return this._el.seekable;
  }

  /**
   * Get the first played range (part) of the video in seconds.
   * @returns {TimeRanges} - First played range (part) of the video in seconds.
   * @public
   */
  public get played(): TimeRanges {
    return this._el.played;
  }

  /**
   * Get the first buffered range (part) of the video in seconds.
   * @returns {TimeRanges} - First buffered range (part) of the video in seconds.
   * @public
   */
  public get buffered(): TimeRanges {
    return this._el.buffered;
  }

  /**
   * Set player muted state.
   * @param {boolean} mute - The new mute value.
   * @public
   * @returns {void}
   */
  public set muted(mute: boolean) {
    this._el.muted = mute;
  }

  /**
   * Get player muted state.
   * @returns {boolean} - The muted value of the video element.
   * @public
   */
  public get muted(): boolean {
    return this._el.muted;
  }

  /**
   * Get the default mute value.
   * @returns {boolean} - The defaultMuted of the video element.
   * @public
   */
  public get defaultMuted(): boolean {
    return this._el.defaultMuted;
  }

  /**
   * Sets an image to be shown while the video is downloading, or until the user hits the play button.
   * @param {string} poster - The image url to be shown.
   * @returns {void}
   * @public
   */
  public set poster(poster: string) {
    this._el.poster = poster;
  }

  /**
   * Gets an image to be shown while the video is downloading, or until the user hits the play button.
   * @returns {poster} - The image url.
   * @public
   */
  public get poster(): string {
    return this._el.poster;
  }

  /**
   * Specifies if and how the author thinks that the video should be loaded when the page loads.
   * @param {string} preload - The preload value.
   * @public
   * @returns {void}
   */
  public set preload(preload: 'none' | 'metadata' | 'auto' | '') {
    this._el.preload = preload;
  }

  /**
   * Gets the preload value of the video element.
   * @returns {string} - The preload value.
   * @public
   */
  public get preload(): 'none' | 'metadata' | 'auto' | '' {
    return this._el.preload;
  }

  /**
   * Set if the video will automatically start playing as soon as it can do so without stopping.
   * @param {boolean} autoplay - The autoplay value.
   * @public
   * @returns {void}
   */
  public set autoplay(autoplay: boolean) {
    this._el.autoplay = autoplay;
  }

  /**
   * Gets the autoplay value of the video element.
   * @returns {boolean} - The autoplay value.
   * @public
   */
  public get autoplay(): boolean {
    return this._el.autoplay;
  }

  /**
   * Set to specifies that the video will start over again, every time it is finished.
   * @param {boolean} loop - the loop value.
   * @public
   * @returns {void}
   */
  public set loop(loop: boolean) {
    this._el.loop = loop;
  }

  /**
   * Gets the loop value of the video element.
   * @returns {boolean} - The loop value.
   * @public
   */
  public get loop(): boolean {
    return this._el.loop;
  }

  /**
   * Set to specifies that video controls should be displayed.
   * @param {boolean} controls - the controls value.
   * @public
   * @returns {void}
   */
  public set controls(controls: boolean) {
    this._el.controls = controls;
  }

  /**
   * Gets the controls value of the video element.
   * @returns {boolean} - The controls value.
   * @public
   */
  public get controls(): boolean {
    return this._el.controls;
  }

  /**
   * Sets the current playback speed of the audio/video.
   * @param {Number} playbackRate - The playback speed value.
   * @public
   * @returns {void}
   */
  public set playbackRate(playbackRate: number) {
    this._el.playbackRate = playbackRate;
  }

  /**
   * Gets the current playback speed of the audio/video.
   * @returns {Number} - The current playback speed value.
   * @public
   */
  public get playbackRate(): number {
    return this._el.playbackRate;
  }

  /**
   * Sets the default playback speed of the audio/video.
   * @param {Number} defaultPlaybackRate - The default playback speed value.
   * @public
   * @returns {void}
   */
  public set defaultPlaybackRate(defaultPlaybackRate: number) {
    this._el.defaultPlaybackRate = defaultPlaybackRate;
  }

  /**
   * Gets the default playback speed of the audio/video.
   * @returns {Number} - The default playback speed value.
   * @public
   */
  public get defaultPlaybackRate(): number {
    return this._el.defaultPlaybackRate;
  }

  /**
   * The ended property returns whether the playback of the audio/video has ended.
   * @returns {boolean} - The ended value.
   * @public
   */
  public get ended(): boolean {
    return this._el.ended;
  }

  /**
   * The error property returns a MediaError object.
   * @returns {MediaError} - The MediaError object has a code property containing the error state of the audio/video.
   * @public
   */
  public get error(): MediaError | null {
    return this._el.error;
  }

  /**
   * @returns {Number} - The current network state (activity) of the audio/video.
   * @public
   */
  public get networkState(): number {
    return this._el.networkState;
  }

  /**
   * Indicates if the audio/video is ready to play or not.
   * @returns {Number} - The current ready state of the audio/video.
   * 0 = HAVE_NOTHING - no information whether or not the audio/video is ready.
   * 1 = HAVE_METADATA - metadata for the audio/video is ready.
   * 2 = HAVE_CURRENT_DATA - data for the current playback position is available, but not enough data to play next frame/millisecond.
   * 3 = HAVE_FUTURE_DATA - data for the current and at least the next frame is available.
   * 4 = HAVE_ENOUGH_DATA - enough data available to start playing.
   */
  public get readyState(): number {
    return this._el.readyState;
  }

  /**
   * @returns {Number} - The height of the video player, in pixels.
   * @public
   */
  public get videoHeight(): number {
    return this._el.videoHeight;
  }

  /**
   * @returns {Number} - The width of the video player, in pixels.
   * @public
   */
  public get videoWidth(): number {
    return this._el.videoWidth;
  }

  /**
   * @param {boolean} playsinline - Whether to set on the video tag the playsinline attribute.
   */
  public set playsinline(playsinline: boolean) {
    if (playsinline) {
      this._el.setAttribute('playsinline', '');
    } else {
      this._el.removeAttribute('playsinline');
    }
  }

  /**
   * @returns {boolean} - Whether the video tag has an attribute of playsinline.
   */
  public get playsinline(): boolean {
    return this._el.getAttribute('playsinline') === '';
  }

  /**
   * Set crossOrigin attribute.
   * @param {?string} crossOrigin - 'anonymous' or 'use-credentials'
   */
  public set crossOrigin(crossOrigin: string) {
    if (typeof crossOrigin === 'string') {
      this._el.setAttribute('crossorigin', crossOrigin);
    } else {
      this._el.removeAttribute('crossorigin');
    }
  }

  /**
   * Get crossOrigin attribute.
   * @returns {?string} - 'anonymous' or 'use-credentials'
   */
  public get crossOrigin(): string | null {
    return this._el.getAttribute('crossorigin');
  }

  /**
   * get the playback rates
   * @return {number[]} - playback rates
   */
  public get playbackRates(): Array<number> {
    return Html5.PLAYBACK_RATES;
  }

  /**
   * get if the engine's video element is the one in the PIP
   * @return {boolean} boolean - is in PIP
   */
  public get isInPictureInPicture(): boolean {
    // Check if the engine's video element is the one in the PIP
    return (
      (!!document.pictureInPictureElement && document.pictureInPictureElement !== null && this._el === document.pictureInPictureElement) ||
      // @ts-expect-error - Property 'webkitPresentationMode' does not exist on type 'HTMLVideoElement'
      (!!this._el.webkitPresentationMode && this._el.webkitPresentationMode === 'picture-in-picture')
    );
  }

  /**
   * Initializes the engine.
   * @param {PKMediaSourceObject} source - The selected source object.
   * @param {Object} config - The player configuration.
   * @private
   * @returns {void}
   */
  private _init(source: PKMediaSourceObject, config: any): void {
    this._config = config;
    this._reset = false;
    this._loadMediaSourceAdapter(source);
    this.attach();
  }

  /**
   * Creates a video element dom object.
   * @param {string} playerId - the id to be set as the key of the video element
   * @private
   * @returns {void}
   */
  private _createVideoElement(playerId: string): void {
    this._el = Html5.videoElementStore[playerId] || Utils.Dom.createElement('video');
    this._el.id = Utils.Generator.uniqueId(5);
    this._el.controls = false;
  }

  /**
   * Loads the appropriate media source extension adapter.
   * @param {PKMediaSourceObject} source - The selected source object.
   * @private
   * @returns {void}
   */
  private _loadMediaSourceAdapter(source: PKMediaSourceObject): void {
    this._mediaSourceAdapter = MediaSourceProvider.getMediaSourceAdapter(this.getVideoElement(), source, this._config);
    if (this._mediaSourceAdapter) {
      this._droppedFramesWatcher = new DroppedFramesWatcher(this._mediaSourceAdapter, this._config.abr, this._el);

      if (this._cachedUrls.length) {
        this._mediaSourceAdapter.setCachedUrls(this._cachedUrls);
        this._cachedUrls = [];
      }
    }
  }

  /**
   * Add cuechange listener to active textTrack.
   * @returns {void}
   * @private
   */
  private _addCueChangeListener(): void {
    const textTrackEl = Array.from(this._el.textTracks).find((track) => PKTextTrack.isNativeTextTrack(track) && track.mode !== PKTextTrack.MODE.DISABLED);
    if (textTrackEl) {
      this._eventManager.listen(textTrackEl, 'cuechange', (e: FakeEvent) => this._onCueChange(e));
    }
  }

  /**
   * Remove cuechange listeners from textTracks
   * @returns {void}
   * @private
   */
  private _removeCueChangeListeners(): void {
    Array.from(this._el.textTracks)
      .filter((track) => !PKTextTrack.isMetaDataTrack(track))
      .forEach((track) => {
        this._eventManager.unlisten(track, 'cuechange');
      });
  }

  /**
   * oncuechange event handler.
   * @param {FakeEvent} e - The event arg.
   * @returns {void}
   * @private
   */
  private _onCueChange(e: FakeEvent): void {
    const activeCues: TextTrackCueList = e.currentTarget.activeCues;
    const normalizedActiveCues = getActiveCues(activeCues);
    this.dispatchEvent(new FakeEvent(CustomEventType.TEXT_CUE_CHANGED, { cues: normalizedActiveCues }));
  }

  /**
   * set hasBeenReset to true for all the cues. (use case: when cues should be recalculated for display)
   * @returns {void}
   */
  public resetAllCues(): void {
    const activeTextTrack = Array.from(this._el.textTracks).find((track) => PKTextTrack.isNativeTextTrack(track) && track.mode !== PKTextTrack.MODE.DISABLED);
    if (activeTextTrack) {
      for (let i = 0; i < activeTextTrack.cues!.length; i++) {
        // @ts-expect-error - Property 'hasBeenReset' does not exist on type 'TextTrackCue'
        activeTextTrack.cues![i].hasBeenReset = true;
      }
    }
  }

  /**
   * Handles errors from the video element
   * @returns {void}
   * @private
   */
  private _handleVideoError(): void {
    if (!this._el.error) return;
    const code = this._el.error.code;
    if (code === MediaError.MEDIA_ERR_ABORTED) {
      // Ignore this error code.js, which should only occur when navigating away or
      // deliberately stopping playback of HTTP content.
      return;
    }

    // Extra error information from MS Edge and IE11:
    const extended = this._getMsExtendedError();

    // Extra error information from Chrome:
    // $FlowFixMe
    const message = this._el.error.message;
    if (this._mediaSourceAdapter && !this._mediaSourceAdapter.handleMediaError(this._el.error)) {
      const error = new Error(Error.Severity.CRITICAL, Error.Category.MEDIA, Error.Code.VIDEO_ERROR, {
        code: code,
        extended: extended,
        message: message
      });
      this.dispatchEvent(new FakeEvent(Html5EventType.ERROR, error));
    }
  }

  private _handleWaiting(): void {
    let playing = false;
    this._eventManager.listenOnce(this._el, Html5EventType.PLAYING, () => (playing = true));
    setTimeout(() => {
      // prevent sending waiting event for too short buffering
      if (!playing) {
        this.dispatchEvent(new FakeEvent(Html5EventType.WAITING));
      }
    }, SHORT_BUFFERING_TIMEOUT);
  }

  /**
   * more info about the error
   * @returns {string} info about the video element error
   * @private
   */
  private _getMsExtendedError(): string {
    //@ts-expect-error - Property 'msExtendedCode' does not exist on type 'MediaError'
    let extended = this._el.error.msExtendedCode;
    if (extended) {
      // Convert to unsigned:
      if (extended < 0) {
        extended += Math.pow(2, 32);
      }
      // Format as hex:
      extended = extended.toString(16);
    }
    return extended;
  }

  private _handleMetadataTrackEvents(): void {
    const listenToCueChange = (metadataTrack: any): void => {
      metadataTrack.mode = PKTextTrack.MODE.HIDDEN;
      this._eventManager.listen(metadataTrack, 'cuechange', () => {
        let activeCues: VTTCue[] = [];
        Array.from(this._el.textTracks).forEach((track: TextTrack) => {
          if (PKTextTrack.isMetaDataTrack(track)) {
            activeCues = activeCues.concat(getActiveCues(track.activeCues!));
          }
        });
        activeCues = activeCues.sort((a: VTTCue, b: VTTCue) => {
          return a.startTime - b.startTime;
        });
        this.dispatchEvent(new FakeEvent(CustomEventType.TIMED_METADATA, { cues: activeCues }));
        this.dispatchEvent(
          new FakeEvent(CustomEventType.TIMED_METADATA_CHANGE, {
            cues: activeCues.map((cue) => createTimedMetadata(cue))
          })
        );
      });
    };

    Array.from(this._el.textTracks).forEach((track: TextTrack) => {
      if (PKTextTrack.isMetaDataTrack(track)) {
        listenToCueChange(track);
      }
    });

    this._eventManager.listen(this._el.textTracks, 'addtrack', (event: any) => {
      if (PKTextTrack.isMetaDataTrack(event.track)) {
        listenToCueChange(event.track);
      }
    });

    this._eventManager.listen(this._el.textTracks, 'change', () => {
      Array.from(this._el.textTracks).forEach((track: TextTrack) => {
        if (PKTextTrack.isMetaDataTrack(track) && track.mode !== PKTextTrack.MODE.HIDDEN) {
          track.mode = PKTextTrack.MODE.HIDDEN;
        }
      });
    });
  }

  public get targetBuffer(): number {
    if (this._mediaSourceAdapter) {
      return this._mediaSourceAdapter.targetBuffer;
    }
    return NaN;
  }

  public get availableBuffer(): number {
    let retVal = 0;
    if (this.buffered) {
      for (let i = 0; i < this.buffered.length; i++) {
        // find the relevant buffer time range containing the current time
        if (this.buffered.start(i) <= this._el.currentTime && this._el.currentTime <= this.buffered.end(i)) {
          retVal = this.buffered.end(i) - this._el.currentTime;
        }
      }
    }
    return retVal;
  }

  public addTextTrack(kind: TextTrackKind, label?: string, language?: string): TextTrack | undefined {
    return this._el.addTextTrack(kind, label, language);
  }

  /**
   * get the native text tracks
   * @function getNativeTextTracks
   * @returns {Array<TextTrack>} - The native TextTracks array.
   * @public
   */
  public getNativeTextTracks(): TextTrack[] {
    return Array.from(this._el.textTracks);
  }

  public getDrmInfo(): PKDrmDataObject | null {
    return this._mediaSourceAdapter ? this._mediaSourceAdapter.getDrmInfo() : null;
  }

  public setCachedUrls(cachedUrls: string[]): void {
    this._cachedUrls = cachedUrls;

    if (this._mediaSourceAdapter) {
      this._mediaSourceAdapter.setCachedUrls(cachedUrls);
      this._cachedUrls = [];
    }
  }
}
