package allen.town.podcast.playback.base;

import android.content.Context;
import android.media.AudioManager;
import android.net.wifi.WifiManager;
import androidx.annotation.NonNull;
import android.util.Log;
import android.util.Pair;
import android.view.SurfaceHolder;

import java.util.List;
import java.util.concurrent.Future;

import androidx.annotation.Nullable;
import allen.town.podcast.model.playback.MediaType;
import allen.town.podcast.model.playback.Playable;


/*
 * An inconvenience of an implementation like this is that some members and methods that once were
 * private are now protected, allowing for access from classes of the same package, namely
 * PlaybackService. A workaround would be to move this to a dedicated package.
 */
/**
 * Abstract class that allows for different implementations of the PlaybackServiceMediaPlayer for local
 * and remote (cast devices) playback.
 */
public abstract class PlaybackServiceMediaPlayer {
    private static final String TAG = "baseMediaPlayer";

    /**
     * Return value of some PSMP methods if the method call failed.
     */
    public static final int INVALID_TIME = -1;

    private volatile PlayerStatus oldPlayerStatus;
    protected volatile PlayerStatus playerStatus;

    /**
     * A wifi-lock that is acquired if the media file is being streamed.
     */
    private WifiManager.WifiLock wifiLock;

    protected final PSMPCallback callback;
    protected final Context context;

    protected PlaybackServiceMediaPlayer(@NonNull Context context,
                               @NonNull PSMPCallback callback){
        this.context = context;
        this.callback = callback;

        playerStatus = PlayerStatus.STOPPED;
    }

    /**
     * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing
     * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will
     * not do anything.
     * Whether playback starts immediately depends on the given parameters. See below for more details.
     * <p/>
     * States:
     * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters.
     * <p/>
     * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If
     * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state.
     * <p/>
     * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object
     * will enter the ERROR state.
     * <p/>
     * This method is executed on an internal executor service.
     *
     * @param playable           The Playable object that is supposed to be played. This parameter must not be null.
     * @param stream             The type of playback. If false, the Playable object MUST provide access to a locally available file via
     *                           getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by
     *                           the Android MediaPlayer via getStreamUrl.
     * @param startWhenPrepared  Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the
     *                           episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared
     *                           for playback immediately (see 'prepareImmediately' parameter for more details)
     * @param prepareImmediately Set to true if the method should also prepare the episode for playback.
     */
    public abstract void playMediaObject(@NonNull Playable playable, boolean stream, boolean startWhenPrepared, boolean prepareImmediately);

    /**
     * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state.
     * nothing will happen.
     * <p/>
     * This method is executed on an internal executor service.
     */
    public abstract void resume();

    /**
     * Saves the current position and pauses playback. Note that, if audiofocus
     * is abandoned, the lockscreen controls will also disapear.
     * <p/>
     * This method is executed on an internal executor service.
     *
     * @param abandonFocus is true if the service should release audio focus
     * @param reinit       is true if service should reinit after pausing if the media
     *                     file is being streamed
     */
    public abstract void pause(boolean abandonFocus, boolean reinit);

    /**
     * Prepared media player for playback if the service is in the INITALIZED
     * state.
     * <p/>
     * This method is executed on an internal executor service.
     */
    public abstract void prepare();

    /**
     * Resets the media player and moves it into INITIALIZED state.
     * <p/>
     * This method is executed on an internal executor service.
     */
    public abstract void reinit();

    /**
     * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
     * Invalid time values (< 0) will be ignored.
     * <p/>
     * This method is executed on an internal executor service.
     */
    public abstract void seekTo(int t);

    /**
     * Seek a specific position from the current position
     *
     * @param d offset from current position (positive or negative)
     */
    public abstract void seekDelta(int d);

    /**
     * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved.
     */
    public abstract int getDuration();

    /**
     * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved.
     */
    public abstract int getPosition();

    public abstract boolean isStartWhenPrepared();

    public abstract void setStartWhenPrepared(boolean startWhenPrepared);

    /**
     * Sets the playback parameters.
     * - Speed
     * - SkipSilence (ExoPlayer only)
     * This method is executed on an internal executor service.
     */
    public abstract void  setPlaybackParams(final float speed, final boolean skipSilence);

    /**
     * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned.
     */
    public abstract float getPlaybackSpeed();

    /**
     * Sets the playback volume.
     * This method is executed on an internal executor service.
     */
    public abstract void setVolume(float volumeLeft, float volumeRight);

    /**
     * Returns true if the mediaplayer can mix stereo down to mono
     */
    public abstract boolean canDownmix();

    public abstract void setDownmix(boolean enable);

    public abstract void setLoudness(boolean enable);

    public abstract MediaType getCurrentMediaType();

    public abstract boolean isStreaming();

    /**
     * Releases internally used resources. This method should only be called when the object is not used anymore.
     */
    public abstract void shutdown();

    public abstract void setVideoSurface(SurfaceHolder surface);

    public abstract void resetVideoSurface();

    /**
     * Return width and height of the currently playing video as a pair.
     *
     * @return Width and height as a Pair or null if the video size could not be determined. The method might still
     * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return
     * invalid values.
     */
    public abstract Pair<Integer, Integer> getVideoSize();

    /**
     * Returns a PSMInfo object that contains information about the current state of the PSMP object.
     *
     * @return The PSMPInfo object.
     */
    public final synchronized PSMPInfo getPSMPInfo() {
        return new PSMPInfo(oldPlayerStatus, playerStatus, getPlayable());
    }

