import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
import {
  PlayerLifecycleListener,
  playerManager,
} from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
import { setUserCellPlayerMutedPreference } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/userCellPlayerMutedPreference";
import { loggerLiveImageManager } from "./loggerHelper";
import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
import { Component } from "react";

const TIMEOUT_FOR_DELAY_CHECK_PLAYER_POSITION = 500; // ms

const { log_debug, log_info, log_error } = loggerLiveImageManager;

export type LiveImageManagerEvent =
  | "onEnd"
  | "onStart"
  | "onError"
  | "onDestroy";

export type LiveImageManagerListenerData = {
  id: string;
  listener: Record<
    LiveImageManagerEvent,
    (event?: Record<string, any>) => void
  >;
};

export enum LiveImageType {
  Video = "video",
  Image = "image",
}

type Position = {
  centerX: number;
  centerY: number;
  top: number;
  bottom: number;
  left: number;
  right: number;
};

type LiveImageProps = {
  player: Player;
  playerId: string;
  setMode?: (type: LiveImageType) => void;
  component: Component;
  // TODO: ...primary, powerCell, tvGallery, etc.
  // type: string;
};

// Disabled because we have only unmute button but no play/pause state anymore
const IS_ALLOWED_REPLAY = false;

const playerInfo = (player: Player | null): string =>
  player
    ? `playerId: ${player.playerId}, title: ${player.getEntry()?.title}`
    : "null";

export class LiveImageManager implements PlayerLifecycleListener {
  protected items: LiveImage[] = [];

  // Only allow one player to play at a time for now
  protected currentlyPlaying: LiveImage | null = null;
  protected primaryPlayer: Player | null = null;

  private checkPlayerPositionTimeout: ReturnType<typeof setTimeout> | null =
    null;

  protected listeners: Record<
    string,
    Record<LiveImageManagerEvent, (event?: Record<string, any>) => void>
  > = {};

  constructor() {
    playerManager.addLifecycleListener(this);
    this.listeners = {};
  }

  private static _instance: LiveImageManager;

  public static get instance() {
    return this._instance || (this._instance = new this());
  }

  public register = (item: LiveImage): (() => void) => {
    this.items.push(item);
    log_debug(`register: live image ${playerInfo(item.player)}`);

    // TV only Start playing video once registered
    if (isTV()) {
      this.playLiveImage(item);
    }

    return () => this.unregister(item);
  };

  public unregister = (item: LiveImage) => {
    log_debug(`unregister: live-image ${playerInfo(item.player)}`);

    if (this.currentlyPlaying === item) {
      this.currentlyPlaying = null;

      log_debug(
        `unregister: currently playing live-image was destroyed, ${playerInfo(
          item.player
        )}`
      );

      // TODO: Maybe start another one
    }

    this.items = this.items.filter((i) => i !== item);

    this.invokeListenersUpdate({
      callbackName: "onDestroy",
      event: {
        item,
        playerId: this.currentlyPlaying?.playerId,
        primaryPlayerId: this.primaryPlayer?.playerId,
        entry: item.getPlayer().getEntry(),
      },
    });
  };

  private cancelCheckPlayerPositionTimeout = () => {
    clearTimeout(this.checkPlayerPositionTimeout);
    this.checkPlayerPositionTimeout = null;
  };

  public onViewportEnter = (item: LiveImage) => {
    log_debug(
      `onViewportEnter: live-image ${playerInfo(
        item.player
      )}, primary ${playerInfo(this.primaryPlayer)}`
    );

    if (!isTV()) {
      // mobile only
      // we have to delay running checkPlayerPosition, because sometimes on fast scrolling we get wrong order onEnter, then onLeave.
      // which could cause select wrong item to play

      this.cancelCheckPlayerPositionTimeout();

      this.checkPlayerPositionTimeout = setTimeout(() => {
        this.cancelCheckPlayerPositionTimeout();

        this.checkPlayerPosition(item);
      }, TIMEOUT_FOR_DELAY_CHECK_PLAYER_POSITION);
    } else {
      this.checkPlayerPosition(item);
    }
  };

  public onViewportLeave = (item: LiveImage) => {
    log_debug(
      `onViewportLeave: live-image playerId: ${playerInfo(
        item.player
      )}, primary ${playerInfo(this.primaryPlayer)}`
    );

    this.pauseItem(item);
  };

  public getCurrentlyPlaying = () => {
    return this.currentlyPlaying;
  };

  private findNextPlayableItem = () => {
    if (isTV()) {
      return this.items[this.items.length - 1];
    }

    return (
      this.items
        .filter(({ isFullyVisible }) => isFullyVisible)
        .sort((a: LiveImage, b: LiveImage) => {
          return a.position.centerX - b.position.centerX;
        })
        .sort((a: LiveImage, b: LiveImage) => {
          const a1 = Math.abs(a.position.centerY - 0.5);
          const b1 = Math.abs(b.position.centerY - 0.5);

          return a1 - b1;
        })?.[0] || this.currentlyPlaying
    );
  };

  private findItem = (playerId: string): LiveImage | null =>
    this.items.find((i) => i.playerId === playerId) || null;

