import {
  requireNativeComponent,
  UIManager,
  findNodeHandle,
  Platform,
  type ViewProps
} from 'react-native';
import React, { Component } from 'react';

const ELEMENTS_NATIVE_COMPONENT = 'RCTElementsPlayer';
const ELEMENTS_LINKING_ERROR =
  `The package 'connatix-player-sdk-react-native' doesn't seem to be linked. Make sure: \n\n` +
  Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
  '- You rebuilt the app after installing the package\n' +
  '- You are not using Expo managed workflow\n';

type RCTElementsPlayerProps = ViewProps & {
  onSetElementsConfig: (event: any) => void
  onPlay: (event: any) => void
  onPause: (event: any) => void
  onSetVolume: (event: any) => void
  onSetQuality: (event: any) => void
  onGetQuality: (event: any) => void
  onDisableAdvertising: (event: any) => void
  onEnableAdvertising: (event: any) => void
  onSetMacros: (event: any) => void
  onSetPreRollBreak: (event: any) => void
  onSetPostRollBreak: (event: any) => void
  onAddFriendlyObstruction: (event: any) => void
  onRemoveFriendlyObstruction: (event: any) => void

  onGetVideoDetails: (event: any) => void
  onGetVideoCurrentPosition: (event: any) => void
  onGetVideoDuration: (event: any) => void
  onGetVideoIndex: (event: any) => void
  onSetVideoIndex: (event: any) => void
  onSetAutoQuality: (event: any) => void
  onGetAvailableQualities: (event: any) => void
  onToggleFullscreen: (event: any) => void
  onToggleSubtitles: (event: any) => void
  onGetSubtitles: (event: any) => void
  onSetSubtitle: (event: any) => void

  onListenFor: (event: any) => void
  onListenForMore: (event: any) => void
  onRemove: (event: any) => void
  onPlayerEvent: (event: any) => void
};

const RCTElementsPlayer =
  UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT) != null
    ? requireNativeComponent<RCTElementsPlayerProps>(ELEMENTS_NATIVE_COMPONENT)
    : () => {
        throw new Error(ELEMENTS_LINKING_ERROR);
      };

type ElementsPlayerState = {
  setElementsConfigCallback: ((errorMessage: string | undefined, success: Boolean | undefined) => void) | undefined
  playCallback: ((errorMessage: string) => void) | undefined
  pauseCallback: ((errorMessage: string) => void) | undefined
  setVolumeCallback: ((errorMessage: string) => void) | undefined
  setQualityCallback: ((errorMessage: string) => void) | undefined
  getQualityCallback: ((errorMessage: string | undefined, quality: number | undefined) => void) | undefined
  disableAdvertisingCallback: ((errorMessage: string) => void) | undefined
  enableAdvertisingCallback: ((errorMessage: string) => void) | undefined
  setMacrosCallback: ((errorMessage: string) => void) | undefined
  setPreRollBreakCallback: ((errorMessage: string) => void) | undefined
  setPostRollBreakCallback: ((errorMessage: string) => void) | undefined
  addFriendlyObstructionCallback: ((errorMessage: string) => void) | undefined
  removeFriendlyObstructionCallback: ((errorMessage: string) => void) | undefined

  getVideoDetailsCallback: ((errorMessage: string | undefined, videoDetails: Map<string, any> | undefined) => void) | undefined
  getVideoCurrentPositionCallback: ((errorMessage: string | undefined, position: number | undefined) => void) | undefined
  getVideoDurationCallback: ((errorMessage: string | undefined, duration: number | undefined) => void) | undefined
  getVideoIndexCallback: ((errorMessage: string | undefined, duration: number | undefined) => void) | undefined
  setVideoIndexCallback: ((errorMessage: string) => void) | undefined
  setAutoQualityCallback: ((errorMessage: string) => void) | undefined
  getAvailableQualitiesCallback: ((errorMessage: string | undefined, qualities: Array<number> | undefined) => void) | undefined
  toggleFullscreenCallback: ((errorMessage: string) => void) | undefined
  toggleSubtitlesCallback: ((errorMessage: string) => void) | undefined
  getSubtitlesCallback: ((errorMessage: string | undefined, tracks: Array<Map<string, any>> | undefined) => void) | undefined
  setSubtitleCallback: ((errorMessage: string) => void) | undefined

  listenForCallback: ((errorMessage: string) => void) | undefined
  listenForMoreCallback: ((errorMessage: string) => void) | undefined
  removeCallback: ((errorMessage: string) => void) | undefined
};

