package com.castlabs.reactnative.player;

import android.app.Activity;
import android.app.PictureInPictureParams;
import android.graphics.Rect;
import android.os.Build;
import android.util.Rational;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.castlabs.android.drm.Drm;
import com.castlabs.android.player.AbstractPlayerListener;
import com.castlabs.android.player.VideoFilterConfiguration;
import com.castlabs.reactnative.errors.ErrorSeverity;
import com.castlabs.reactnative.utils.DrmUtils;
import com.castlabs.android.player.PlayerConfig;
import com.castlabs.android.player.PlayerController;
import com.castlabs.android.player.TrickplayConfiguration;
import com.castlabs.android.player.models.AudioTrack;
import com.castlabs.android.player.models.SubtitleTrack;
import com.castlabs.android.player.models.VideoTrack;
import com.castlabs.android.player.models.VideoTrackQuality;
import com.castlabs.reactnative.sdk.Plugin;
import com.castlabs.reactnative.errors.PrestoPlayError;
import com.castlabs.reactnative.network.DeferredRequestModifier;
import com.castlabs.reactnative.network.DeferredResponseModifier;
import com.castlabs.reactnative.network.RequestCompleter;
import com.castlabs.reactnative.network.ResponseCompleter;
import com.castlabs.reactnative.playerview.PlayerView;
import com.castlabs.reactnative.utils.BridgeDeserializer;
import com.castlabs.reactnative.utils.JsonMap;
import com.facebook.react.bridge.ReactContext;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * The player class.
 */
@RequiresApi(api = Build.VERSION_CODES.N)
public class Player implements VideoSizeChangedExtension.VideoSizeListener {
  private final @NonNull String instanceId;
  private final @NonNull Map<String, PlayerView> playerViewInstanceManager;
  private final @NonNull List<Plugin> pluginInstanceManager;
  private final @NonNull Map<String, RequestCompleter> requestCompleterCache;
  private final @NonNull Map<String, ResponseCompleter> responseCompleterCache;
  private final @NonNull PlayerEventEmitter playerEventEmitter;

  private final @NonNull PlayerController playerController;
  private final @NonNull VideoSizeChangedExtension videoSizeChangedExtension;
  private final @NonNull ExoPlayerProxy exoPlayerProxy;
  private final @NonNull PlaybackStatsMonitor playbackStatsMonitor;
  private final @NonNull LifecycleProxy lifecycleProxy;

  private final @NonNull DeferredRequestModifier deferredRequestModifier;
  private final @NonNull DeferredResponseModifier deferredResponseModifier;
  private final @NonNull EmittableTrackSelectionListener emittableTrackSelectionListener;
  private final @NonNull EmittablePlayerListener emittablePlayerListener;
  private final @NonNull EmittableExoPlayerListener emittableExoPlayerListener;
  private final @NonNull EmittablePictureInPictureListener emittablePictureInPictureListener;
  private final @NonNull EmittablePlaybackStatsListener emittablePlaybackStatsListener;
  private AbstractPlayerListener playerListenerForVideoFilter = null;

  private @NonNull List<PlayerExtension> extensions = new ArrayList<>();

  private @Nullable PlayerConfig playerConfig;

  public @Nullable String playerViewInstanceId;
  private float volume;

