package com.castlabs.reactnative.player;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.castlabs.android.SdkConsts;
import com.castlabs.android.drm.Drm;
import com.castlabs.android.player.PlayerController;
import com.castlabs.android.player.exceptions.CastlabsPlayerException;
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.errors.PrestoPlayError;
import com.castlabs.reactnative.utils.BridgeSerializer;
import com.castlabs.reactnative.utils.DrmEventEmitter;
import com.castlabs.reactnative.utils.EventEmitter;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * The event emitter.
 */
public class PlayerEventEmitter extends DrmEventEmitter {
  public PlayerEventEmitter(EventEmitter emitter) {
    super(emitter);
  }

  /**
   * Emits the playWhenReadyChanged event.
   *
   * @param playerId the player ID
   * @param playWhenReady the playing intention
   */
  public void emitPlayWhenReadyChanged(String playerId, boolean playWhenReady) {
    WritableMap event = Arguments.createMap();
    event.putBoolean("playWhenReady", playWhenReady);
    emit(playerId, "playWhenReadyChanged", event);
  }

  /**
   * Emits the liveChanged event.
   *
   * @param playerId the player ID
   * @param live true if live stream; false otherwise
   */
  public void emitLiveChangedEvent(String playerId, boolean live) {
    WritableMap event = Arguments.createMap();
    event.putBoolean("live", live);
    emit(playerId, "liveChanged", event);
  }

  /**
   * Emits the durationChanged event.
   *
   * @param playerId the player ID
   * @param durationUs true if live stream; false otherwise
   */
  public void emitDurationChangedEvent(String playerId, long durationUs) {
    WritableMap event = Arguments.createMap();
    event.putDouble("duration", TimeUnit.MICROSECONDS.toMillis(durationUs));
    emit(playerId, "durationChanged", event);
  }

  /**
   * Emits the pictureInPictureModeChanged event.
   *
   * @param playerId the player ID
   * @param pictureInPictureMode the mode of the picture-in-picture.
   */
  public void emitPictureInPictureModeChangedEvent(String playerId, boolean pictureInPictureMode) {
    WritableMap event = Arguments.createMap();
    event.putBoolean("pictureInPictureMode", pictureInPictureMode);
    emit(playerId, "pictureInPictureModeChanged", event);
  }

  /**
   * Emits the volumeChanged event.
   *
   * @param playerId the player ID
   * @param volume the volume level between 0.0 and 1.0
   */
  public void emitVolumeChangedEvent(String playerId, float volume) {
    WritableMap event = Arguments.createMap();
    event.putDouble("volume", volume);
    emit(playerId, "volumeChanged", event);
  }

  /**
   * Emits the playbackRateChanged event.
   *
   * @param playerId the player ID
   * @param playbackRate the speed at which the video is being played.
   */
  public void emitPlaybackRateChangedEvent(String playerId, float playbackRate) {
    WritableMap event = Arguments.createMap();
    event.putDouble("playbackRate", playbackRate);
    emit(playerId, "playbackRateChanged", event);
  }

  /**
   * Emits the mutedChanged event.
   *
   * @param playerId the player ID
   * @param muted true if muted playback; false otherwise
   */
  public void emitMutedChangedEvent(String playerId, boolean muted) {
    WritableMap event = Arguments.createMap();
    event.putBoolean("muted", muted);
    emit(playerId, "mutedChanged", event);
  }

  /**
   * Emits the stateChanged event.
   *
   * @param playerId the player ID
   * @param state the player state
   */
  public void emitStateChangedEvent(String playerId, PlayerController.State state) {
    WritableMap event = Arguments.createMap();
    event.putString("currentState", BridgeSerializer.toState(state));
    emit(playerId, "stateChanged", event);
  }

  /**
   * Emits the stateChanged event.
   *
   * @param playerId the player ID
   * @param state the player state
   */
  public void emitStateChangedEvent(String playerId, String state) {
    WritableMap event = Arguments.createMap();
    event.putString("currentState", state);
    emit(playerId, "stateChanged", event);
  }

  /**
   * Emits the positionChanged event.
   *
   * @param playerId the player Id
   * @param positionMs the current position in milliseconds
   */
  public void emitPositionChanged(String playerId, long positionMs, long liveStartTime) {
    WritableMap event = Arguments.createMap();
    event.putDouble("position", positionMs);
    event.putDouble("liveStartTime", liveStartTime);
    emit(playerId, "positionChanged", event);
  }