type ElementsPlayerProps = ViewProps & {
  onPlayerEvent?: ((event: any) => void) | undefined
};

class ElementsPlayer extends Component<ElementsPlayerProps, ElementsPlayerState>{
  constructor(props: ElementsPlayerProps) {
    super(props);
    this._handleNativeSetElementsConfigCallback = this._handleNativeSetElementsConfigCallback.bind(this);
    this._handleNativePlayCallback = this._handleNativePlayCallback.bind(this);
    this._handleNativePauseCallback = this._handleNativePauseCallback.bind(this);
    this._handleNativeSetVolumeCallback = this._handleNativeSetVolumeCallback.bind(this);
    this._handleNativeSetQualityCallback = this._handleNativeSetQualityCallback.bind(this);
    this._handleNativeGetQualityCallback = this._handleNativeGetQualityCallback.bind(this);
    this._handleNativeDisableAdvertisingCallback = this._handleNativeDisableAdvertisingCallback.bind(this);
    this._handleNativeEnableAdvertisingCallback = this._handleNativeEnableAdvertisingCallback.bind(this);
    this._handleNativeSetMacrosCallback = this._handleNativeSetMacrosCallback.bind(this);
    this._handleNativeSetPreRollBreakCallback = this._handleNativeSetPreRollBreakCallback.bind(this);
    this._handleNativeSetPostRollBreakCallback = this._handleNativeSetPostRollBreakCallback.bind(this);
    this._handleNativeAddFriendlyObstructionCallback = this._handleNativeAddFriendlyObstructionCallback.bind(this);
    this._handleNativeRemoveFriendlyObstructionCallback = this._handleNativeRemoveFriendlyObstructionCallback.bind(this);

    this._handleNativeGetVideoDetailsCallback = this._handleNativeGetVideoDetailsCallback.bind(this);
    this._handleNativeGetVideoCurrentPositionCallback = this._handleNativeGetVideoCurrentPositionCallback.bind(this);
    this._handleNativeGetVideoDurationCallback = this._handleNativeGetVideoDurationCallback.bind(this);
    this._handleNativeGetVideoIndexCallback = this._handleNativeGetVideoIndexCallback.bind(this);
    this._handleNativeSetVideoIndexCallback = this._handleNativeSetVideoIndexCallback.bind(this);
    this._handleNativeSetAutoQualityCallback = this._handleNativeSetAutoQualityCallback.bind(this);
    this._handleNativeGetAvailableQualitiesCallback = this._handleNativeGetAvailableQualitiesCallback.bind(this);
    this._handleNativeToggleFullscreenCallback = this._handleNativeToggleFullscreenCallback.bind(this);
    this._handleNativeToggleSubtitlesCallback = this._handleNativeToggleSubtitlesCallback.bind(this);
    this._handleNativeGetSubtitlesCallback = this._handleNativeGetSubtitlesCallback.bind(this);
    this._handleNativeSetSubtitleCallback = this._handleNativeSetSubtitleCallback.bind(this);

    this._handleNativeListenForCallback = this._handleNativeListenForCallback.bind(this);
    this._handleNativeListenForMoreCallback = this._handleNativeListenForMoreCallback.bind(this);
    this._handleNativeRemoveCallback = this._handleNativeRemoveCallback.bind(this);
    this._handleNativeOnPlayerEventCallback = this._handleNativeOnPlayerEventCallback.bind(this);
  }

