import * as R from "ramda";
import { BehaviorSubject } from "rxjs";
import { isEmptyOrNil } from "@applicaster/zapp-react-native-utils/cellUtils";
import { createLogger, utilsLogger } from "../../logger";
import { getBoolFromConfigValue } from "../../configurationUtils";
import { PlayerRole, SharedPlayerCallBacksKeys } from "./conts";
import { OverlaysObserver } from "./OverlayObserver/OverlaysObserver";

const logger = createLogger({
  category: "PlayerController",
  subsystem: "General",
  parent: utilsLogger,
});

const { log_verbose, log_info, log_error } = logger;

export declare type PlayerControllerListenerData = {
  id: string;
  listener: QuickBrickPlayer.SharedPlayerCallBacks;
};
/**
 * Player Instance Controller
 * Instance exposes utility methods that will help set or get state
 * Keeps track of the difference between ad content and regular content
 * Creates and keeps track of all of the player instances created
 * Inherited all of the methods that used to exist in the player manager
 * PlayerManager continues to be used for register, unregister of player,
 * and subscription to events. We only support one castInstance at a time.
 * When using playerManager.on we have to use fat arrow to not loose context of this
 */

// TODO: Must be abstract, can do it until fill be fixed babel/metro/webpack

export class Player {
  readonly playerId: string;
  readonly playerState: QuickBrickPlayer.PlayerState;

  public playerPluginId: string;
  protected _entry: ZappEntry | null;

  protected config?: Record<string, any>;

  protected startPosition?: number | null;
  readonly playerRole: PlayerRole = PlayerRole.Unspecified;
  private overlaysObserver?: OverlaysObserver | null;
  private entryObservable?: BehaviorSubject<ZappEntry> | null;

  protected listeners: {
    [key: string]: QuickBrickPlayer.SharedPlayerCallBacks;
  } = {};

  get entry(): ZappEntry | null {
    return this._entry;
  }

  set entry(newEntry: ZappEntry | null) {
    this._entry = newEntry;
    this.entryObservable?.next(newEntry);
  }

  constructor(props) {
    this.playerState = {
      // We want isLive null by default to be able not to show controls if we do not know type
      isLive: null,
      isPaused: R.isNil(props?.autoplay) ? null : !props?.autoplay,
      isMuted: props?.muted || false,
      isBuffering: null,
      // TODO: Reset if reference null, view destroyed
      isReadyToPlay: false,
      seekableDuration: null,
      seekPosition: null,
      contentDuration: null,
      contentPosition: null,
      adState: null,
      trackState: {
        textTracks: [],
        audioTracks: [],
        audioTrackId: null,
        textTrackId: null,
      },
    };

    this.playerId = props.playerId || "error, no player id";

    this.listeners = {};
    this.startPosition = props?.startPosition || null;

    this.config = props.config || null;
    this.playerRole = props.playerRole || PlayerRole.Unspecified;

    this.onVideoProgress = this.onVideoProgress.bind(this);
    this.onVideoLoad = this.onVideoLoad.bind(this);
    this.isFullScreenSupported = this.isFullScreenSupported.bind(this);
    this.entryObservable = null;
  }

  getOverlayObservable = () => {
    if (!this.overlaysObserver) {
      this.overlaysObserver = new OverlaysObserver({ player: this });
    }

    return this.overlaysObserver;
  };

  getEntryObservable = (): BehaviorSubject<ZappEntry | null> => {
    if (!this.entryObservable) {
      this.entryObservable = new BehaviorSubject(this.entry);
    }

    return this.entryObservable;
  };

  /**
   * ---------------------------------------- PLAYER SETTERS GETTERS ----------------------------------------
   * These methods let you get information about the content / ad playing
   * or they let you get set certain properties of the player directly
   */

  getContentPosition() {
    return this.playerState.contentPosition;
  }

  /**
   * getContent duration returns the duration of the playing content
   * This is the method we use when we don't want to display adDuration
   * This duration should not change during playback of ads.
   */
  getContentDuration = () => {
    return this.playerState.contentDuration;
  };

  getSeekableDuration = () => {
    return this.playerState.seekableDuration;
  };

  /**
   * getDuration returns the duration of the currentAd or the content
   * Used for situations where you want to know the duration of whatever is playing
   */
  getDuration = () => {
    return this.playerState.adState
      ? this.playerState.adState.adDuration
      : this.playerState.contentDuration;
  };

  getPosition = () =>
    this.isSeeking()
      ? this.playerState.seekPosition
      : this.playerState.adState
        ? this.playerState.adState.adPosition
        : this.playerState.contentPosition;

