/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.media3.exoplayer.ima;

import static androidx.media3.common.util.Assertions.checkState;

import android.os.Looper;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.trackselection.TrackSelectionArray;
import androidx.media3.test.utils.StubExoPlayer;

/** A fake {@link ExoPlayer} for testing content/ad playback. */
/* package */ final class FakeExoPlayer extends StubExoPlayer {

  private final ListenerSet<Listener> listeners;
  private final Timeline.Period period;
  private final Object windowUid = new Object();
  private final Object periodUid = new Object();
  private final MediaItem mediaItem = MediaItem.fromUri("http://google.com/0");

  private Timeline timeline;
  private @Player.State int state;
  private boolean playWhenReady;
  private int periodIndex;
  private long positionMs;
  private long contentPositionMs;
  private boolean isPlayingAd;
  private int adGroupIndex;
  private int adIndexInAdGroup;

  public FakeExoPlayer() {
    listeners =
        new ListenerSet<>(
            Looper.getMainLooper(),
            Clock.DEFAULT,
            (listener, flags) -> listener.onEvents(/* player= */ this, new Events(flags)));
    period = new Timeline.Period();
    state = Player.STATE_IDLE;
    playWhenReady = true;
    timeline = Timeline.EMPTY;
  }

  /** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */
  public void updateTimeline(Timeline timeline, @TimelineChangeReason int reason) {
    this.timeline = timeline;
    listeners.sendEvent(
        Player.EVENT_TIMELINE_CHANGED, listener -> listener.onTimelineChanged(timeline, reason));
  }

  /**
   * Sets the state of this player as if it were playing content at the given {@code position}. If
   * an ad is currently playing, this will trigger a position discontinuity.
   */
  public void setPlayingContentPosition(int periodIndex, long positionMs) {
    boolean notify = isPlayingAd;
    PositionInfo oldPosition =
        new PositionInfo(
            windowUid,
            /* mediaItemIndex= */ 0,
            mediaItem,
            periodUid,
            /* periodIndex= */ 0,
            this.positionMs,
            this.contentPositionMs,
            this.adGroupIndex,
            this.adIndexInAdGroup);
    isPlayingAd = false;
    adGroupIndex = C.INDEX_UNSET;
    adIndexInAdGroup = C.INDEX_UNSET;
    this.periodIndex = periodIndex;
    this.positionMs = positionMs;
    contentPositionMs = positionMs;
    if (notify) {
      PositionInfo newPosition =
          new PositionInfo(
              windowUid,
              /* mediaItemIndex= */ 0,
              mediaItem,
              periodUid,
              /* periodIndex= */ 0,
              positionMs,
              this.contentPositionMs,
              this.adGroupIndex,
              this.adIndexInAdGroup);
      listeners.sendEvent(
          Player.EVENT_POSITION_DISCONTINUITY,
          listener ->
              listener.onPositionDiscontinuity(
                  oldPosition, newPosition, DISCONTINUITY_REASON_AUTO_TRANSITION));
    }
  }

  /**
   * Sets the state of this player as if it were playing an ad with the given indices at the given
   * {@code position}. If the player is playing a different ad or content, this will trigger a
   * position discontinuity.
   */
  public void setPlayingAdPosition(
      int periodIndex,
      int adGroupIndex,
      int adIndexInAdGroup,
      long positionMs,
      long contentPositionMs) {
    boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup;
    PositionInfo oldPosition =
        new PositionInfo(
            windowUid,
            /* mediaItemIndex= */ 0,
            mediaItem,
            periodUid,
            /* periodIndex= */ 0,
            this.positionMs,
            this.contentPositionMs,
            this.adGroupIndex,
            this.adIndexInAdGroup);
    isPlayingAd = true;
    this.periodIndex = periodIndex;
    this.adGroupIndex = adGroupIndex;
    this.adIndexInAdGroup = adIndexInAdGroup;
    this.positionMs = positionMs;
    this.contentPositionMs = contentPositionMs;
    if (notify) {
      PositionInfo newPosition =
          new PositionInfo(
              windowUid,
              /* mediaItemIndex= */ 0,
              mediaItem,
              periodUid,
              /* periodIndex= */ 0,
              positionMs,
              contentPositionMs,
              adGroupIndex,
              adIndexInAdGroup);
      listeners.sendEvent(
          EVENT_POSITION_DISCONTINUITY,
          listener ->
              listener.onPositionDiscontinuity(
                  oldPosition, newPosition, DISCONTINUITY_REASON_AUTO_TRANSITION));
    }
  }

  /** Sets the {@link Player.State} of this player. */
  @SuppressWarnings("deprecation")
  public void setState(@Player.State int state, boolean playWhenReady) {
    boolean playWhenReadyChanged = this.playWhenReady != playWhenReady;
    boolean playbackStateChanged = this.state != state;
    this.state = state;
    this.playWhenReady = playWhenReady;
    if (playbackStateChanged || playWhenReadyChanged) {
      listeners.sendEvent(
          Player.EVENT_PLAYBACK_STATE_CHANGED,
          listener -> {
            listener.onPlayerStateChanged(playWhenReady, state);
            if (playbackStateChanged) {
              listener.onPlaybackStateChanged(state);
            }
            if (playWhenReadyChanged) {
              listener.onPlayWhenReadyChanged(
                  playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
            }
          });
    }
  }

  /**
   * Sets an error on this player.
   *
   * <p>This will propagate the error to {@link Player.Listener#onPlayerError(PlaybackException)}
   * and {@link Player.Listener#onPlayerErrorChanged(PlaybackException)} and will also update the
   * state to {@link Player#STATE_IDLE}.
   *
   * <p>The player must be in {@link #STATE_BUFFERING} or {@link #STATE_READY}.
   */
  @SuppressWarnings("deprecation") // Calling deprecated listener.onPlayerStateChanged()
  public void setPlayerError(PlaybackException error) {
    checkState(state == STATE_BUFFERING || state == STATE_READY);
    this.state = Player.STATE_IDLE;
    listeners.sendEvent(
        Player.EVENT_PLAYBACK_STATE_CHANGED,
        listener -> {
          listener.onPlayerError(error);
          listener.onPlayerErrorChanged(error);
          listener.onPlayerStateChanged(playWhenReady, state);
          listener.onPlaybackStateChanged(state);
        });
  }

  // ExoPlayer methods. Other methods are unsupported.

  @Override
  public AudioComponent getAudioComponent() {
    return null;
  }

  @Override
  public Looper getApplicationLooper() {
    return Looper.getMainLooper();
  }

  @Override
  public void addListener(Player.Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void removeListener(Player.Listener listener) {
    listeners.remove(listener);
  }

  @Override
  public Commands getAvailableCommands() {
    return Commands.EMPTY;
  }

  @Override
  public @Player.State int getPlaybackState() {
    return state;
  }

  @Override
  public boolean getPlayWhenReady() {
    return playWhenReady;
  }

  @Override
  public @RepeatMode int getRepeatMode() {
    return REPEAT_MODE_OFF;
  }

  @Override
  public boolean getShuffleModeEnabled() {
    return false;
  }

  @Override
  public int getRendererCount() {
    return 0;
  }

  @Override
  public TrackSelectionArray getCurrentTrackSelections() {
    return new TrackSelectionArray();
  }

  @Override
  public Tracks getCurrentTracks() {
    return Tracks.EMPTY;
  }

  @Override
  public TrackSelectionParameters getTrackSelectionParameters() {
    return TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT;
  }

  @Override
  public void setTrackSelectionParameters(TrackSelectionParameters parameters) {}

  @Override
  public Timeline getCurrentTimeline() {
    return timeline;
  }

  @Override
  public int getCurrentPeriodIndex() {
    return periodIndex;
  }

  @Override
  public int getCurrentMediaItemIndex() {
    return 0;
  }

  @Override
  public long getDuration() {
    if (timeline.isEmpty()) {
      return C.INDEX_UNSET;
    }
    if (isPlayingAd()) {
      long adDurationUs =
          timeline.getPeriod(0, period).getAdDurationUs(adGroupIndex, adIndexInAdGroup);
      return Util.usToMs(adDurationUs);
    } else {
      return timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();
    }
  }

  @Override
  public long getCurrentPosition() {
    return positionMs;
  }

  @Override
  public boolean isPlayingAd() {
    return isPlayingAd;
  }

  @Override
  public int getCurrentAdGroupIndex() {
    return adGroupIndex;
  }

  @Override
  public int getCurrentAdIndexInAdGroup() {
    return adIndexInAdGroup;
  }

  @Override
  public long getContentPosition() {
    return contentPositionMs;
  }
}