  private pauseItem = (item: LiveImage) => {
    log_debug(`pauseItem: live-image ${playerInfo(item.player)}`);

    if (!item.player.playerState.isReadyToPlay) {
      log_debug(
        `playItem: live-image not ready, will start playback after loading, ${playerInfo(
          item.player
        )}`
      );
    } else {
      item.player?.pause();
    }

    // Fake close event, because we unmount native view
    item.player?.onPlayerClose();
    item.setMode?.(LiveImageType.Image);

    if (item === this.currentlyPlaying) {
      this.currentlyPlaying = null;
    }
  };

  public playLiveImage = (item: LiveImage) => {
    log_debug(
      `playLiveImage: live-image ${playerInfo(
        item.player
      )}, primary ${playerInfo(this.primaryPlayer)}`
    );

    if (this.primaryPlayer) {
      return;
    }

    if (this.currentlyPlaying) {
      if (this.currentlyPlaying?.player?.playerId === item.player.playerId) {
        return;
      } else {
        this.pauseItem(this.currentlyPlaying);
      }
    }

    this.currentlyPlaying = item;
    item.setMode?.(LiveImageType.Video);

    if (item.player.playerState.isReadyToPlay) {
      item.player.play();
    }
  };

  public pauseLiveImage = (item: LiveImage) => {
    log_debug(
      `pauseLiveImage: live-image playerId: ${playerInfo(
        item.player
      )}, primary ${playerInfo(this.primaryPlayer)}`
    );

    this.pauseItem(item);
  };

  public onLiveImageCompleted = (_item: LiveImage) => {
    // TODO: Notify listeners that player has died
    // Do not try look for new playable, otherwise you will restart video finish to play
  };

  public muteAll = () => {
    log_debug("muteAll");

    setUserCellPlayerMutedPreference(true);

    this.items.forEach((liveImage) => liveImage.player.mute());
  };

  public unmuteAll = () => {
    log_debug("unmuteAll");

    setUserCellPlayerMutedPreference(false);

    this.items.forEach((liveImage) => liveImage.player.unmute());
  };

  public checkPlayerPosition = (item: LiveImage) => {
    this.cancelCheckPlayerPositionTimeout();

    log_debug(
      `checkPlayerPosition: live-image playerId: ${playerInfo(
        item.player
      )}, primary ${playerInfo(this.primaryPlayer)}`
    );

    const playerItem = this.findNextPlayableItem();

    if (playerItem) {
      if (!playerItem.isFullyVisible) {
        log_error(
          `checkPlayerPosition: trying to start playback currently invisible item: ${playerInfo(
            playerItem.player
          )}`
        );
      }

      // Will also check if it's item that already playing
      this.playLiveImage(playerItem);
    }
  };

  onPrimaryPlayerClosed = () => {
    const playerItem = this.findNextPlayableItem();
    log_info("onPrimaryPlayerClosed: starting to play visible live-image");

    this.currentlyPlaying = null;

    if (playerItem) {
      this.playLiveImage(playerItem);
    }
  };

  onPrimaryPlayerCreated = () => {
    log_info("onPrimaryPlayerCreated: pausing visible live-images");

    if (this.currentlyPlaying) {
      this.pauseItem(this.currentlyPlaying);
    }
  };

  private liveImageManagerListenerId = "live-image-manager";

  onRegistered = (player: Player) => {
    const item = this.findItem(player.playerId);

    if (item) {
      return;
    }

    player.addListener({
      id: this.liveImageManagerListenerId,
      listener: {
        onVideoEnd: (event) => () => this.onPrimaryPlayerEnded(player, event),
        onError: (event) => () => this.onPrimaryPlayerError(player, event),
        onLoad: (event) => () => this.onPrimaryPlayerLoad(player, event),
      },
    });

    // Should not happen with current architecture
    if (!this.primaryPlayer) {
      this.primaryPlayer = player;

      this.onPrimaryPlayerCreated();
    } else {
      log_error(
        `onRegistered: multiple primary players not allowed, primary ${playerInfo(
          this.primaryPlayer
        )}`
      );
    }
  };

  onUnRegistered = (player: Player) => {
    const item = this.findItem(player.playerId);
    if (item) return;

    player.removeListener(this.liveImageManagerListenerId);

    if (player === this.primaryPlayer) {
      this.primaryPlayer = null;
      this.onPrimaryPlayerClosed();
    }
  };

  // Primary player callbacks
  onPrimaryPlayerEnded = (_player: Player, _event) => {
    // Not used now, primary player has been destroyed
  };

  onPrimaryPlayerError = (_player: Player, _event) => {
    // Not used now, primary player has been destroyed
  };

  onPrimaryPlayerLoad = (_player: Player, _event) => {
    // Not used now, we stop playing when primary player is registered
  };

  // Live Image player callbacks

