/*
 * Copyright (c) 2018 Nam Nguyen, nam@ene.im
 *
 * 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 ml.docilealligator.infinityforreddit.videoautoplay;

import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media3.common.Metadata;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.metadata.MetadataOutput;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.text.TextOutput;
import androidx.media3.ui.PlayerView;

import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;

import ml.docilealligator.infinityforreddit.videoautoplay.annotations.RemoveIn;
import ml.docilealligator.infinityforreddit.videoautoplay.media.PlaybackInfo;
import ml.docilealligator.infinityforreddit.videoautoplay.media.VolumeInfo;

/**
 * Define an interface to control a playback, specific for {@link ExoPlayer} and {@link PlayerView}.
 * <p>
 * This interface is designed to be reused across Config change. Implementation must not hold any
 * strong reference to Activity, and if it supports any kind of that, make sure to implicitly clean
 * it up.
 *
 * @author eneim
 * @since 3.4.0
 */

@SuppressWarnings("unused") //
public interface Playable {

    /**
     * Prepare the resource for a {@link ExoPlayer}. This method should:
     * - Request for new {@link ExoPlayer} instance if there is not a usable one.
     * - Configure {@link EventListener} for it.
     * - If there is non-trivial PlaybackInfo, update it to the ExoPlayer.
     * - If client request to prepare MediaSource, then prepare it.
     * <p>
     * This method must be called before {@link #setPlayerView(PlayerView)}.
     *
     * @param prepareSource if {@code true}, also prepare the MediaSource when preparing the Player,
     *                      if {@code false} just do nothing for the MediaSource.
     */
    void prepare(boolean prepareSource);

    /**
     * Set the {@link PlayerView} for this Playable. It is expected that a playback doesn't require a
     * UI, so this setup is optional. But it must be called after the ExoPlayer is prepared,
     * that is after {@link #prepare(boolean)} and before {@link #release()}.
     * <p>
     * Changing the PlayerView during playback is expected, though not always recommended, especially
     * on old Devices with low Android API.
     *
     * @param playerView the PlayerView to set to the ExoPlayer.
     */
    void setPlayerView(@Nullable PlayerView playerView);

    /**
     * Get current {@link PlayerView} of this Playable.
     *
     * @return current PlayerView instance of this Playable.
     */
    @Nullable
    PlayerView getPlayerView();

    /**
     * Start the playback. If the {@link MediaSource} is not prepared, then also prepare it.
     */
    void play();

    /**
     * Pause the playback.
     */
    void pause();

    /**
     * Reset all resource, so that the playback can start all over again. This is to cleanup the
     * playback for reuse. The ExoPlayer instance must be still usable without calling
     * {@link #prepare(boolean)}.
     */
    void reset();

    /**
     * Release all resource. After this, the ExoPlayer is released to the Player pool and the
     * Playable must call {@link #prepare(boolean)} again to use it again.
     */
    void release();

    /**
     * Get current {@link PlaybackInfo} of the playback.
     *
     * @return current PlaybackInfo of the playback.
     */
    @NonNull
    PlaybackInfo getPlaybackInfo();

    /**
     * Set the custom {@link PlaybackInfo} for this playback. This could suggest a seek.
     *
     * @param playbackInfo the PlaybackInfo to set for this playback.
     */
    void setPlaybackInfo(@NonNull PlaybackInfo playbackInfo);

    /**
     * Add a new {@link EventListener} to this Playable. As calling {@link #prepare(boolean)} also
     * triggers some internal events, this method should be called before {@link #prepare(boolean)} so
     * that Client could received them all.
     *
     * @param listener the EventListener to add, must be not {@code null}.
     */
    void addEventListener(@NonNull EventListener listener);

    /**
     * Remove an {@link EventListener} from this Playable.
     *
     * @param listener the EventListener to be removed. If null, nothing happens.
     */
    void removeEventListener(EventListener listener);

    /**
     * !This must only work if the Player in use is a {@link ToroExoPlayer}.
     */
    void addOnVolumeChangeListener(@NonNull ToroPlayer.OnVolumeChangeListener listener);

    void removeOnVolumeChangeListener(@Nullable ToroPlayer.OnVolumeChangeListener listener);

    /**
     * Check if current Playable is playing or not.
     *
     * @return {@code true} if this Playable is playing, {@code false} otherwise.
     */
    boolean isPlaying();

    /**
     * Change the volume of current playback.
     *
     * @param volume the volume value to be set. Must be a {@code float} of range from 0 to 1.
     * @deprecated use {@link #setVolumeInfo(VolumeInfo)} instead.
     */
    @RemoveIn(version = "3.6.0")
    @Deprecated
    //
    void setVolume(@FloatRange(from = 0.0, to = 1.0) float volume);

    /**
     * Obtain current volume value. The returned value is a {@code float} of range from 0 to 1.
     *
     * @return current volume value.
     * @deprecated use {@link #getVolumeInfo()} instead.
     */
    @RemoveIn(version = "3.6.0")
    @Deprecated  //
    @FloatRange(from = 0.0, to = 1.0)
    float getVolume();