  /**
   * Creates a player instance without configuration.
   *
   * @param reactContext The application context
   * @param instanceId The player UUID
   * @param playerViewInstanceManager The global player view instance manager
   * @param requestCompleterCache The global request completer cache
   * @param responseCompleterCache The global response completer cache
   */
  public Player(
      @NonNull ReactContext reactContext,
      @NonNull String instanceId,
      @NonNull Map<String, PlayerView> playerViewInstanceManager,
      @NonNull List<Plugin> pluginInstanceManager,
      @NonNull Map<String, RequestCompleter> requestCompleterCache,
      @NonNull Map<String, ResponseCompleter> responseCompleterCache,
      @NonNull PlayerEventEmitter playerEventEmitter
  ) {
    this.instanceId = instanceId;
    this.playerViewInstanceManager = playerViewInstanceManager;
    this.pluginInstanceManager = pluginInstanceManager;
    this.requestCompleterCache = requestCompleterCache;
    this.responseCompleterCache = responseCompleterCache;
    this.playerEventEmitter = playerEventEmitter;

    playerController = new PlayerController(reactContext);
    exoPlayerProxy = new ExoPlayerProxy(playerController);
    playbackStatsMonitor = new PlaybackStatsMonitor(playerController);
    lifecycleProxy = new LifecycleProxy(reactContext);
    videoSizeChangedExtension = new VideoSizeChangedExtension(playerController);

    emittableTrackSelectionListener = new EmittableTrackSelectionListener(
        instanceId,
        playerController,
        playerEventEmitter
    );

    emittablePlayerListener = new EmittablePlayerListener(
        instanceId,
        playerController,
        playerEventEmitter
    );

    emittableExoPlayerListener = new EmittableExoPlayerListener(
        instanceId,
        playerEventEmitter
    );

    emittablePictureInPictureListener = new EmittablePictureInPictureListener(
        instanceId,
        playerEventEmitter
    );

    emittablePlaybackStatsListener = new EmittablePlaybackStatsListener(
      instanceId,
      playerEventEmitter
    );

    deferredRequestModifier = new DeferredRequestModifier(
        playerEventEmitter,
        requestCompleterCache,
        instanceId
    );

    deferredResponseModifier = new DeferredResponseModifier(
        playerEventEmitter,
        responseCompleterCache,
        instanceId
    );

    registerListeners();

    volume = playerController.getVolume();
    playerEventEmitter.emitVolumeChangedEvent(instanceId, volume);

    pluginInstanceManager.forEach(plugin -> plugin.onPlayerCreated(this));
  }

  private void registerListeners() {
    playerController.addPlayerListener(emittablePlayerListener);
    playerController.addTrackSelectionListener(emittableTrackSelectionListener);
    exoPlayerProxy.addListener(emittableExoPlayerListener);
    lifecycleProxy.addListener(emittablePictureInPictureListener);
    playbackStatsMonitor.addListener(emittablePlaybackStatsListener);
    videoSizeChangedExtension.addListener(this);
    playerController.addRequestModifier(deferredRequestModifier);
    playerController.addResponseModifier(deferredResponseModifier);
    playerController.addTimelineChangedListener(emittablePlayerListener);
  }

  private void unregisterListeners() {
    playerController.removePlayerListener(emittablePlayerListener);
    playerController.removeTrackSelectionListener(emittableTrackSelectionListener);
    exoPlayerProxy.removeListener(emittableExoPlayerListener);
    lifecycleProxy.removeListener(emittablePictureInPictureListener);
    playbackStatsMonitor.removeListener(emittablePlaybackStatsListener);
    videoSizeChangedExtension.removeListener(this);
    playerController.removeRequestModifier(deferredRequestModifier);
    playerController.removeResponseModifier(deferredResponseModifier);
    playerController.removeTimelineChangedListener(emittablePlayerListener);
    if (playerListenerForVideoFilter != null) {
      playerController.removePlayerListener(playerListenerForVideoFilter);
    }
  }

  /**
   * Starts playback for the given configuration.
   *
   * @param jsonPlayerConfiguration The player configuration of the React Native SDK
   * @throws PrestoPlayError Invalid configuration
   */
  public void open(
      final @NonNull JsonMap jsonPlayerConfiguration
  ) throws PrestoPlayError {
    extensions.forEach(
        extension -> extension.onContentWillLoad(
            jsonPlayerConfiguration
        )
    );

    // To load a new stream, the player must first be stopped.
    // Otherwise, playerController.open will have no effect if a stream is already active.
    release();

    Optional<JsonMap> jsonDrmConfiguration = jsonPlayerConfiguration.getMap("drm");
    if (jsonDrmConfiguration.isPresent()) {
      Drm selectedDrm = DrmUtils.selectDrm(jsonDrmConfiguration.get());
      playerEventEmitter.emitDrmChanged(instanceId, selectedDrm);
    }

    final PlayerConfig playerConfig = BridgeDeserializer.toPlayerConfig(
        jsonPlayerConfiguration
    );

    // Create a copy of PlayerConfig to safeguard against potential changes over time.
    // For instance, PlayerConfig.positionUs is continuously updated as the media playhead advances.
    // TODO create Android SDK bug report
    this.playerConfig = new PlayerConfig.Builder(playerConfig).get();

    playerController.setSecondaryDisplay(
      BridgeDeserializer.toSecondaryDisplay(jsonPlayerConfiguration)
    );

    playerController.open(playerConfig);

    extensions.forEach(
        extension -> extension.onContentLoaded(
            jsonPlayerConfiguration
        )
    );
  }