    /**
     * Returns the current status, if you need the media and the player status together, you should
     * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition
     * could result in nonsensical results (like a status of PLAYING, but a null playable)
     * @return the current player status
     */
    public synchronized PlayerStatus getPlayerStatus() {
        return playerStatus;
    }

    /**
     * Returns the current media, if you need the media and the player status together, you should
     * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition
     * could result in nonsensical results (like a status of PLAYING, but a null playable)
     * @return the current media. May be null
     */
    public abstract Playable getPlayable();

    protected abstract void setPlayable(Playable playable);

    public abstract List<String> getAudioTracks();

    public abstract void setAudioTrack(int track);

    public abstract int getSelectedAudioTrack();

    public void skip() {
        if (getPosition() < 1000) {
            Log.d(TAG, "Ignoring skip, is in first second of playback");
            return;
        }
        endPlayback(false, true, true, true);
    }

    /**
     * Ends playback of current media (if any) and moves into INDETERMINATE state, unless
     * {@param toStoppedState} is set to true, in which case it moves into STOPPED state.
     *
     * @see #endPlayback(boolean, boolean, boolean, boolean)
     */
    public Future<?> stopPlayback(boolean toStoppedState) {
        return endPlayback(false, false, false, toStoppedState);
    }

    /**
     * Internal method that handles end of playback.
     *
     * Currently, it has 5 use cases:
     * <ul>
     * <li>Media playback has completed: call with (true, false, true, true)</li>
     * <li>User asks to skip to next episode: call with (false, true, true, true)</li>
     * <li>Skipping to next episode due to playback error: call with (false, false, true, true)</li>
     * <li>Stopping the media player: call with (false, false, false, true)</li>
     * <li>We want to change the media player implementation: call with (false, false, false, false)</li>
     * </ul>
     *
     * @param hasEnded         If true, we assume the current media's playback has ended, for
     *                         purposes of post playback processing.
     * @param wasSkipped       Whether the user chose to skip the episode (by pressing the skip
     *                         button).
     * @param shouldContinue   If true, the media player should try to load, and possibly play,
     *                         the next item, based on the user preferences and whether such item
     *                         exists.
     * @param toStoppedState   If true, the playback state gets set to STOPPED if the media player
     *                         is not loading/playing after this call, and the UI will reflect that.
     *                         Only relevant if {@param shouldContinue} is set to false, otherwise
     *                         this method's behavior defaults as if this parameter was true.
     *
     * @return a Future, just for the purpose of tracking its execution.
     */
    protected abstract Future<?> endPlayback(boolean hasEnded, boolean wasSkipped,
                                             boolean shouldContinue, boolean toStoppedState);

    /**
     * @return {@code true} if the WifiLock feature should be used, {@code false} otherwise.
     */
    protected abstract boolean shouldLockWifi();

    public abstract boolean isCasting();

    protected final synchronized void acquireWifiLockIfNecessary() {
        if (shouldLockWifi()) {
            if (wifiLock == null) {
                wifiLock = ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE))
                        .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
                wifiLock.setReferenceCounted(false);
            }
            wifiLock.acquire();
        }
    }

    protected final synchronized void releaseWifiLockIfNecessary() {
        if (wifiLock != null && wifiLock.isHeld()) {
            wifiLock.release();
        }
    }

    /**
     * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time
     * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null).
     * <p/>
     * This method will notify the callback about the change of the player status (even if the new status is the same
     * as the old one).
     * <p/>
     * It will also call {@link PSMPCallback#onPlaybackPause(Playable, int)} or {@link PSMPCallback#onPlaybackStart(Playable, int)}
     * depending on the status change.
     *
     * @param newStatus The new PlayerStatus. This must not be null.
     * @param newMedia  The new playable object of the PSMP object. This can be null.
     * @param position  The position to be set to the current Playable object in case playback started or paused.
     *                  Will be ignored if given the value of {@link #INVALID_TIME}.
     */
    protected final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus,
                                                      Playable newMedia, int position) {
//        Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus);

        this.oldPlayerStatus = playerStatus;
        this.playerStatus = newStatus;
        setPlayable(newMedia);

        if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) {
            if (oldPlayerStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING) {
                callback.onPlaybackPause(newMedia, position);
            } else if (oldPlayerStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING) {
                callback.onPlaybackStart(newMedia, position);
            }
        }

        callback.statusChanged(new PSMPInfo(oldPlayerStatus, playerStatus, getPlayable()));
    }

    public boolean isAudioChannelInUse() {
        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        return (audioManager.getMode() != AudioManager.MODE_NORMAL || audioManager.isMusicActive());
    }

    /**
     * @see #setPlayerStatus(PlayerStatus, Playable, int)
     */
    protected final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
        setPlayerStatus(newStatus, newMedia, INVALID_TIME);
    }

    public interface PSMPCallback {
        void statusChanged(PSMPInfo newInfo);

        void shouldStop();

        void onMediaChanged(boolean reloadUI);

        void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext);

        void onPlaybackStart(@NonNull Playable playable, int position);

        void onPlaybackPause(Playable playable, int position);

        Playable getNextInQueue(Playable currentMedia);

        @Nullable
        Playable findMedia(@NonNull String url);

        void onPlaybackEnded(MediaType mediaType, boolean stopPlaying);

        void ensureMediaInfoLoaded(@NonNull Playable media);
    }

    /**
     * Holds information about a PSMP object.
     */
    public static class PSMPInfo {
        public final PlayerStatus oldPlayerStatus;
        public PlayerStatus playerStatus;
        public Playable playable;

        public PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) {
            this.oldPlayerStatus = oldPlayerStatus;
            this.playerStatus = playerStatus;
            this.playable = playable;
        }
    }
}