    /**
     * Update playback's volume.
     *
     * @param volumeInfo the {@link VolumeInfo} to update to.
     * @return {@code true} if current Volume info is updated, {@code false} otherwise.
     */
    boolean setVolumeInfo(@NonNull VolumeInfo volumeInfo);

    /**
     * Get current {@link VolumeInfo}.
     */
    @NonNull
    VolumeInfo getVolumeInfo();

    /**
     * Same as {@link Player#setPlaybackParameters(PlaybackParameters)}
     */
    void setParameters(@Nullable PlaybackParameters parameters);

    /**
     * Same as {@link Player#getPlaybackParameters()}
     */
    @Nullable
    PlaybackParameters getParameters();

    void addErrorListener(@NonNull ToroPlayer.OnErrorListener listener);

    void removeErrorListener(@Nullable ToroPlayer.OnErrorListener listener);

    // Combine necessary interfaces.
    @UnstableApi
    interface EventListener extends Player.Listener, TextOutput, MetadataOutput {

        @Override
        default void onCues(@NonNull List<Cue> cues) {

        }

        @Override
        default void onCues(@NonNull CueGroup cueGroup) {

        }

        @Override
        default void onMetadata(@NonNull Metadata metadata) {

        }
    }

    /**
     * Default empty implementation
     */
    @UnstableApi
    class DefaultEventListener implements EventListener {
        @Override
        public void onTimelineChanged(@NonNull Timeline timeline, int reason) {

        }

        @Override
        public void onTracksChanged(@NonNull Tracks tracks) {

        }

        @Override
        public void onIsLoadingChanged(boolean isLoading) {

        }

        @Override
        public void onPlaybackStateChanged(int playbackState) {

        }

        @Override
        public void onRepeatModeChanged(int repeatMode) {

        }

        @Override
        public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {

        }

        @Override
        public void onPlayerError(@NonNull PlaybackException error) {

        }

        @Override
        public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) {

        }

        @Override
        public void onPlaybackParametersChanged(@NonNull PlaybackParameters playbackParameters) {

        }

        @Override
        public void onVideoSizeChanged(@NonNull VideoSize videoSize) {
            EventListener.super.onVideoSizeChanged(videoSize);
        }

        @Override
        public void onRenderedFirstFrame() {

        }

        @Override
        public void onCues(@NonNull CueGroup cueGroup) {
            EventListener.super.onCues(cueGroup);
        }

        @Override
        public void onMetadata(@NonNull Metadata metadata) {

        }
    }

    /**
     * List of EventListener
     */
    @UnstableApi
    class EventListeners extends CopyOnWriteArraySet<EventListener> implements EventListener {

        EventListeners() {
        }

        @Override
        public void onEvents(@NonNull Player player, @NonNull Player.Events events) {
            for (EventListener eventListener : this) {
                eventListener.onEvents(player, events);
            }
        }

        @Override
        public void onVideoSizeChanged(@NonNull VideoSize videoSize) {
            for (EventListener eventListener : this) {
                eventListener.onVideoSizeChanged(videoSize);
            }
        }

        @Override
        public void onRenderedFirstFrame() {
            for (EventListener eventListener : this) {
                eventListener.onRenderedFirstFrame();
            }
        }

        @Override
        public void onTimelineChanged(@NonNull Timeline timeline, int reason) {
            for (EventListener eventListener : this) {
                eventListener.onTimelineChanged(timeline, reason);
            }
        }

        @Override
        public void onTracksChanged(@NonNull Tracks tracks) {
            for (EventListener eventListener : this) {
                eventListener.onTracksChanged(tracks);
            }
        }

        @Override
        public void onIsLoadingChanged(boolean isLoading) {
            for (EventListener eventListener : this) {
                eventListener.onIsLoadingChanged(isLoading);
            }
        }

        @Override
        public void onPlaybackStateChanged(int playbackState) {
            for (EventListener eventListener : this) {
                eventListener.onPlaybackStateChanged(playbackState);
            }
        }

        @Override
        public void onRepeatModeChanged(int repeatMode) {
            for (EventListener eventListener : this) {
                eventListener.onRepeatModeChanged(repeatMode);
            }
        }

        @Override
        public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
            for (EventListener eventListener : this) {
                eventListener.onShuffleModeEnabledChanged(shuffleModeEnabled);
            }
        }

        @Override
        public void onPlayerError(@NonNull PlaybackException error) {
            for (EventListener eventListener : this) {
                eventListener.onPlayerError(error);
            }
        }

        @Override
        public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) {
            for (EventListener eventListener : this) {
                eventListener.onPositionDiscontinuity(oldPosition, newPosition, reason);
            }
        }

        @Override
        public void onPlaybackParametersChanged(@NonNull PlaybackParameters playbackParameters) {
            for (EventListener eventListener : this) {
                eventListener.onPlaybackParametersChanged(playbackParameters);
            }
        }

        @Override
        public void onCues(@NonNull CueGroup cueGroup) {
            for (EventListener eventListener : this) {
                eventListener.onCues(cueGroup);
            }
        }

        @Override
        public void onMetadata(@NonNull Metadata metadata) {
            for (EventListener eventListener : this) {
                eventListener.onMetadata(metadata);
            }
        }
    }
}
