package org.schabi.newpipe.player.helper;

import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
import static org.schabi.newpipe.extractor.stream.VideoStream.RESOLUTION_UNKNOWN;
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.PixelFormat;
import android.os.Build;
import android.provider.Settings;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.CaptioningManager;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;

import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
import com.google.android.exoplayer2.ui.CaptionStyleCompat;
import com.google.android.exoplayer2.util.MimeTypes;

import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.ListHelper;

import java.lang.annotation.Retention;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Formatter;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public final class PlayerHelper {
    private static final StringBuilder STRING_BUILDER = new StringBuilder();
    private static final Formatter STRING_FORMATTER
            = new Formatter(STRING_BUILDER, Locale.getDefault());
    private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
    private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");

    /**
     * Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using
     * NewPipe's popup player.
     *
     * <p>
     * This value is hardcoded instead of being get dynamically with the method linked of the
     * constant documentation below, because it is not static and popup player layout parameters
     * are generated with static methods.
     * </p>
     *
     * @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
     */
    private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f;

    @Retention(SOURCE)
    @IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
            AUTOPLAY_TYPE_NEVER})
    public @interface AutoplayType {
        int AUTOPLAY_TYPE_ALWAYS = 0;
        int AUTOPLAY_TYPE_WIFI = 1;
        int AUTOPLAY_TYPE_NEVER = 2;
    }

    @Retention(SOURCE)
    @IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND,
            MINIMIZE_ON_EXIT_MODE_POPUP})
    public @interface MinimizeMode {
        int MINIMIZE_ON_EXIT_MODE_NONE = 0;
        int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1;
        int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
    }

    private PlayerHelper() {
    }

    ////////////////////////////////////////////////////////////////////////////
    // Exposed helpers
    ////////////////////////////////////////////////////////////////////////////

    @NonNull
    public static String getTimeString(final int milliSeconds) {
        final int seconds = (milliSeconds % 60000) / 1000;
        final int minutes = (milliSeconds % 3600000) / 60000;
        final int hours = (milliSeconds % 86400000) / 3600000;
        final int days = (milliSeconds % (86400000 * 7)) / 86400000;

        STRING_BUILDER.setLength(0);
        return (days > 0
                ? STRING_FORMATTER.format("%d:%02d:%02d:%02d", days, hours, minutes, seconds)
                : hours > 0
                ? STRING_FORMATTER.format("%d:%02d:%02d", hours, minutes, seconds)
                : STRING_FORMATTER.format("%02d:%02d", minutes, seconds)
        ).toString();
    }

    @NonNull
    public static String formatSpeed(final double speed) {
        return SPEED_FORMATTER.format(speed);
    }

    @NonNull
    public static String formatPitch(final double pitch) {
        return PITCH_FORMATTER.format(pitch);
    }

    @NonNull
    public static String subtitleMimeTypesOf(@NonNull final MediaFormat format) {
        switch (format) {
            case VTT:
                return MimeTypes.TEXT_VTT;
            case TTML:
                return MimeTypes.APPLICATION_TTML;
            default:
                throw new IllegalArgumentException("Unrecognized mime type: " + format.name());
        }
    }

    @NonNull
    public static String captionLanguageOf(@NonNull final Context context,
                                           @NonNull final SubtitlesStream subtitles) {
        final String displayName = subtitles.getDisplayLanguageName();
        return displayName + (subtitles.isAutoGenerated()
                ? " (" + context.getString(R.string.caption_auto_generated) + ")" : "");
    }

    @NonNull
    public static String captionLanguageStemOf(@NonNull final String language) {
        if (!language.contains("(") || !language.contains(")")) {
            return language;
        }

        if (language.startsWith("(")) {
            // language text is right-to-left
            final String[] parts = language.split("\\)");
            return parts[parts.length - 1].trim();
        }

        return language.split("\\(")[0].trim();
    }

    @NonNull
    public static String resizeTypeOf(@NonNull final Context context,
                                      @ResizeMode final int resizeMode) {
        switch (resizeMode) {
            case AspectRatioFrameLayout.RESIZE_MODE_FIT:
                return context.getResources().getString(R.string.resize_fit);
            case AspectRatioFrameLayout.RESIZE_MODE_FILL:
                return context.getResources().getString(R.string.resize_fill);
            case AspectRatioFrameLayout.RESIZE_MODE_ZOOM:
                return context.getResources().getString(R.string.resize_zoom);
            case AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT:
            case AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH:
            default:
                throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode);
        }
    }

    @NonNull
    public static String cacheKeyOf(@NonNull final StreamInfo info,
                                    @NonNull final VideoStream videoStream) {
        String cacheKey = info.getUrl() + " " + videoStream.getId();

        final String resolution = videoStream.getResolution();
        final MediaFormat mediaFormat = videoStream.getFormat();
        if (resolution.equals(RESOLUTION_UNKNOWN) && mediaFormat == null) {
            // The hash code is only used in the cache key in the case when the resolution and the
            // media format are unknown
            cacheKey += " " + videoStream.hashCode();
        } else {
            if (mediaFormat != null) {
                cacheKey += " " + videoStream.getFormat().getName();
            }
            if (!resolution.equals(RESOLUTION_UNKNOWN)) {
                cacheKey += " " + resolution;
            }
        }

        return cacheKey;
    }

    @NonNull
    public static String cacheKeyOf(@NonNull final StreamInfo info,
                                    @NonNull final AudioStream audioStream) {
        String cacheKey = info.getUrl() + " " + audioStream.getId();

        final int averageBitrate = audioStream.getAverageBitrate();
        final MediaFormat mediaFormat = audioStream.getFormat();
        if (averageBitrate == UNKNOWN_BITRATE && mediaFormat == null) {
            // The hash code is only used in the cache key in the case when the resolution and the
            // media format are unknown
            cacheKey += " " + audioStream.hashCode();
        } else {
            if (mediaFormat != null) {
                cacheKey += " " + audioStream.getFormat().getName();
            }
            if (averageBitrate != UNKNOWN_BITRATE) {
                cacheKey += " " + averageBitrate;
            }
        }

        return cacheKey;
    }

    /**
     * Given a {@link StreamInfo} and the existing queue items,
     * provide the {@link SinglePlayQueue} consisting of the next video for auto queueing.
     * <p>
     * This method detects and prevents cycles by naively checking
     * if a candidate next video's url already exists in the existing items.
     * </p>
     * <p>
     * The first item in {@link StreamInfo#getRelatedItems()} is checked first.
     * If it is non-null and is not part of the existing items, it will be used as the next stream.
     * Otherwise, a random stream with non-repeating url will be selected
     * from the {@link StreamInfo#getRelatedItems()}. Non-stream items are ignored.
     * </p>
     *
     * @param info          currently playing stream
     * @param existingItems existing items in the queue
     * @return {@link SinglePlayQueue} with the next stream to queue
     */
    @Nullable
    public static PlayQueue autoQueueOf(@NonNull final StreamInfo info,
                                        @NonNull final List<PlayQueueItem> existingItems,
                                        boolean dontAutoQueueLong) {
        final Set<String> urls = new HashSet<>(existingItems.size());
        for (final PlayQueueItem item : existingItems) {
            urls.add(item.getUrl());
        }

        final List<InfoItem> relatedItems = info.getRelatedItems();
        if (Utils.isNullOrEmpty(relatedItems)) {
            return null;
        }
        if(info.getServiceId() == 5){
            if (info.isRoundPlayStream()) {
                return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0));
            }
        }

        // if YouTube, only enqueue the first one since others are not guaranteed to be related
        if (relatedItems.get(0) instanceof StreamInfoItem && info.getService().getServiceId() == ServiceList.YouTube.getServiceId()
                && !urls.contains(relatedItems.get(0).getUrl())) {
            if(!dontAutoQueueLong || ((StreamInfoItem) relatedItems.get(0)).getDuration() < 360){
                return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0));
            } else {
                return null;
            }
        }

        final List<StreamInfoItem> autoQueueItems = new ArrayList<>();
        for (final InfoItem item : relatedItems) {
            if (item instanceof StreamInfoItem && !urls.contains(item.getUrl())) {
                if (!dontAutoQueueLong || ((StreamInfoItem) item).getDuration() < 360) {
                    autoQueueItems.add((StreamInfoItem) item);
                }
            }
        }

        Collections.shuffle(autoQueueItems);
        return autoQueueItems.isEmpty()
                ? null : getAutoQueuedSinglePlayQueue(autoQueueItems.get(0));
    }

    ////////////////////////////////////////////////////////////////////////////
    // Settings Resolution
    ////////////////////////////////////////////////////////////////////////////

    public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) {
        return getPreferences(context)
                .getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), false);
    }

    public static boolean isVolumeGestureEnabled(@NonNull final Context context) {
        return getPreferences(context)
                .getBoolean(context.getString(R.string.volume_gesture_control_key), true);
    }

    public static boolean isBrightnessGestureEnabled(@NonNull final Context context) {
        return getPreferences(context)
                .getBoolean(context.getString(R.string.brightness_gesture_control_key), true);
    }

    public static boolean isFullscreenGestureEnabled(@NonNull final Context context) {
        return getPreferences(context)
                .getBoolean(context.getString(R.string.fullscreen_gesture_control_key), true);
    }

    public static boolean isPinchToZoomEnabled(@NonNull final Context context) {
        return getPreferences(context)
                .getBoolean(context.getString(R.string.pinch_to_zoom_key), false);
    }

    public static void setPinchToZoomEnabled(@NonNull final Context context,
                                              final boolean enabled) {
        getPreferences(context).edit()
                .putBoolean(context.getString(R.string.pinch_to_zoom_key), enabled).apply();
    }

    public static boolean isSwipeSeekGestureEnabled(@NonNull final Context context) {
        return getPreferences(context)
                .getBoolean(context.getString(R.string.swipe_seek_gesture_control_key), true);
    }

    public static boolean isPlaybackSpeedGestureEnabled(@NonNull final Context context) {
        return getPreferences(context)
                .getBoolean(context.getString(R.string.playback_speed_gesture_control_key), false);
    }

    public static boolean isStartMainPlayerFullscreenEnabled(@NonNull final Context context) {
        return getPreferences(context)
                .getBoolean(context.getString(R.string.start_main_player_fullscreen_key), false);
    }

    public static boolean isAutoQueueEnabled(@NonNull final Context context) {
        return getPreferences(context)
                .getBoolean(context.getString(R.string.auto_queue_key), false);
    }

    @MinimizeMode
    public static int getMinimizeOnExitAction(@NonNull final Context context) {
        final String action = getPreferences(context)
                .getString(context.getString(R.string.minimize_on_exit_key), "");
        if (action.equals(context.getString(R.string.minimize_on_exit_popup_key))) {
            return MINIMIZE_ON_EXIT_MODE_POPUP;
        } else if (action.equals(context.getString(R.string.minimize_on_exit_none_key))) {
            return MINIMIZE_ON_EXIT_MODE_NONE;
        } else {
            return MINIMIZE_ON_EXIT_MODE_BACKGROUND; // default
        }
    }

    @AutoplayType
    public static int getAutoplayType(@NonNull final Context context) {
        final String type = getPreferences(context).getString(
                context.getString(R.string.autoplay_key), "");
        if (type.equals(context.getString(R.string.autoplay_always_key))) {
            return AUTOPLAY_TYPE_ALWAYS;
        } else if (type.equals(context.getString(R.string.autoplay_never_key))) {
            return AUTOPLAY_TYPE_NEVER;
        } else {
            return AUTOPLAY_TYPE_WIFI; // default
        }
    }

    public static boolean isAutoplayAllowedByUser(@NonNull final Context context) {
        switch (PlayerHelper.getAutoplayType(context)) {
            case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER:
                return false;
            case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI:
                return !ListHelper.isMeteredNetwork(context);
            case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS:
            default:
                return true;
        }
    }

    @NonNull
    public static SeekParameters getSeekParameters(@NonNull final Context context) {
        return isUsingInexactSeek(context) ? SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT;
    }

    public static long getPreferredCacheSize() {
        return 64 * 1024 * 1024L;
    }

    public static long getPreferredFileSize() {
        return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
    }

    @NonNull
    public static ExoTrackSelection.Factory getQualitySelector() {
        return new AdaptiveTrackSelection.Factory(
                1000,
                AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
                AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
                AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION);
    }

    public static boolean isUsingDSP() {
        return true;
    }

    public static int getTossFlingVelocity() {
        return 2500;
    }

    @NonNull
    public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
        final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
                CaptioningManager.class);
        if (captioningManager == null || !captioningManager.isEnabled()) {
            return CaptionStyleCompat.DEFAULT;
        }

        return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
    }

    /**
     * Get scaling for captions based on system font scaling.
     * <p>Options:</p>
     * <ul>
     *     <li>Very small: 0.25f</li>
     *     <li>Small: 0.5f</li>
     *     <li>Normal: 1.0f</li>
     *     <li>Large: 1.5f</li>
     *     <li>Very large: 2.0f</li>
     * </ul>
     *
     * @param context Android app context
     * @return caption scaling
     */
    public static float getCaptionScale(@NonNull final Context context) {
        final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
                CaptioningManager.class);
        if (captioningManager == null || !captioningManager.isEnabled()) {
            return 1.0f;
        }

        return captioningManager.getFontScale();
    }

    /**
     * @param context the Android context
     * @return the screen brightness to use. A value less than 0 (the default) means to use the
     * preferred screen brightness
     */
    public static float getScreenBrightness(@NonNull final Context context) {
        final SharedPreferences sp = getPreferences(context);
        final long timestamp =
                sp.getLong(context.getString(R.string.screen_brightness_timestamp_key), 0);
        // Hypothesis: 4h covers a viewing block, e.g. evening.
        // External lightning conditions will change in the next
        // viewing block so we fall back to the default brightness
        if ((System.currentTimeMillis() - timestamp) > TimeUnit.HOURS.toMillis(4)) {
            return -1;
        } else {
            return sp.getFloat(context.getString(R.string.screen_brightness_key), -1);
        }
    }

    public static void setScreenBrightness(@NonNull final Context context,
                                           final float screenBrightness) {
        getPreferences(context).edit()
                .putFloat(context.getString(R.string.screen_brightness_key), screenBrightness)
                .putLong(context.getString(R.string.screen_brightness_timestamp_key),
                        System.currentTimeMillis())
                .apply();
    }

    public static boolean globalScreenOrientationLocked(final Context context) {
        // 1: Screen orientation changes using accelerometer
        // 0: Screen orientation is locked
        return android.provider.Settings.System.getInt(
                context.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 0;
    }

    public static int getProgressiveLoadIntervalBytes(@NonNull final Context context) {
        final String preferredIntervalBytes = getPreferences(context).getString(
                context.getString(R.string.progressive_load_interval_key),
                context.getString(R.string.progressive_load_interval_default_value));

        if (context.getString(R.string.progressive_load_interval_exoplayer_default_value)
                .equals(preferredIntervalBytes)) {
            return ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
        }
        // Keeping the same KiB unit used by ProgressiveMediaSource
        return Integer.parseInt(preferredIntervalBytes) * 1024;
    }

    ////////////////////////////////////////////////////////////////////////////
    // Private helpers
    ////////////////////////////////////////////////////////////////////////////

    @NonNull
    private static SharedPreferences getPreferences(@NonNull final Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context);
    }

    private static boolean isUsingInexactSeek(@NonNull final Context context) {
        return getPreferences(context)
                .getBoolean(context.getString(R.string.use_inexact_seek_key), false);
    }

    public static SinglePlayQueue getAutoQueuedSinglePlayQueue(
            final StreamInfoItem streamInfoItem) {
        final SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem);
        Objects.requireNonNull(singlePlayQueue.getItem()).setAutoQueued(true);
        return singlePlayQueue;
    }


    ////////////////////////////////////////////////////////////////////////////
    // Utils used by player
    ////////////////////////////////////////////////////////////////////////////

    public static PlayerService.PlayerType retrievePlayerTypeFromIntent(final Intent intent) {
        // If you want to open popup from the app just include Constants.POPUP_ONLY into an extra
        return PlayerService.PlayerType.values()[
                intent.getIntExtra(PLAYER_TYPE, PlayerService.PlayerType.VIDEO.ordinal())];
    }

    public static boolean isPlaybackResumeEnabled(final Player player) {
        return player.getPrefs().getBoolean(
                player.getContext().getString(R.string.enable_watch_history_key), true)
                && player.getPrefs().getBoolean(
                player.getContext().getString(R.string.enable_playback_resume_key), true);
    }

    @RepeatMode
    public static int nextRepeatMode(@RepeatMode final int repeatMode) {
        switch (repeatMode) {
            case REPEAT_MODE_OFF:
                return REPEAT_MODE_ONE;
            case REPEAT_MODE_ONE:
                return REPEAT_MODE_ALL;
            case REPEAT_MODE_ALL:
            default:
                return REPEAT_MODE_OFF;
        }
    }

    /**
     * Aspect ratios selectable in the player. Indices in both arrays match.
     */
    public static final String[] ASPECT_RATIO_LABELS = {"1:1", "4:3", "16:9", "18:9", "21:9"};
    public static final float[] ASPECT_RATIO_VALUES = {
            1.0f, 4.0f / 3.0f, 16.0f / 9.0f, 18.0f / 9.0f, 21.0f / 9.0f};

    public static String aspectRatioNameOf(final float aspectRatio) {
        for (int i = 0; i < ASPECT_RATIO_VALUES.length; i++) {
            if (Math.abs(ASPECT_RATIO_VALUES[i] - aspectRatio) < 0.001f) {
                return ASPECT_RATIO_LABELS[i];
            }
        }
        return String.format(Locale.US, "%.2f", aspectRatio);
    }

    /**
     * Parses a user-entered aspect ratio like {@code "16:9"}, {@code "16/9"} or {@code "1.78"}.
     *
     * @return the ratio as a float, or {@code 0} if the input could not be parsed
     */
    public static float parseAspectRatio(@Nullable final String input) {
        if (input == null) {
            return 0.0f;
        }
        final String trimmed = input.trim();
        try {
            final String[] parts = trimmed.split("[:/]");
            if (parts.length == 2) {
                final float width = Float.parseFloat(parts[0].trim());
                final float height = Float.parseFloat(parts[1].trim());
                if (width > 0 && height > 0) {
                    return width / height;
                }
            } else if (parts.length == 1) {
                final float ratio = Float.parseFloat(trimmed.replace(',', '.'));
                if (ratio > 0) {
                    return ratio;
                }
            }
        } catch (final NumberFormatException ignored) {
            // fall through to invalid
        }
        return 0.0f;
    }

    @ResizeMode
    public static int retrieveResizeModeFromPrefs(final Player player) {
        return player.getPrefs().getInt(player.getContext().getString(R.string.last_resize_mode),
                AspectRatioFrameLayout.RESIZE_MODE_FIT);
    }

    /**
     * Persists the given resize mode so it can be restored in a future session.
     */
    public static void saveResizeMode(final Player player,
                                      @ResizeMode final int resizeMode) {
        player.getPrefs().edit().putInt(
                player.getContext().getString(R.string.last_resize_mode), resizeMode).apply();
    }

    public static PlaybackParameters retrievePlaybackParametersFromPrefs(final Player player) {
        final float speed = player.getPrefs().getFloat(player.getContext().getString(
                R.string.playback_speed_key), player.getPlaybackSpeed());
        final float pitch = player.getPrefs().getFloat(player.getContext().getString(
                R.string.playback_pitch_key), player.getPlaybackPitch());
        return new PlaybackParameters(speed, pitch);
    }

    public static void savePlaybackParametersToPrefs(final Player player,
                                                     final float speed,
                                                     final float pitch,
                                                     final boolean skipSilence) {
        player.getPrefs().edit()
                .putFloat(player.getContext().getString(R.string.playback_speed_key), speed)
                .putFloat(player.getContext().getString(R.string.playback_pitch_key), pitch)
                .putBoolean(player.getContext().getString(R.string.playback_skip_silence_key),
                        skipSilence)
                .apply();
    }

    /**
     * @param player {@code screenWidth} and {@code screenHeight} must have been initialized
     * @return the popup starting layout params
     */
    @SuppressLint("RtlHardcoded")
    public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
            final Player player) {
        final boolean popupRememberSizeAndPos = true;
        final float defaultSize =
                player.getContext().getResources().getDimension(R.dimen.popup_default_width);
        final float popupWidth = popupRememberSizeAndPos
                ? player.getPrefs().getFloat(player.getContext().getString(
                R.string.popup_saved_width_key), defaultSize)
                : defaultSize;
        final float popupHeight = getMinimumVideoHeight(popupWidth);

        final WindowManager.LayoutParams popupLayoutParams = new WindowManager.LayoutParams(
                (int) popupWidth, (int) popupHeight,
                popupLayoutParamType(),
                IDLE_WINDOW_FLAGS,
                PixelFormat.TRANSLUCENT);
        popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;

        final int centerX = (int) (player.getScreenWidth() / 2f - popupWidth / 2f);
        final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
        popupLayoutParams.x = popupRememberSizeAndPos
                ? player.getPrefs().getInt(player.getContext().getString(
                R.string.popup_saved_x_key), centerX) : centerX;
        popupLayoutParams.y = popupRememberSizeAndPos
                ? player.getPrefs().getInt(player.getContext().getString(
                R.string.popup_saved_y_key), centerY) : centerY;

        return popupLayoutParams;
    }

    public static void savePopupPositionAndSizeToPrefs(final Player player) {
        if (player.getPopupLayoutParams() != null) {
            player.getPrefs().edit()
                    .putFloat(player.getContext().getString(R.string.popup_saved_width_key),
                            player.getPopupLayoutParams().width)
                    .putInt(player.getContext().getString(R.string.popup_saved_x_key),
                            player.getPopupLayoutParams().x)
                    .putInt(player.getContext().getString(R.string.popup_saved_y_key),
                            player.getPopupLayoutParams().y)
                    .apply();
        }
    }

    public static float getMinimumVideoHeight(final float width) {
        return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
    }

    @SuppressLint("RtlHardcoded")
    public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() {
        final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;

        final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
                popupLayoutParamType(),
                flags,
                PixelFormat.TRANSLUCENT);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            // Setting maximum opacity allowed for touch events to other apps for Android 12 and
            // higher to prevent non interaction when using other apps with the popup player
            closeOverlayLayoutParams.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER;
        }

        closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        closeOverlayLayoutParams.softInputMode =
                WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
        return closeOverlayLayoutParams;
    }

    public static int popupLayoutParamType() {
        return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
                ? WindowManager.LayoutParams.TYPE_PHONE
                : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
    }

    public static int retrieveSeekDurationFromPreferences(final Player player) {
        return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString(
                player.getContext().getString(R.string.seek_duration_key),
                player.getContext().getString(R.string.seek_duration_default_value))));
    }
}