  /**
   * Emits the error event.
   *
   * @param playerId the player Id
   * @param coreSdkError the thrown exception
   */
  public void emitError(String playerId, CastlabsPlayerException coreSdkError) {
    emitError(playerId, new PrestoPlayError(coreSdkError));
  }

  /**
   * Emits the track model changed.
   *
   * @param playerId player ID
   * @param videoTracks video track
   * @param audioTracks audio track
   * @param currentVideoTrack selected video track
   * @param currentAudioTrack selected audio track
   * @param currentVideoQuality selected video track quality
   * @param videoQualityMode adaptive or manual video quality mode
   */

  public void emitTrackModelChanged(
      String playerId,
      List<VideoTrack> videoTracks,
      List<AudioTrack> audioTracks,
      List<SubtitleTrack> subtitleTracks,
      VideoTrack currentVideoTrack,
      AudioTrack currentAudioTrack,
      SubtitleTrack currentSubtitleTrack,
      VideoTrackQuality currentVideoQuality,
      int videoQualityMode
  ) {
    WritableMap event = Arguments.createMap();

    event.putArray("videoTracks", BridgeSerializer.toVideoTracks(
        videoTracks,
        currentVideoTrack,
        currentVideoQuality
    ));
    event.putArray("audioTracks", BridgeSerializer.toAudioTracks(
        audioTracks,
        currentAudioTrack
    ));
    event.putArray("textTracks", BridgeSerializer.toTextTracks(
        subtitleTracks,
        currentSubtitleTrack
    ));
    event.putBoolean("adaptiveVideoEnabled",
        videoQualityMode == SdkConsts.VIDEO_QUALITY_ADAPTIVE
    );

    // 🔐 Snapshot BEFORE emitting
    Map<String, Object> snapshot = event.toHashMap();

    if (areMapsEqual(snapshot, lastTrackModelSnapshot)) {
      // Do not emit
      return;
    }

    lastTrackModelSnapshot = snapshot;

    emit(playerId, "trackModelChanged", event);
  }

  public void emitPlaybackStatsChanged(
      final @NonNull String playerId,
      final int droppedFrames,
      final @Nullable Long streamBandwidth,
      final @Nullable Long estimatedBandwidth,
      final long bufferedUsec
  ) {
    WritableMap event = Arguments.createMap();

    WritableMap playbackStats = Arguments.createMap();
    playbackStats.putDouble("droppedFrames", droppedFrames);

    if (streamBandwidth != null) {
      playbackStats.putDouble("streamBandwidth", streamBandwidth);
    }
    if (estimatedBandwidth != null) {
      playbackStats.putDouble("estimatedBandwidth", estimatedBandwidth);
    }

    long bufferedMsec = bufferedUsec > 0 ? bufferedUsec/1000 : 0;

    playbackStats.putDouble("bufferedTimeMsec", bufferedMsec);

    event.putMap("playbackStats", playbackStats);

    emit(playerId, "playbackStatsChanged", event);

  }

  /**
   * Emits the DRM changed.
   *
   * @param playerId player ID
   * @param drm key system
   */
  public void emitDrmChanged(
      final @NonNull String playerId,
      final @NonNull Drm drm
  ) {
    WritableMap event = Arguments.createMap();
    event.putString("drm", BridgeSerializer.toDrm(drm));
    emit(playerId, "drmChanged", event);
  }

  /**
   * Emits the seekableRange changed.
   *
   * @param playerId player ID
   * @param startTime in milliseconds from epoch
   * @param endTime in milliseconds from epoch
   */
  public void emitSeekableRangeChangedEvent(
          final @NonNull String playerId,
          final long startTime,
          final long endTime
  ) {
    WritableMap event = Arguments.createMap();
    event.putDouble("startTime", startTime);
    event.putDouble("endTime", endTime);
    emit(playerId, "seekableRangeChanged", event);
  }

  private boolean areMapsEqual(
          Map<String, Object> a,
          Map<String, Object> b
  ) {
    if (a == b) return true;
    if (a == null || b == null) return false;
    return a.equals(b);
  }

  /**
   * To avoid emitting duplicate events on trackModel
   */
  private Map<String, Object> lastTrackModelSnapshot = null;

}