  /**
   * Sets the configuration for the Elements player
   * @param config JSON string needed for the player to run
   * @param callback optional callback which returns true on success or error message on failure cases
   */
  setElementsConfig(config: string, callback?: ((errorMessage: string | undefined, success: Boolean | undefined) => void) | undefined) {
    this.setState({setElementsConfigCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.setElementsConfig,
      [config]
    );
  }

  private _handleNativeSetElementsConfigCallback(event: any) {
    if (this.state.setElementsConfigCallback === undefined) { 
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.setElementsConfigCallback(event.nativeEvent.error, undefined);
      this.setState({setElementsConfigCallback: undefined});
      return;
    }
    if (event.nativeEvent.result !== undefined) {
      this.state.setElementsConfigCallback(undefined, event.nativeEvent.result);
      this.setState({setElementsConfigCallback: undefined});
    }
  }

  /**
   * Stops every function of the player, including the ad session. The player will need to be restarted after this
   */
  stopPlayer() {
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.stopPlayer);
  }

  /**
   * Sets the player state to "play"
   * @param callback optional callback for failure cases
   */
  play(callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({playCallback: callback})
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.play);
  }

  private _handleNativePlayCallback(event: any) {
    if (this.state.playCallback === undefined) { 
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.playCallback(event.nativeEvent.error);
      this.setState({playCallback: undefined});
    }
  }

  /**
   * Sets the player state to "pause"
   * @param callback optional callback for failure cases
   */
  pause(callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({pauseCallback: callback});
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.pause);
  }

  private _handleNativePauseCallback(event: any) {
    if (this.state.pauseCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.pauseCallback(event.nativeEvent.error);
      this.setState({pauseCallback: undefined});
    }
  }

  /**
   * Sets the player volume to the specified value
   * @param volume float between 0 and 1
   * @param callback optional callback for failure cases
   */
  setVolume(volume: number, callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({setVolumeCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.setVolume,
      [volume]
    );
  }

  private _handleNativeSetVolumeCallback(event: any) {
    if (this.state.setVolumeCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.setVolumeCallback(event.nativeEvent.error);
      this.setState({setVolumeCallback: undefined});
    }
  }

  /**
   * Sets the resolution of the video to the specified quality
   * @param quality int, only 360, 720 and 1080 supported
   * @param callback optional callback for failure cases
   */
  setQuality(quality: number, callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({setQualityCallback: callback})
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.setQuality,
      [quality]
    );
  }

  private _handleNativeSetQualityCallback(event: any) {
    if (this.state.setQualityCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.setQualityCallback(event.nativeEvent.error);
      this.setState({setQualityCallback: undefined})
    }
  }

  /**
   * Returns the current resolution of the video
   * @param callback callback for the Int quality on success or the error message on failure
   */
  getQuality(callback: ((errorMessage: string | undefined, quality: number | undefined) => void) | undefined) {
    this.setState({getQualityCallback: callback});
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.getQuality);
  }

  private _handleNativeGetQualityCallback(event: any) {
    if (this.state.getQualityCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.getQualityCallback(event.nativeEvent.error, undefined);
      this.setState({getQualityCallback: undefined});
      return;
    }
    if (event.nativeEvent.result !== undefined) {
      this.state.getQualityCallback(undefined, event.nativeEvent.result);
      this.setState({getQualityCallback: undefined});
    }
  }

  /**
   * Pauses player advertising, note that a running ad will finish if this method is called
   * @param callback optional callback for failure cases
   */
  disableAdvertising(callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({disableAdvertisingCallback: callback});
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.disableAdvertising);
  }

  private _handleNativeDisableAdvertisingCallback(event: any) {
    if (this.state.disableAdvertisingCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.disableAdvertisingCallback(event.nativeEvent.error);
      this.setState({disableAdvertisingCallback: undefined});
    }
  }

  /**
   * If advertising was previously disabled by the 'disableAdvertising' call, it will enable it back
   * @param callback optional callback for failure cases
   */
  enableAdvertising(callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({enableAdvertisingCallback: callback});
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.enableAdvertising);
  }

  private _handleNativeEnableAdvertisingCallback(event: any) {
    if (this.state.enableAdvertisingCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.enableAdvertisingCallback(event.nativeEvent.error);
      this.setState({enableAdvertisingCallback: undefined});
    }
  }

  /**
   * Sets custom macros. The method uses key value pairs of macros { key: value }. Please note that Connatix Macros will NOT we overridden
   * @param macrosJsonString representing key value pairs of macros to be substituted in lineItems URLs before calling for VAST Ads. If any of the macro keys match the "Connatix macros" (ex: height, page_url), the values provided programmatically by the user will NOT override the values detected by the player, only for the externally loaded line items
   * @param callback optional callback for failure cases
   */
  setMacros(macrosJsonString: string, callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({setMacrosCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.setMacros,
      [macrosJsonString]
    );
  }

  private _handleNativeSetMacrosCallback(event: any) {
    if (this.state.setMacrosCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.setMacrosCallback(event.nativeEvent.error);
      this.setState({setMacrosCallback: undefined});
    }
  }

  /**
   * Dynamically set the pre-roll break duration. This method needs to be called with the duration in seconds. To disable the break call this with 0 value
   * @param seconds number of seconds for the pre-roll break
   * @param callback optional callback for failure cases
   */
  setPreRollBreak(seconds: number, callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({setPreRollBreakCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.setPreRollBreak,
      [seconds]
    );
  }

  private _handleNativeSetPreRollBreakCallback(event: any) {
    if (this.state.setPreRollBreakCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.setPreRollBreakCallback(event.nativeEvent.error);
      this.setState({setPreRollBreakCallback: undefined});
    }
  }

  /**
   * Dynamically set the post-roll break duration. This method needs to be called with the duration in seconds. To disable the break call this with 0 value
   * @param seconds number of seconds for the post-roll break
   * @param callback optional callback for failure cases
   */
  setPostRollBreak(seconds: number, callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({setPostRollBreakCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.setPostRollBreak,
      [seconds]
    );
  }

  private _handleNativeSetPostRollBreakCallback(event: any) {
    if (this.state.setPostRollBreakCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.setPostRollBreakCallback(event.nativeEvent.error);
      this.setState({setPostRollBreakCallback: undefined});
    }
  }

  /**
   * Adds friendly obstruction which should then be excluded from all ad session viewability calculations.
   * It also provides a purpose and detailed reason string to pass forward to the measurement vendors.
   * This method will have no effect if called after the ad session has finished.
   * @param obstructionReactTag the react tag of the view to be excluded from all ad session viewability calculations
   * @param purpose the purpose of why this obstruction was necessary. Possible values: "videoControls", "closeAd", "notVisible", "other"
   * @param detailedReason an explanation for why this obstruction is part of the ad experience if not already obvious from the purpose. Can be null. If not null, must be 50 characters or less and only contain characters `A-z`, `0-9`, or spaces
   * @param callback optional callback for failure cases
   */
  addFriendlyObstruction(obstructionReactTag: number, purpose: string, detailedReason: string, callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({addFriendlyObstructionCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.addFriendlyObstruction,
      [obstructionReactTag, purpose, detailedReason]
    );
  }

  private _handleNativeAddFriendlyObstructionCallback(event: any) {
    if (this.state.addFriendlyObstructionCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.addFriendlyObstructionCallback(event.nativeEvent.error);
      this.setState({addFriendlyObstructionCallback: undefined});
    }
  }

  /**
   * Removes registered friendly obstruction. This method will have no effect if called after the ad session has finished
   * @param obstructionReactTag the react tag of the view to be removed from the list of registered friendly obstructions
   * @param callback optional callback for failure cases
   */
  removeFriendlyObstruction(obstructionReactTag: number, callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({removeFriendlyObstructionCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.removeFriendlyObstruction,
      [obstructionReactTag]
    );
  }

  private _handleNativeRemoveFriendlyObstructionCallback(event: any) {
    if (this.state.removeFriendlyObstructionCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.removeFriendlyObstructionCallback(event.nativeEvent.error);
      this.setState({removeFriendlyObstructionCallback: undefined});
    }
  }

  /**
   * Returns the details of the currently playing video
   * @param callback callback for the video details on success or the error message on failure
   */
  getVideoDetails(callback: ((errorMessage: string | undefined, videoDetails: Map<string, any> | undefined) => void) | undefined) {
    this.setState({getVideoDetailsCallback: callback});
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.getVideoDetails);
  }

  private _handleNativeGetVideoDetailsCallback(event: any) {
    if (this.state.getVideoDetailsCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.getVideoDetailsCallback(event.nativeEvent.error, undefined);
      this.setState({getVideoDetailsCallback: undefined});
      return;
    }
    if (event.nativeEvent.result !== undefined) {
      this.state.getVideoDetailsCallback(undefined, event.nativeEvent.result);
      this.setState({getVideoDetailsCallback: undefined});
    }
  }

  /**
   * Returns the timestamp of the currently playing video in seconds
   * @param callback callback for the video current position on success or the error message on failure
   */
  getVideoCurrentPosition(callback: ((errorMessage: string | undefined, position: number | undefined) => void) | undefined) {
    this.setState({getVideoCurrentPositionCallback: callback});
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.getVideoCurrentPosition);
  }

  private _handleNativeGetVideoCurrentPositionCallback(event: any) {
    if (this.state.getVideoCurrentPositionCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.getVideoCurrentPositionCallback(event.nativeEvent.error, undefined);
      this.setState({getVideoCurrentPositionCallback: undefined});
      return;
    }
    if (event.nativeEvent.result !== undefined) {
      this.state.getVideoCurrentPositionCallback(undefined, event.nativeEvent.result);
      this.setState({getVideoCurrentPositionCallback: undefined});
    }
  }

  /**
   * Returns the duration of the currently playing video in seconds
   * @param callback callback for the video duration on success or the error message on failure
   */
  getVideoDuration(callback: ((errorMessage: string | undefined, duration: number | undefined) => void) | undefined) {
    this.setState({getVideoDurationCallback: callback});
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.getVideoDuration);
  }

  private _handleNativeGetVideoDurationCallback(event: any) {
    if (this.state.getVideoDurationCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.getVideoDurationCallback(event.nativeEvent.error, undefined);
      this.setState({getVideoDurationCallback: undefined});
      return;
    }
    if (event.nativeEvent.result !== undefined) {
      this.state.getVideoDurationCallback(undefined, event.nativeEvent.result);
      this.setState({getVideoDurationCallback: undefined});
    }
  }

  /**
   * Returns the index of the video currently playing from the current playlist. If the playlist only has 1 video, the method returns the numeric value 0
   * @param callback callback for the video index on success or the error message on failure
   */
  getVideoIndex(callback: ((errorMessage: string | undefined, index: number | undefined) => void | undefined)) {
    this.setState({getVideoIndexCallback: callback});
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.getVideoIndex);
  }

  private _handleNativeGetVideoIndexCallback(event: any) {
    if (this.state.getVideoIndexCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.getVideoIndexCallback(event.nativeEvent.error, undefined);
      this.setState({getVideoIndexCallback: undefined});
      return;
    }
    if (event.nativeEvent.result !== undefined) {
      this.state.getVideoIndexCallback(undefined, event.nativeEvent.result);
      this.setState({getVideoIndexCallback: undefined});
    }
  }

  /**
   * Sets the playing video with the one found in the playlist at the index provided. The index provided should be a numeric value between 0 and the total number of videos in the current playlist - 1. If the index provided is less than 0 or greater or equal with the total number of videos, the method shows an error message and returns
   * @param index The index of the desired video in the playlist
   * @param callback Optional callback for failure cases
   */
  setVideoIndex(index: number, callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({setVideoIndexCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.setVideoIndex,
      [index]
    );
  }

  private _handleNativeSetVideoIndexCallback(event: any) {
    if (this.state.setVideoIndexCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.setVideoIndexCallback(event.nativeEvent.error);
      this.setState({setVideoIndexCallback: undefined});
    }
  }

  /**
   * Sets the video quality to Auto. Auto quality will be determined by the height of the video element
   * @param callback optional callback for failure cases
   */
  setAutoQuality(callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({setAutoQualityCallback: callback});
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.setAutoQuality);
  }

  private _handleNativeSetAutoQualityCallback(event: any) {
    if (this.state.setAutoQualityCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.setAutoQualityCallback(event.nativeEvent.error);
      this.setState({setAutoQualityCallback: undefined});
    }
  }

  /**
   * Returns an array with the available resolutions of the currently playing video
   * @param callback callback for the available qualities array on success or the error message on failure
   */
  getAvailableQualities(callback: ((errorMessage: string | undefined, qualities: Array<number> | undefined) => void) | undefined) {
    this.setState({getAvailableQualitiesCallback: callback});
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.getAvailableQualities);
  }

  private _handleNativeGetAvailableQualitiesCallback(event: any) {
    if (this.state.getAvailableQualitiesCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.getAvailableQualitiesCallback(event.nativeEvent.error, undefined);
      this.setState({getAvailableQualitiesCallback: undefined});
      return;
    }
    if (event.nativeEvent.result !== undefined) {
      this.state.getAvailableQualitiesCallback(undefined, event.nativeEvent.result);
      this.setState({getAvailableQualitiesCallback: undefined});
    }
  }

  /**
   * Toggles the player full screen state. Only use this method on user interaction
   * @param callback optional callback for failure cases
   */
  toggleFullscreen(callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({toggleFullscreenCallback: callback});
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.toggleFullscreen);
  }

  private _handleNativeToggleFullscreenCallback(event: any) {
    if (this.state.toggleFullscreenCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.toggleFullscreenCallback(event.nativeEvent.error);
      this.setState({toggleFullscreenCallback: undefined});
    }
  }

  /**
   * Shows or hides the video subtitles based on the given boolean. Value is necessary
   * @param shouldShow boolean indicating whether to show subtitles or not
   * @param callback optional callback for failure cases
   */
  toggleSubtitles(shouldShow: boolean, callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({toggleSubtitlesCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.toggleSubtitles,
      [shouldShow]
    );
  }

  private _handleNativeToggleSubtitlesCallback(event: any) {
    if (this.state.toggleSubtitlesCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.toggleSubtitlesCallback(event.nativeEvent.error);
      this.setState({toggleSubtitlesCallback: undefined});
    }
  }

  /**
   * Returns the array of available subtitles for the current media
   * @param callback callback for the subtitles array on success or the error message on failure
   */
  getSubtitles(callback: ((errorMessage: string | undefined, tracks: Array<Map<string, any>> | undefined) => void) | undefined) {
    this.setState({getSubtitlesCallback: callback});
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.getSubtitles);
  }

  private _handleNativeGetSubtitlesCallback(event: any) {
    if (this.state.getSubtitlesCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.getSubtitlesCallback(event.nativeEvent.error, undefined);
      this.setState({getSubtitlesCallback: undefined});
      return;
    }
    if (event.nativeEvent.result !== undefined) {
      this.state.getSubtitlesCallback(undefined, event.nativeEvent.result);
      this.setState({getSubtitlesCallback: undefined});
    }
  }

  /**
   * Sets a subtitle from the array obtained from 'getSubtitles'. If no value is given or the subtitle is invalid (not found in the array), subtitles will be turned off
   * @param trackJsonString obtained from 'getSubtitles'
   * @param callback optional callback for failure cases
   */
  setSubtitle(trackJsonString: string, callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({setSubtitleCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.setSubtitle,
      [trackJsonString]
    );
  }

  private _handleNativeSetSubtitleCallback(event: any) {
    if (this.state.setSubtitleCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.setSubtitleCallback(event.nativeEvent.error);
      this.setState({setSubtitleCallback: undefined});
    }
  }

  /**
   * Listens to a single player event. Can be done once or as long as the player is alive
   * @param event the string event type to listen to
   * @param once boolean indicating whether the event is listened to once or indefinitely
   * @param callback optional callback for failure cases
   */
  listenFor(event: string, once: boolean, callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({listenForCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.listenFor,
      [event, once]
    );
  }

  private _handleNativeListenForCallback(event: any) {
    if (this.state.listenForCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.listenForCallback(event.nativeEvent.error);
      this.setState({listenForCallback: undefined});
    }
  }

  /**
   * Listens to a string array of player event
   * @param events Array of string to listen to indefinitely
   * @param callback optional callback for failure cases
   */
  listenForMore(events: string[], callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({listenForMoreCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.listenForMore,
      [events]
    );
  }

  private _handleNativeListenForMoreCallback(event: any) {
    if (this.state.listenForMoreCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.listenForMoreCallback(event.nativeEvent.error);
      this.setState({listenForCallback: undefined});
    }
  }

  /**
   * Listens to all available player events. Please check the documentation for all the possible event types
   */
  listenForAllEvents() {
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.listenForAllEvents);
  }

  /**
   * Removes the listener for the specified player string event type
   * @param event stop listening for this string event
   * @param callback optional callback for failure cases
   */
  remove(event: string, callback?: ((errorMessage: string) => void) | undefined) {
    this.setState({removeCallback: callback});
    this._dispatchCommandWithArguments(
      UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.remove,
      [event]
    );
  }

  private _handleNativeRemoveCallback(event: any) {
    if (this.state.removeCallback === undefined) {
      return;
    }
    if (event.nativeEvent.error !== undefined) {
      this.state.removeCallback(event.nativeEvent.error);
      this.setState({removeCallback: undefined});
    }
  }

  /**
   * Removes the listeners for all player events
   */
  removeAllEvents() {
    this._dispatchCommand(UIManager.getViewManagerConfig(ELEMENTS_NATIVE_COMPONENT).Commands.removeAllEvents);
  }

  private _handleNativeOnPlayerEventCallback(event: any) {
    if (!this.props.onPlayerEvent) {
      return;
    }
    this.props.onPlayerEvent(event.nativeEvent);
  }

  private _dispatchCommand(command: number | undefined) {
    if (command !== undefined) {
      UIManager?.dispatchViewManagerCommand(
        findNodeHandle(this),
        command,
        []
      );
    }
  }

  private _dispatchCommandWithArguments(command: number | undefined, args: any[]) {
    if (command !== undefined) {
      UIManager?.dispatchViewManagerCommand(
        findNodeHandle(this),
        command,
        args
      );
    }
  }

  render() {
    return (
      <RCTElementsPlayer {...this.props}
      onSetElementsConfig={this._handleNativeSetElementsConfigCallback}
      onPlay={this._handleNativePlayCallback}
      onPause={this._handleNativePauseCallback}
      onSetVolume={this._handleNativeSetVolumeCallback}
      onSetQuality={this._handleNativeSetQualityCallback}
      onGetQuality={this._handleNativeGetQualityCallback}
      onDisableAdvertising={this._handleNativeDisableAdvertisingCallback}
      onEnableAdvertising={this._handleNativeEnableAdvertisingCallback}
      onSetMacros={this._handleNativeSetMacrosCallback}
      onSetPreRollBreak={this._handleNativeSetPreRollBreakCallback}
      onSetPostRollBreak={this._handleNativeSetPostRollBreakCallback}
      onAddFriendlyObstruction={this._handleNativeAddFriendlyObstructionCallback}
      onRemoveFriendlyObstruction={this._handleNativeRemoveFriendlyObstructionCallback}
      onGetVideoDetails={this._handleNativeGetVideoDetailsCallback}
      onGetVideoCurrentPosition={this._handleNativeGetVideoCurrentPositionCallback}
      onGetVideoDuration={this._handleNativeGetVideoDurationCallback}
      onGetVideoIndex={this._handleNativeGetVideoIndexCallback}
      onSetVideoIndex={this._handleNativeSetVideoIndexCallback}
      onSetAutoQuality={this._handleNativeSetAutoQualityCallback}
      onGetAvailableQualities={this._handleNativeGetAvailableQualitiesCallback}
      onToggleFullscreen={this._handleNativeToggleFullscreenCallback}
      onToggleSubtitles={this._handleNativeToggleSubtitlesCallback}
      onGetSubtitles={this._handleNativeGetSubtitlesCallback}
      onSetSubtitle={this._handleNativeSetSubtitleCallback}
      onListenFor={this._handleNativeListenForCallback}
      onListenForMore={this._handleNativeListenForMoreCallback}
      onRemove={this._handleNativeRemoveCallback}
      onPlayerEvent={this._handleNativeOnPlayerEventCallback}
      />
    );
  }
} 

export default ElementsPlayer;
