package com.castlabs.reactnative.player;

import android.app.Activity;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Process;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.castlabs.reactnative.sdk.Plugin;
import com.castlabs.reactnative.errors.ErrorCode;
import com.castlabs.reactnative.errors.ErrorSeverity;
import com.castlabs.reactnative.errors.PrestoPlayError;
import com.castlabs.reactnative.errors.Rejecter;
import com.castlabs.reactnative.network.RequestCompleter;
import com.castlabs.reactnative.network.ResponseCompleter;
import com.castlabs.reactnative.playerview.PlayerView;
import com.castlabs.reactnative.utils.JsonMap;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * The bridge used to interact with {@link Player}.
 */
@RequiresApi(api = Build.VERSION_CODES.N)
public class PlayerModule extends ReactContextBaseJavaModule {
  public static final String REACT_CLASS = "PlayerModule";

  private final @NonNull Map<String, Player> playerInstanceManager;
  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;

  /**
   * Initializes the player module.
   *
   * @param context the application context
   * @param playerInstanceManager the global player instance manager
   * @param playerViewInstanceManager the global player view instance manager
   * @param requestCompleterCache the global request completer cache
   * @param responseCompleterCache the global response completer cache
   */
  public PlayerModule(
      @NonNull ReactApplicationContext context,
      @NonNull Map<String, Player> playerInstanceManager,
      @NonNull Map<String, PlayerView> playerViewInstanceManager,
      @NonNull List<Plugin> pluginInstanceManager,
      @NonNull Map<String, RequestCompleter> requestCompleterCache,
      @NonNull Map<String, ResponseCompleter> responseCompleterCache,
      @NonNull PlayerEventEmitter playerEventEmitter
  ) {
    super(context);
    this.playerInstanceManager = playerInstanceManager;
    this.playerViewInstanceManager = playerViewInstanceManager;
    this.pluginInstanceManager = pluginInstanceManager;
    this.requestCompleterCache = requestCompleterCache;
    this.responseCompleterCache = responseCompleterCache;
    this.playerEventEmitter = playerEventEmitter;
  }

  @Override
  @NonNull
  public String getName() {
    return REACT_CLASS;
  }

  /**
   * Destroys all the player instances.
   * The method is executed on app reload.
   */
  @Override
  public void onCatalystInstanceDestroy() {
    Activity activity = getCurrentActivity();
    if (activity == null) {
      return;
    }

    // Create a snapshot of instance IDs before
    // executing asynchronous clean up
    Set<String> ids = new HashSet<>(playerInstanceManager.keySet());
    activity.runOnUiThread(new Runnable() {
      @Override
      public void run() {
        for (String id : ids) {
          Player player = playerInstanceManager.remove(id);
          if (player != null) {
            player.destroy();
          }
        }
      }
    });
  }

  @ReactMethod
  public void addListener(String eventName) {
    // https://stackoverflow.com/a/69650217
  }

  @ReactMethod
  public void removeListeners(Integer count) {
    // https://stackoverflow.com/a/69650217
  }

  /**
   * Creates an instance of the Player.
   * The newly created Player is registered with the given instance ID.
   *
   * @param instanceId The player UUID
   * @param promise    The promise
   */
  @ReactMethod
  public void create(String instanceId, Promise promise) {
    Activity activity = getCurrentActivity();
    if (activity != null) {
      activity.runOnUiThread(new Runnable() {

        @Override
        public void run() {
          ReactContext context = getReactApplicationContext();
          Player player = new Player(
              context,
              instanceId,
              playerViewInstanceManager,
              pluginInstanceManager,
              requestCompleterCache,
              responseCompleterCache,
              playerEventEmitter
          );
          playerInstanceManager.put(instanceId, player);
          promise.resolve(null);
        }
      });
    } else {
      new Rejecter(promise).reject(
          new PrestoPlayError(
              ErrorSeverity.FATAL,
              ErrorCode.PLAYER_NOT_FOUND,
              "Activity is not defined"
          )
      );
    }
  }