  getEntry = (): ZappEntry | null => this.entry;

  logState = (text, additionalParams = {}) => {
    const isSeeking = this.playerState.seekPosition !== null;

    log_verbose(
      `logState id: ${this.playerId}: ${text}, title: ${this.entry?.title}, contentPosition: ${this.playerState.contentPosition}, seekPosition: ${this.playerState.seekPosition}, isSeeking: ${isSeeking}`,
      {
        playerState: this.playerState,
        isLiveCheck: this.isLive(),
        isSeeking: isSeeking,
        isAd: this.isAd(),
        duration: this.getDuration(),
        entryId: this.entry?.id,
        playerId: this.playerId,
        playerRole: this.playerRole,
        ...additionalParams,
        item: null, // it's too verbose for verbose log
      }
    );
  };

  /**
   * Handle events that have currentTime and duration updates
   */
  setPlaybackState = ({ currentTime, duration, seekableDuration, isLive }) => {
    if (isEmptyOrNil(currentTime)) {
      this.logState("Event tried to set invalid playback state", {
        currentTime,
        duration,
        seekableDuration,
      });

      return;
    }

    this.playerState.isLive = isLive;

    if (this.isAd()) {
      this.playerState.adState = {
        adDuration: duration,
        adPosition: currentTime,
      };
    } else {
      if (!isEmptyOrNil(seekableDuration) && seekableDuration >= 0) {
        this.playerState.seekableDuration = seekableDuration;
      }

      if (!isEmptyOrNil(duration) && duration >= 0) {
        this.playerState.contentDuration = duration;
      }

      if (!isEmptyOrNil(currentTime) && currentTime >= 0) {
        this.playerState.contentPosition = currentTime;
      }
    }
  };

  getState = () => null;

  hasSeekableDuration = () => {
    const duration = this.getSeekableDuration();

    if (typeof duration !== "number") {
      return false;
    }

    if (Number.isNaN(duration)) {
      return false;
    }

    return duration > 0;
  };

  hasContentDuration = () => {
    const duration = this.getContentDuration();

    if (typeof duration !== "number") {
      return false;
    }

    if (Number.isNaN(duration)) {
      return false;
    }

    return duration > 0;
  };

  isAd = () => {
    return !!this.playerState.adState || this.playerState.isInAdBreak;
  };

  isSeeking = () => {
    return this.playerState.seekPosition !== null;
  };

  isPaused = () => {
    return !!this.playerState.isPaused;
  };

  isPlaying = () => {
    return !!this.playerState.isPaused === false;
  };

  getIsMuted = () => {
    return this.playerState.isMuted;
  };