  onLiveImageVideoLoaded = (item: LiveImage) => {
    log_debug(
      `onLiveImageVideoLoaded: live-image ${playerInfo(
        item.player
      )}, currentPlayingId: ${
        this.currentlyPlaying?.playerId
      }, primaryPlayerId: ${this.primaryPlayer?.playerId}`
    );

    if (this.currentlyPlaying === item) {
      item.player?.play();
    }

    this.invokeListenersUpdate({
      callbackName: "onStart",
      event: {
        item,
        playerId: this.currentlyPlaying?.playerId,
        primaryPlayerId: this.primaryPlayer?.playerId,
        entry: item.getPlayer().getEntry(),
      },
    });
  };

  onLiveImageEnded = (item: LiveImage) => {
    log_debug(
      `onLiveImageEnded: live-image ${playerInfo(
        item.player
      )}, currentPlayingId: ${
        this.currentlyPlaying?.playerId
      }, primaryPlayerId: ${this.primaryPlayer?.playerId}`
    );

    const isCurrentItemEnded = this.currentlyPlaying === item;

    // TODO: This branch was used when we had play button to toggle replay.
    //  Now we mute button instead but we keep it just in case.
    if (IS_ALLOWED_REPLAY && !isTV() && isCurrentItemEnded) {
      this.currentlyPlaying?.player.seekTo(0);
      this.currentlyPlaying?.player.pause();

      // TODO: ...Maybe player some other item
    } else {
      item.setMode?.(LiveImageType.Image);
    }

    // Prevent receiving onEnd event from both `close` and `end` player events
    isCurrentItemEnded &&
      this.invokeListenersUpdate({
        callbackName: "onEnd",
        event: {
          item,
          playerId: this.currentlyPlaying?.playerId,
          primaryPlayerId: this.primaryPlayer?.playerId,
          entry: item.getPlayer().getEntry(),
        },
      });
  };

  onLiveImageError = (item: LiveImage, error: Error) => {
    item.setMode?.(LiveImageType.Image);

    const currentItem = this.currentlyPlaying;

    log_debug(
      `onLiveImageError: error: ${error.message}, live-image ${playerInfo(
        item.player
      )}, currentPlayingId: ${
        this.currentlyPlaying?.playerId
      }, primaryPlayerId: ${this.primaryPlayer?.playerId}`
    );

    if (currentItem === item) {
      this.currentlyPlaying = null;

      log_debug(
        `onLiveImageError: currentitem: ${currentItem.playerId} was removed`
      );

      // TODO: ...Maybe player some other item
    }

    this.invokeListenersUpdate({
      callbackName: "onError",
      event: {
        item,
        error,
        playerId: currentItem?.playerId,
        primaryPlayerId: currentItem?.playerId,
        entry: item.getPlayer().getEntry(),
      },
    });
  };

  addListener = ({ id, listener }: LiveImageManagerListenerData) => {
    this.listeners[id] = listener;

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

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

  public invokeListenersUpdate = ({
    callbackName,
    event = {},
  }: {
    callbackName: LiveImageManagerEvent;
    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 }
        );
      }
    }
  };
}

// TODO: now we can check primary player exist and we can remove this hack
// then primary player was created and LiveImageManager doesn't know that primary player exist
// Happens when home screen has LiveImage and deep link open a player
// https://applicaster.monday.com/boards/1615228456/pulses/5979392200?notification=4136716156
LiveImageManager.instance;

export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
  public player: Player;
  public setMode: (type: LiveImageType) => void;
  // Will be replaced with rects
  public isFullyVisible: boolean = false;
  public position: Position = {
    centerX: 0,
    centerY: 0,
    top: 0,
    bottom: 0,
    right: 0,
    left: 0,
  };

  readonly playerId: string;
  readonly component: Component;

  constructor(props: LiveImageProps) {
    this.player = props.player;
    this.setMode = props.setMode;
    this.playerId = this.player.playerId;
    this.component = props.component;
    this.player.addListener({ id: "live-image", listener: this });
  }

  public getPlayer = (): Player => {
    return this.player;
  };

  onLoad = (_event) => {
    log_info(
      `onLoad: live-image, video loaded, switching to video, ${playerInfo(
        this.player
      )}`
    );

    LiveImageManager.instance.onLiveImageVideoLoaded(this);
  };

  onError = (error: Error) => {
    log_error(
      `onError: live-image, fail to play, switching back to image. error: ${
        error?.message
      }, ${playerInfo(this.player)}`
    );

    LiveImageManager.instance.onLiveImageError(this, error);
  };

  onVideoError = (error: Error) => {
    log_error(
      `onVideoError: live-image, fail to play, switching back to image. error: ${
        error?.message
      }, ${playerInfo(this.player)}`
    );

    LiveImageManager.instance.onLiveImageError(this, error);
  };

  onVideoEnd = (_event) => {
    log_info(
      `onVideoEnd: live-image, video completed, switching back to image, ${playerInfo(
        this.player
      )}`
    );

    LiveImageManager.instance.onLiveImageEnded(this);
  };

  onPlayerClose = () => {
    log_info(
      `onPlayerClose: live-image, player closed, switching back to image, ${playerInfo(
        this.player
      )}`
    );

    // TODO: we maybe want to add onPlayerClose to separate completion behavior
    LiveImageManager.instance.onLiveImageEnded(this);
  };
}