  public void play() {
    playerController.play();
  }

  public void replay() {
    if (
      playerController.isLive() ||
      playerController.getPlayerState() != PlayerController.State.Finished ||
      playerConfig == null
    ) {
      return;
    }

    // Default is 0 if the PlayerConfiguration.startTimeMs wasn't defined
    final long startTimeUs = playerConfig.positionUs;
    playerController.setPosition(startTimeUs);
    playerController.play();
  }

  public void pause() {
    playerController.pause();
  }

  public void stop() {

    // HDPRN-89, RN-506:
    //
    // When a fatal error is thrown the player is in the Idle state.
    // The JavaScript player clears any fatal errors when it enters the Stopping state.
    //
    // The Android player does not natively have a "Stopping" state.
    // To align the behavior with the iOS implementation,
    // we manually trigger the Stopping state when Player.stop() is explicitly called.
    // This state should represent a direct user action and not be the result of internal state changes.
    // Once the stop is complete, the player should transition to the Idle state.
    playerEventEmitter.emitStateChangedEvent(instanceId, "Stopping");
    playerEventEmitter.emitStateChangedEvent(instanceId, "Idle");

    release();
  }

  /**
   * Moves the playhead (seeks) to specified time position.
   *
   * @param newPositionMs the new playhead in milliseconds
   */
  public void setPosition(long newPositionMs) {
    playerController.setPosition(TimeUnit.MILLISECONDS.toMicros(newPositionMs));
  }

  /**
   * Sets the volume to specified level.
   *
   * @param newPlaybackRate the new speed at which the video is being played
   */
  public void setPlaybackRate(float newPlaybackRate) {
    if (newPlaybackRate != 1.0) {
      TrickplayConfiguration.Builder builder = new TrickplayConfiguration.Builder();
      builder.speed(newPlaybackRate);
      builder.keepAudioEnabled(true);
      playerController.setTrickplayConfiguration(builder.get());
      playerController.enableTrickplayMode(true);
    } else {
      playerController.enableTrickplayMode(false);
    }

    playerEventEmitter.emitPlaybackRateChangedEvent(instanceId, playerController.getSpeed());
  }

  /**
   * Sets the volume to specified level.
   *
   * @param newVolume the volume level between 0.0 and 1.0
   */
  public void setVolume(float newVolume) {
    volume = newVolume;
    playerController.setVolume(newVolume);
    playerEventEmitter.emitVolumeChangedEvent(instanceId, newVolume);
  }

  /**
   * Mutes or unmutes the player.
   *
   * @param newMuted true if playback should be muted; false otherwise
   */
  public void setMuted(boolean newMuted) {
    if (newMuted) {
      playerController.setVolume(0);
    } else {
      playerController.setVolume(volume);
    }
    playerEventEmitter.emitMutedChangedEvent(instanceId, newMuted);
  }

  private void release() {
    onPlayerWillRelease();

    synchronized (this) {
      playerController.release();
    }

    playerConfig = null;

    // RN-503: Native Player doesn't preserve listeners on Android
    exoPlayerProxy.removeListener(emittableExoPlayerListener);
    exoPlayerProxy.addListener(emittableExoPlayerListener);
  }