  mute = () => {
    this.playerState.isMuted = true;

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnPlayerMute,
    });
  };

  unmute = () => {
    this.playerState.isMuted = false;

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnPlayerUnmute,
    });
  };

  isBuffering = (): boolean => {
    return this.playerState.isBuffering;
  };

  isReadyToPlay = (): boolean => {
    return this.playerState.isReadyToPlay;
  };

  // Methods to call actions on the player
  seekTo = (_position: number) => {};

  forward = (_deltaTime: number) => {};

  rewind = (_deltaTime: number) => {};

  /**
   * Determine whether content is live using three checks
   * First checks feed to see if content is specified as live
   * Second checks if player identified a duration for the content
   * Future iterations could just receive a prop from the native player
   * This order gives users the ability to specify what is live before the player
   */
  isLive = () => {
    const entry = this.getEntry();

    if (entry) {
      const entryDeclaredAsLive =
        entry.type?.value === "channel" ||
        getBoolFromConfigValue(entry.extensions?.live) ||
        getBoolFromConfigValue(entry.extensions?.isLive);

      if (entryDeclaredAsLive) {
        return true;
      }
    }

    return this.playerState.isLive;
  };

  /**
   * ---------------------------------------- PLAYER EVENT HANDLERS ----------------------------------------
   * The legacy methods are calling the player's event handler directly, newer events handle the event in this class
   * For instance onVideoProgress, onVideoLoad
   * Only extending these to support legacy behavior but if you are triggering an event
   * You should use dispatchPlayerEvent at PlayerContainer or call playerManager.invokeHandler directly
   * This allows you to use one method for everything.
   */
  onSeekStart = (event: QuickBrickPlayer.OnSeekStartEvent) => {
    const { isLive, seekableDuration, currentTime, duration, seekTime } = event;

    this.setPlaybackState({ currentTime, duration, seekableDuration, isLive });

    if (this.playerState.seekPosition === null) {
      this.playerState.seekPosition = seekTime;
    }

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnPlayerSeekStart,
      event,
    });
  };

  onSeekComplete = (event: QuickBrickPlayer.OnSeekCompleteEvent) => {
    const { isLive, seekableDuration, currentTime, duration } = event;

    this.setPlaybackState({ currentTime, duration, seekableDuration, isLive });
    this.playerState.seekPosition = null;

    if (!isLive) {
      this.playerState.contentPosition = currentTime;
    }

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnPlayerSeekComplete,
      event,
    });
  };

  onVideoEnd = (event) => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnVideoEnd,
      event,
    });
  };

  onVideoError = (error: Error) => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnVideoError,
      event: error,
    });
  };

  onError = (error: Error) => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnError,
      event: error,
    });
  };

  onPlayerPause = (event) => {
    this.playerState.isPaused = true;

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnPlayerPause,
      event,
    });
  };

  onPlayerResume = (event) => {
    this.playerState.isPaused = false;

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnPlayerResume,
      event,
    });
  };

  onPlaybackRateChange = (event) => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnPlaybackRateChange,
      event,
    });
  };

  onTracksChanged = (event: QuickBrickPlayer.TracksState) => {
    this.playerState.trackState = event;

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnTracksChanged,
      event,
    });
  };

  onBufferStart = (event: QuickBrickPlayer.OnBufferEvent) => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnBufferStart,
      event,
    });
  };

  onBufferComplete = (event: QuickBrickPlayer.OnBufferEvent) => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnBufferComplete,
      event,
    });
  };

  onVideoFullscreenPlayerWillDismiss = () => {
    this.invokeListenersUpdate({
      callbackName:
        SharedPlayerCallBacksKeys.onVideoFullscreenPlayerWillDismiss,
    });
  };

  onVideoFullscreenPlayerDidDismiss = () => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.onVideoFullscreenPlayerDidDismiss,
    });
  };

  onVideoFullscreenPlayerWillPresent = () => {
    this.invokeListenersUpdate({
      callbackName:
        SharedPlayerCallBacksKeys.onVideoFullscreenPlayerWillPresent,
    });
  };

  onVideoFullscreenPlayerDidPresent = () => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.onVideoFullscreenPlayerDidPresent,
    });
  };

  public invokeListenersUpdate = ({
    callbackName,
    event = {},
  }: {
    callbackName: SharedPlayerCallBacksKeys;
    event?: Record<string, any>;
  }) => {
    for (const [key, listener] of Object.entries(this.listeners)) {
      try {
        listener[callbackName]?.(event);
      } catch (error) {
        log_error(
          `invokeListenersUpdate: Error: listenerId: ${key}, invoking callback: ${callbackName}, message: ${error?.message}`,
          { error }
        );
      }
    }

    this.logState(callbackName, event);
  };

  onLoad = (event) => {
    const { currentTime, duration, seekableDuration, isLive = false } = event;

    this.setPlaybackState({ currentTime, duration, seekableDuration, isLive });

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnLoad,
      event: { ...event, entry: this.getEntry() },
    });
  };

  onVideoLoad(event) {
    const { currentTime, duration, seekableDuration, isLive = false } = event;
    const shouldCallOnLoad = !this.playerState.isReadyToPlay;
    this.playerState.isReadyToPlay = true;
    this.setPlaybackState({ currentTime, duration, seekableDuration, isLive });

    if (shouldCallOnLoad) {
      this.onLoad?.(event);
    }

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnVideoLoad,
      event,
    });
  }

  /**
   * Util method that updates the ad or content playback position in the player interface
   * Keeps ad and content position / duration data seperate
   */
  onVideoProgress(event) {
    const { currentTime, duration, seekableDuration, isLive = false } = event;

    if (this.isSeeking()) {
      this.setPlaybackState({
        currentTime: this.playerState.seekPosition,
        duration,
        seekableDuration,
        isLive,
      });

      this.notifyPlayHeadPositionUpdate();

      return;
    }

    this.setPlaybackState({ currentTime, duration, seekableDuration, isLive });

    this.notifyPlayHeadPositionUpdate();
  }

  onPlayerClose = () => {
    this.playerState.isReadyToPlay = false;

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnPlayerClose,
    });

    this.entryObservable?.complete();
  };

  onAdBegin = (event) => {
    this.playerState.adState = {
      adPosition: null,
      adDuration: null,
    };

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnAdBegin,
      event,
    });
  };

  onAdBreakBegin = (event) => {
    this.playerState.isInAdBreak = true;

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnAdBreakBegin,
      event,
    });
  };

  onAdBreakEnd = (event) => {
    this.playerState.adState = null;
    this.playerState.isInAdBreak = false;

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnAdBreakEnd,
      event,
    });
  };

  onAdEnd = (event) => {
    this.playerState.adState = null;

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnAdEnd,
      event,
    });
  };

  onAdError = (event) => {
    this.playerState.adState = null;

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnAdError,
      event,
    });
  };

  onAdRequest = (event) => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnAdRequest,
      event,
    });
  };

  onAdClicked = (event) => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnAdClicked,
      event,
    });
  };

  onAdTapped = (event) => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnAdTapped,
      event,
    });
  };

  onPlayerDetached = (event) => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnPlayerDetached,
      event,
    });
  };

  onPlayerAttached = (event) => {
    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnPlayerAttached,
      event,
    });
  };

  getTracksState = (): QuickBrickPlayer.TracksState =>
    this.playerState.trackState;

  selectTrack = (_track) => {};

  protected notifyPlayHeadPositionUpdate = () => {
    const event = {
      currentTime: this.getPosition(),
      duration: this.getDuration(),
      item: this.getEntry(),
      seekableDuration: this.playerState.seekableDuration,
      isLive: this.isLive(),
    };

    this.invokeListenersUpdate({
      callbackName: SharedPlayerCallBacksKeys.OnVideoProgress,
      event,
    });
  };

  addListener = ({ id, listener }: PlayerControllerListenerData) => {
    if (!R.isNil(this.listeners[id])) {
      log_error(
        `addListener: Listener already exists for id: ${id}, listener will not be added`
      );

      return;
    }

    this.listeners[id] = listener;

    return () => this.removeListener(id);
  };

  removeListener = (id: string) => {
    delete this.listeners[id];
  };

  public getListener = (): QuickBrickPlayer.SharedPlayerCallBacks => ({
    onPlayerSeekStart: this.onSeekStart,
    onPlayerSeekComplete: this.onSeekComplete,
    onVideoEnd: this.onVideoEnd,
    onError: this.onError,
    onVideoError: this.onVideoError,
    onLoad: this.onLoad,
    onPlayerPause: this.onPlayerPause,
    onPlayerResume: this.onPlayerResume,
    onPlaybackRateChange: this.onPlaybackRateChange,
    onVideoProgress: this.onVideoProgress,
    onTracksChanged: this.onTracksChanged,
    onVideoLoad: this.onVideoLoad,
    onBufferStart: this.onBufferStart,
    onBufferComplete: this.onBufferComplete,

    onPlayerClose: this.onPlayerClose,
    onVideoFullscreenPlayerWillPresent: this.onVideoFullscreenPlayerWillPresent,
    onVideoFullscreenPlayerDidPresent: this.onVideoFullscreenPlayerDidPresent,
    onVideoFullscreenPlayerWillDismiss: this.onVideoFullscreenPlayerWillDismiss,
    onVideoFullscreenPlayerDidDismiss: this.onVideoFullscreenPlayerDidDismiss,

    // Ads Callbacks:
    onAdBegin: this.onAdBegin,
    onAdBreakBegin: this.onAdBreakBegin,
    onAdBreakEnd: this.onAdBreakEnd,
    onAdEnd: this.onAdEnd,
    onAdError: this.onAdError,
    onAdRequest: this.onAdRequest,
    onAdClicked: this.onAdClicked,
    onAdTapped: this.onAdTapped,

    // PIP
    onPlayerDetached: this.onPlayerDetached,
    onPlayerAttached: this.onPlayerAttached,
  });

  play = () => {};
  pause = () => {};
  disableBufferAnimation = (): boolean => true;

  setPlaybackRate = (_rate) => {};
  startSleepTimer = (_timestamp) => {};
  cancelSleepTimer = () => {};

  getPluginConfiguration = () => null;
  appStateChange = (_appState, _previousAppState) => {};

  close = () => {};
  closeNativePlayer = () => {};
  togglePlayPause = () => {};

  isCellPlayer = () => false;

  getContinueWatchingOffset = ({ entry, ignoreContinueWatching = false }) => {
    if (ignoreContinueWatching) {
      log_info(
        "getContinueWatchingOffset: ignoreContinueWatching is true, skipping continue watching data"
      );

      return null;
    }

    const resumeTime = Number(entry?.extensions?.resumeTime);

    if (resumeTime && !isNaN(resumeTime)) {
      return resumeTime;
    }

    return null;
  };

  public getConfig = () => this.config;

  // TODO: replace with enum of supported orientations
  isFullScreenSupported(): boolean {
    return true;
  }

  public supportsNativeControls = (): boolean => false;
  public supportNativeCast = (): boolean => false;
  public destroy = () => {};
}