  /**
   * Loads the given configuration.
   *
   * @param instanceId          The player UUID
   * @param playerConfiguration The player configuration
   * @param promise             The promise
   */
  @ReactMethod
  public void open(
      String instanceId,
      ReadableMap playerConfiguration,
      Promise promise
  ) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
    } else {
      try {
        player.open(new JsonMap(playerConfiguration));
        promise.resolve(null);
      } catch (PrestoPlayError e) {
        new Rejecter(promise).reject(e);
      }
    }
  }

  /**
   * Starts playback.
   *
   * @param instanceId The player UUID.
   * @param promise    The promise.
   */
  @ReactMethod
  public void play(String instanceId, Promise promise) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
    } else {
      player.play();
      promise.resolve(null);
    }
  }

  /**
   * Re-starts playback.
   *
   * @param instanceId The player UUID.
   * @param promise    The promise.
   */
  @ReactMethod
  public void replay(String instanceId, Promise promise) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
    } else {
      player.replay();
      promise.resolve(null);
    }
  }

  /**
   * Pauses playback.
   *
   * @param instanceId The player UUID
   * @param promise    The promise
   */
  @ReactMethod
  public void pause(String instanceId, Promise promise) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
    } else {
      player.pause();
      promise.resolve(null);
    }
  }

  /**
   * Unloads a stream.
   * The player instance can be reused with a new player configuration.
   *
   * @param instanceId the player UUID
   * @param promise    the promise
   */
  @ReactMethod
  public void stop(String instanceId, Promise promise) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
    } else {
      player.stop();
      promise.resolve(null);
    }
  }

  /**
   * Unregisters and destroys the player.
   *
   * @param instanceId the player UUID
   * @param promise    the promise
   */
  @ReactMethod
  public void destroy(String instanceId, Promise promise) {
    Player player = playerInstanceManager.remove(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
    } else {
      Activity activity = getCurrentActivity();
      if (activity != null) {
        activity.runOnUiThread(player::destroy);
        promise.resolve(null);
      } else {
        new Rejecter(promise).reject(
            new PrestoPlayError(
                ErrorSeverity.FATAL,
                ErrorCode.PLAYER_NOT_FOUND,
                "Activity is not defined"
            )
        );
      }
    }
  }

  /**
   * Selects the audio track by track ID.
   *
   * @param instanceId the player UUID
   * @param trackId the audio track ID
   * @param promise the promise
   */
  @ReactMethod
  public void setAudioTrack(
      @NonNull String instanceId,
      @NonNull String trackId,
      @NonNull Promise promise
  ) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
      return;
    }

    player.setAudioTrack(trackId);
    promise.resolve(null);
  }

  /**
   * Selects the video track by the track ID.
   *
   * @param instanceId the player UUID
   * @param trackId the video track ID
   * @param promise the promise
   */
  @ReactMethod
  public void setVideoTrack(
      @NonNull String instanceId,
      @NonNull String trackId,
      @NonNull Promise promise
  ) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
      return;
    }

    player.setVideoTrack(trackId);
    promise.resolve(null);
  }

  /**
   * Selects the video rendition by the rendition ID.
   *
   * @param instanceId the player UUID
   * @param renditionId the rendition ID
   * @param promise the promise
   */
  @ReactMethod
  public void setVideoRendition(
      @NonNull String instanceId,
      @NonNull String renditionId,
      @NonNull Promise promise
  ) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
      return;
    }

    player.setVideoRendition(renditionId);
    promise.resolve(null);
  }

  /**
   * Selects the text track by track ID.
   *
   * @param instanceId the player UUID
   * @param trackId the text track ID
   * @param promise the promise
   */
  @ReactMethod
  public void setTextTrack(
      @NonNull String instanceId,
      @Nullable String trackId,
      @NonNull Promise promise
  ) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
      return;
    }

    player.setTextTrack(trackId);
    promise.resolve(null);
  }

  /**
   * Enables the ABR mode.
   *
   * @param instanceId the player UUID
   * @param promise the promise
   */
  @ReactMethod
  public void enableAdaptiveVideo(
      @NonNull String instanceId,
      @NonNull Promise promise
  ) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
      return;
    }

    player.enableAdaptiveVideo();
    promise.resolve(null);
  }

  /**
   * Moves the playhead (seeks) to specified time position.
   *
   * @param instanceId    the player UUID
   * @param newPositionMs the new playhead in milliseconds
   * @param promise       the promise
   */
  @ReactMethod
  public void setPosition(String instanceId, double newPositionMs, Promise promise) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
      return;
    }

    player.setPosition((long) newPositionMs);
    promise.resolve(null);
  }

  /**
   * Sets the volume to specified level.
   *
   * @param instanceId the player UUID
   * @param newVolume  the volume level between 0.0 and 1.0.
   * @param promise    the promise
   */
  @ReactMethod
  public void setVolume(String instanceId, double newVolume, Promise promise) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
      return;
    }

    player.setVolume((float) newVolume);
    promise.resolve(null);
  }

  /**
   * Set the playback speed, where 1 represents 'normal' speed.
   *
   * @param instanceId      the player UUID
   * @param newPlaybackRate the new speed at which the video is being played
   * @param promise         the promise
   */
  @ReactMethod
  public void setPlaybackRate(String instanceId, double newPlaybackRate, Promise promise) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
      return;
    }

    player.setPlaybackRate((float) newPlaybackRate);
    promise.resolve(null);
  }

  /**
   * Mutes or unmutes the player.
   *
   * @param instanceId the player UUID
   * @param newMuted   true if muted playback; false otherwise
   * @param promise    the promise
   */
  @ReactMethod
  public void setMuted(String instanceId, boolean newMuted, Promise promise) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
      return;
    }

    player.setMuted(newMuted);
    promise.resolve(null);
  }

  /**
   * Returns true if Picture-in-Picture mode is supported; otherwise false.
   *
   * @param instanceId The player UUID
   * @param promise    The promise
   */
  @ReactMethod
  public void supportsPictureInPicture(String instanceId, Promise promise) {
    // Check API version
    boolean isSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
    // Check PackageManager
    PackageManager packageManager = getReactApplicationContext().getPackageManager();
    isSupported =
        isSupported && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
    // Check AppOpsManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      AppOpsManager appOpsManager = (AppOpsManager) getReactApplicationContext()
          .getSystemService(Context.APP_OPS_SERVICE);
      int mode = appOpsManager.checkOp(
          AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
          Process.myUid(),
          getReactApplicationContext().getPackageName());
      isSupported = isSupported && mode == AppOpsManager.MODE_ALLOWED;
    }
    promise.resolve(isSupported);
  }

  /**
   * Resolves the promise with true if Picture-in-Picture mode is active.
   *
   * @param instanceId The player UUID
   * @param promise    The promise
   */
  @ReactMethod
  public void isInPictureInPictureMode(String instanceId, Promise promise) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
      Activity activity = getCurrentActivity();
      if (activity != null) {
        promise.resolve(activity.isInPictureInPictureMode());
      } else {
        new Rejecter(promise).reject(
            new PrestoPlayError(
                ErrorSeverity.FATAL,
                ErrorCode.PLAYER_NOT_FOUND,
                "Activity is not defined"
            )
        );
      }
    } else {
      new Rejecter(promise).reject(
          new PrestoPlayError(
              ErrorSeverity.FATAL,
              ErrorCode.PLATFORM_API_MISMATCH,
              "Requires API level 24 or later"
          )
      );
    }
  }

  /**
   * Enters Picture-in-Picture mode.
   *
   * @param instanceId The player UUID
   * @param promise    The promise
   */
  @ReactMethod
  public void enterPictureInPictureMode(String instanceId, Promise promise) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
          new PrestoPlayError(
              ErrorSeverity.FATAL,
              ErrorCode.PLAYER_NOT_FOUND,
              "Activity is not defined"
          )
      );
    } else {
      player.enterPictureInPictureMode(getCurrentActivity());
      promise.resolve(null);
    }
  }

  /**
   * Exits Picture-in-Picture mode.
   *
   * @param instanceId The player UUID
   * @param promise    The promise
   */
  @ReactMethod
  public void exitPictureInPictureMode(String instanceId, Promise promise) {
    promise.resolve(null);
  }

  /**
   * Set a video filter configuration
   *
   * @param instanceId The player UUID
   * @param videoFilterConfiguration The video filter configuration, can be null to set default.
   * @param promise    The promise
   */
  @ReactMethod
  public void setVideoFilterConfiguration(
    String instanceId,
    ReadableMap videoFilterConfiguration,
    Promise promise
  ) {
    Player player = playerInstanceManager.get(instanceId);
    if (player == null) {
      new Rejecter(promise).reject(
              new PrestoPlayError(ErrorSeverity.FATAL, ErrorCode.PLAYER_NOT_FOUND)
      );
    } else {
        player.setVideoFilterConfiguration(new JsonMap(videoFilterConfiguration));
        promise.resolve(null);
    }
  }
}