  /**
   * Unregisters and destroys the player instance.
   */
  public void destroy() {
    playerConfig = null;

    extensions.forEach(PlayerExtension::onPlayerWillDestroy);
    pluginInstanceManager.forEach(plugin -> plugin.onPlayerWillDestroy(this));

    extensions = new ArrayList<>();

    unregisterListeners();

    exoPlayerProxy.dispose();
    playbackStatsMonitor.dispose();
    videoSizeChangedExtension.dispose();

    // Remove all pending requests after player is destroyed
    synchronized (requestCompleterCache) {
      final Iterator<Map.Entry<String, RequestCompleter>> iterator = requestCompleterCache.entrySet().iterator();
      while (iterator.hasNext()) {
        final Map.Entry<String, RequestCompleter> entry = iterator.next();
        final RequestCompleter requestCompleter = entry.getValue();
        if (requestCompleter != null && instanceId.equals(requestCompleter.playerId)) {
          iterator.remove();
        }
      }
    }

    // Remove all pending responses after player is destroyed
    synchronized (responseCompleterCache) {
      final Iterator<Map.Entry<String, ResponseCompleter>> iterator = responseCompleterCache.entrySet().iterator();
      while (iterator.hasNext()) {
        final Map.Entry<String, ResponseCompleter> entry = iterator.next();
        final ResponseCompleter responseCompleter = entry.getValue();
        if (responseCompleter != null && instanceId.equals(responseCompleter.playerId)) {
          iterator.remove();
        }
      }
    }

    synchronized (this) {
      playerController.destroy();
    }
  }

  public void setAudioTrack(@NonNull String trackIdx) {
    AudioTrack newAudioTrack = playerController.getAudioTracks().get(Integer.parseInt(trackIdx));
    playerController.setAudioTrack(newAudioTrack);
  }

  public void setVideoTrack(@NonNull String trackIdx) {
    VideoTrack newVideoTrack = playerController.getVideoTracks().get(Integer.parseInt(trackIdx));
    playerController.setVideoTrack(newVideoTrack);
  }

  /**
   * Selects the video rendition by ID.
   *
   * @param trackIdxAndRenditionIdx rendition ID
   */
  public void setVideoRendition(@NonNull String trackIdxAndRenditionIdx) {
    int trackIdx = Integer.parseInt(trackIdxAndRenditionIdx.split("-")[0]);
    int qualityIdx = Integer.parseInt(trackIdxAndRenditionIdx.split("-")[1]);
    VideoTrack newVideoTrack = playerController.getVideoTracks().get(trackIdx);
    VideoTrackQuality newVideoTrackQuality = newVideoTrack.getQualities().get(qualityIdx);
    playerController.setVideoTrack(
        newVideoTrack,
        newVideoTrackQuality
    );
  }

  /**
   * Selects the text track by ID.
   *
   * @param trackIdx text ID
   */
  public void setTextTrack(@Nullable String trackIdx) {
    if (trackIdx == null) {
      // disable text overlay
      playerController.setSubtitleTrack(null);
      return;
    }

    SubtitleTrack newSubtitleTrack = playerController.getSubtitleTracks().get(Integer.parseInt(trackIdx));
    playerController.setSubtitleTrack(newSubtitleTrack);
  }

  /**
   * Enables the ABR mode.
   */
  public void enableAdaptiveVideo() {
    playerController.setVideoQuality(null);
    playerEventEmitter.emitTrackModelChanged(
        instanceId,
        playerController.getVideoTracks(),
        playerController.getAudioTracks(),
        playerController.getSubtitleTracks(),
        playerController.getVideoTrack(),
        playerController.getAudioTrack(),
        playerController.getSubtitleTrack(),
        playerController.getVideoQuality(),
        playerController.getVideoQualityMode()
    );
  }

  /**
   * Activates Picture-in-Picture mode.
   *
   * @param activity The activity.
   */
  public void enterPictureInPictureMode(Activity activity) {
    if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
      PlayerView playerView = playerViewInstanceManager.get(playerViewInstanceId);
      updatePipParams(activity, playerView);
      activity.enterPictureInPictureMode();
    }
  }

  @NonNull
  public PlayerController getDelegate() {
    return playerController;
  }

  private void updatePipParams(@NonNull Activity activity, @Nullable PlayerView playerView) {
    // Set PiP params if supported
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder();
      Rect sourceRectHint = new Rect();
      if (playerView != null) {
        playerView.getGlobalVisibleRect(sourceRectHint);
      }
      if (playerController != null && playerController.getVideoQuality() != null) {
        int w = playerController.getVideoQuality().getWidth();
        int h = playerController.getVideoQuality().getHeight();
        builder.setAspectRatio(new Rational(w, h));
        if (playerView != null) {
          builder.setSourceRectHint(getVideoRect(w, h, sourceRectHint, playerView));
        }
      } else {
        builder.setAspectRatio(new Rational(16, 9));
        if (playerView != null) {
          builder.setSourceRectHint(sourceRectHint);
        }
      }
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(false);
        builder.setSeamlessResizeEnabled(true);
      }
      activity.setPictureInPictureParams(builder.build());
    }
  }

  private Rect getVideoRect(int videoWidth, int videoHeight, Rect mainRect, PlayerView playerView) {

    float videoRatio = (float) videoWidth / videoHeight;
    float mainRatio = (float) playerView.getWidth() / playerView.getHeight();

    if (videoRatio > mainRatio) {
      int left = mainRect.left;
      int right = mainRect.right;
      int margin = (int) ((playerView.getHeight() - playerView.getWidth() / videoRatio) / 2);
      int top = mainRect.top + margin;
      int bottom = mainRect.bottom - margin;
      return new Rect(left, top, right, bottom);
    } else {
      int top = mainRect.top;
      int bottom = mainRect.bottom;
      int margin = (int) ((playerView.getWidth() - playerView.getHeight() * videoRatio) / 2);
      int left = mainRect.left + margin;
      int right = mainRect.right - margin;
      return new Rect(left, top, right, bottom);
    }
  }

  public void setVideoFilterConfiguration(final @NonNull JsonMap videoFilterConfiguration) {

    VideoFilterConfiguration.Builder builder = new VideoFilterConfiguration.Builder();

    videoFilterConfiguration.getLong("maxBitrate").ifPresent(builder::maxBitrate);
    videoFilterConfiguration.getInteger("maxHeight").ifPresent(builder::maxHeight);
    videoFilterConfiguration.getInteger("maxWidth").ifPresent(builder::maxWidth);
    videoFilterConfiguration.getLong("maxPixel").ifPresent(builder::maxPixel);

    playerController.setVideoFilterConfiguration(builder.get(), null);

    /**
     * NOTE: setVideoFilterConfiguration is a NO-OP before
     */
    if (playerListenerForVideoFilter != null) {
      playerController.removePlayerListener(playerListenerForVideoFilter);
    }
    playerListenerForVideoFilter = new AbstractPlayerListener() {
      @Override
      public void onPlayerModelChanged() {
        playerController.setVideoFilterConfiguration(builder.get(), null);
      }
    };
    playerController.addPlayerListener(playerListenerForVideoFilter);
  }

  @Override
  public void onVideoSizeChanged() {
    PlayerView playerView = playerViewInstanceManager.get(playerViewInstanceId);
    if (playerView != null) {
      playerView.measureAndLayout();
    }
  }

  public void onPlayerViewCreated(PlayerView playerView) {
    extensions.forEach(extension -> extension.onPlayerViewCreated(playerView));
  }
  public void onPlayerWillRelease() {
    extensions.forEach(PlayerExtension::onPlayerWillRelease);
  }
  public void onPlayerViewWillDestroy(PlayerView playerView) {
    extensions.forEach(extension -> extension.onPlayerViewWillDestroy(playerView));
  }

  public void addExtension(@NonNull PlayerExtension playerExtension) {
    extensions.add(playerExtension);
  }

  @Nullable
  public <T extends PlayerExtension> T getExtension(@NonNull Class<T> clazz) {
    for (PlayerExtension extension : extensions) {
      if (clazz.isInstance(extension)) {
        return clazz.cast(extension);
      }
    }

    return null;
  }

  public void emitError (PrestoPlayError error) {
    playerEventEmitter.emitError(instanceId, error);
  }
}

