package com.perflyst.twire.fragments;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.media.AudioManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.media.session.MediaSessionCompat;
import android.transition.Transition;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.CheckedTextView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.DrawableRes;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.Fade;
import androidx.transition.TransitionManager;

import com.afollestad.materialdialogs.DialogAction;
import com.balysv.materialripple.MaterialRippleLayout;
import com.bumptech.glide.Glide;
import com.bumptech.glide.signature.ObjectKey;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.snackbar.Snackbar;
import com.perflyst.twire.R;
import com.perflyst.twire.activities.ChannelActivity;
import com.perflyst.twire.activities.stream.StreamActivity;
import com.perflyst.twire.adapters.PanelAdapter;
import com.perflyst.twire.chat.ChatManager;
import com.perflyst.twire.misc.FollowHandler;
import com.perflyst.twire.misc.ResizeHeightAnimation;
import com.perflyst.twire.misc.ResizeWidthAnimation;
import com.perflyst.twire.model.ChannelInfo;
import com.perflyst.twire.model.Quality;
import com.perflyst.twire.model.SleepTimer;
import com.perflyst.twire.service.DialogService;
import com.perflyst.twire.service.Service;
import com.perflyst.twire.service.Settings;
import com.perflyst.twire.tasks.GetLiveStreamURL;
import com.perflyst.twire.tasks.GetPanelsTask;
import com.perflyst.twire.tasks.GetStreamChattersTask;
import com.perflyst.twire.tasks.GetStreamViewersTask;
import com.perflyst.twire.tasks.GetVODStreamURL;
import com.rey.material.widget.ProgressView;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import biz.kasual.materialnumberpicker.MaterialNumberPicker;

public class StreamFragment extends Fragment implements Player.EventListener, PlaybackPreparer {
    private final int HIDE_ANIMATION_DELAY = 3000;
    private final int SNACKBAR_SHOW_DURATION = 4000;

    private final String LOG_TAG = getClass().getSimpleName();
    private final Handler delayAnimationHandler = new Handler(),
            progressHandler = new Handler(),
            fetchViewCountHandler = new Handler(),
            fetchChattersHandler = new Handler();
    public StreamFragmentListener streamFragmentCallback;
    public boolean chatOnlyViewVisible = false;
    public boolean isFullscreen = false;
    private boolean castingViewVisible = false,
            audioViewVisible = false,
            autoPlay = true,
            hasPaused = false,
            seeking = false;
    private ChannelInfo mChannelInfo;
    private String vodId;
    private HeadsetPlugIntentReceiver headsetIntentReceiver;
    private Settings settings;
    private SleepTimer sleepTimer;
    private LinkedHashMap<String, Quality> qualityURLs;
    private boolean isLandscape = false, previewInbackGround = false;
    private Runnable fetchViewCountRunnable;
    private PlayerView mVideoView;
    private ExoPlayer player;
    private MediaSource currentMediaSource;
    private Toolbar mToolbar;
    private RelativeLayout mControlToolbar;
    private ConstraintLayout mVideoWrapper;
    private ConstraintLayout mPlayPauseWrapper;
    private ImageView mPauseIcon,
            mPlayIcon,
            mQualityButton,
            mFullScreenButton,
            mPreview,
            mShowChatButton,
            mForward,
            mBackward;
    private SeekBar mProgressBar;
    private TextView mCurrentProgressView, castingTextView, mCurrentViewersView;
    private HashMap<String, TextView> QualityOptions = new HashMap<>();
    private AppCompatActivity mActivity;
    private Snackbar snackbar;
    private ProgressView mBufferingView;
    private BottomSheetDialog mQualityBottomSheet, mProfileBottomSheet;
    private CheckedTextView mAudioOnlySelector, mChatOnlySelector;
    private ViewGroup rootView;
    private MenuItem optionsMenuItem;
    private LinearLayout mQualityWrapper;
    private View mClickIntercepter;
    private final Runnable hideAnimationRunnable = () -> {
        if (getActivity() != null)
            hideVideoInterface();
    };
    private final Runnable progressRunnable = new Runnable() {
        @Override
        public void run() {
            if (player == null)
                return;

            if (player.isPlaying()) {
                if (currentProgress != player.getCurrentPosition())
                    mProgressBar.setProgress((int) player.getCurrentPosition());

                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
                    mBufferingView.stop();
                    delayHiding();
                    if (!previewInbackGround) {
                        hidePreview();
                    }
                }
            }
            progressHandler.postDelayed(this, 1000);
        }
    };
    private int originalCtrlToolbarPadding,
            originalMainToolbarPadding,
            vodLength = 0,
            currentProgress = 0,
            videoHeightBeforeChatOnly,
            fetchViewCountDelay = 1000 * 60, // A minute
            fetchChattersDelay = 1000 * 60; // 30 seco... Nah just kidding. Also a minute.
    private Integer triesForNextBest = 0;

    private static int totalVerticalInset;
    private boolean pictureInPictureEnabled;
    private MediaSessionCompat mediaSession;

    public static StreamFragment newInstance(Bundle args) {
        StreamFragment fragment = new StreamFragment();
        fragment.setArguments(args);
        return fragment;
    }

    /**
     * Gets a Rect representing the usable area of the screen
     *
     * @return A Rect representing the usable area of the screen
     */
    public static Rect getScreenRect(Activity activity) {
        if (activity != null) {
            Display display = activity.getWindowManager().getDefaultDisplay();
            DisplayMetrics metrics = new DisplayMetrics();
            Point size = new Point();
            int width, height;

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode()) {
                    display.getMetrics(metrics);
                } else {
                    display.getRealMetrics(metrics);
                }

                width = metrics.widthPixels;
                height = metrics.heightPixels;
            } else {
                display.getSize(size);
                width = size.x;
                height = size.y;
            }

            return new Rect(0, 0, Math.min(width, height), Math.max(width, height) - totalVerticalInset);
        }

        return new Rect();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        Bundle args = getArguments();
        setHasOptionsMenu(true);
        settings = new Settings(getActivity());

        if (args != null) {
            mChannelInfo = args.getParcelable(getString(R.string.stream_fragment_streamerInfo));
            vodId = args.getString(getString(R.string.stream_fragment_vod_id));
            vodLength = args.getInt(getString(R.string.stream_fragment_vod_length));
            autoPlay = args.getBoolean(getString(R.string.stream_fragment_autoplay));

            settings.setVodLength(vodId, vodLength);
        }

        final View mRootView = inflater.inflate(R.layout.fragment_stream, container, false);
        mRootView.requestLayout();

        // If the user has been in FULL SCREEN mode and presses the back button, we want to change the orientation to portrait.
        // As soon as the orientation has change we don't want to force the user to will be in portrait, so we "release" the request.
        if (getActivity().getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
            getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
        }

        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
            isLandscape = true;
        }

        //  If no streamer info is available we cant show the stream.
        if (mChannelInfo == null) {
            if (getActivity() != null) {
                getActivity().finish();
            }
            return rootView;
        }

        rootView = (ViewGroup) mRootView;
        mToolbar = mRootView.findViewById(R.id.main_toolbar);
        mControlToolbar = mRootView.findViewById(R.id.control_toolbar_wrapper);
        mVideoWrapper = mRootView.findViewById(R.id.video_wrapper);
        mVideoView = mRootView.findViewById(R.id.VideoView);
        mPlayPauseWrapper = mRootView.findViewById(R.id.play_pause_wrapper);
        mPlayIcon = mRootView.findViewById(R.id.ic_play);
        mPauseIcon = mRootView.findViewById(R.id.ic_pause);
        mPreview = mRootView.findViewById(R.id.preview);
        mQualityButton = mRootView.findViewById(R.id.settings_icon);
        mFullScreenButton = mRootView.findViewById(R.id.fullscreen_icon);
        mShowChatButton = mRootView.findViewById(R.id.show_chat_button);
        mForward = mRootView.findViewById(R.id.forward);
        mBackward = mRootView.findViewById(R.id.backward);
        mCurrentProgressView = mRootView.findViewById(R.id.currentProgess);
        castingTextView = mRootView.findViewById(R.id.chromecast_text);
        mProgressBar = mRootView.findViewById(R.id.progressBar);
        mBufferingView = mRootView.findViewById(R.id.circle_progress);
        mCurrentViewersView = mRootView.findViewById(R.id.txtViewViewers);
        mActivity = ((AppCompatActivity) getActivity());
        mClickIntercepter = mRootView.findViewById(R.id.click_intercepter);
        View mCurrentViewersWrapper = mRootView.findViewById(R.id.viewers_wrapper);

        setupToolbar();
        setupSpinner();
        setupProfileBottomSheet();
        setupLandscapeChat();
        setupShowChatButton();

        if (savedInstanceState == null)
            setPreviewAndCheckForSharedTransition();

        mFullScreenButton.setOnClickListener(v -> toggleFullscreen());
        mPlayPauseWrapper.setOnClickListener(v -> {
            if (mPlayPauseWrapper.getAlpha() < 0.5f) {
                return;
            }

            try {
                if (player.isPlaying()) {
                    pauseStream();
                } else if (!player.isPlaying()) {
                    resumeStream();
                }
            } catch (Exception e) {
                e.printStackTrace();
                startStreamWithQuality(settings.getPrefStreamQuality());
            }
        });

        mVideoWrapper.setOnClickListener(v -> {
            delayAnimationHandler.removeCallbacks(hideAnimationRunnable);
            if (isVideoInterfaceShowing()) {
                hideVideoInterface();
                if (isDeviceBelowKitkat())
                    setAndroidUiMode();
            } else {
                showVideoInterface();

                // Show the navigation bar
                if (isLandscape && settings.getStreamPlayerShowNavigationBar() && Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
                    View decorView = getActivity().getWindow().getDecorView();
                    decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                            | View.SYSTEM_UI_FLAG_FULLSCREEN // Hide Status bar
                            | View.SYSTEM_UI_FLAG_IMMERSIVE);
                }

                if (player.isPlaying()) {
                    delayHiding();
                }

                Handler h = new Handler();
                h.postDelayed(this::setAndroidUiMode, HIDE_ANIMATION_DELAY);
            }
        });

        initializePlayer();

        mRootView.setOnSystemUiVisibilityChangeListener(
                visibility -> {
                    if (visibility == 0) {
                        showVideoInterface();
                        delayHiding();
                        Handler h = new Handler();
                        h.postDelayed(this::setAndroidUiMode, HIDE_ANIMATION_DELAY);
                    }
                }
        );


        int seekButtonVisibility = vodId == null ? View.INVISIBLE : View.VISIBLE;
        mForward.setVisibility(seekButtonVisibility);
        mBackward.setVisibility(seekButtonVisibility);

        if (vodId == null) {
            View mTimeController = mRootView.findViewById(R.id.time_controller);
            mTimeController.setVisibility(View.INVISIBLE);

            if (args != null && args.containsKey(getString(R.string.stream_fragment_viewers)) && settings.getStreamPlayerShowViewerCount()) {
                mCurrentViewersView.setText("" + args.getInt(getString(R.string.stream_fragment_viewers)));
                startFetchingViewers();
            } else {
                mCurrentViewersWrapper.setVisibility(View.GONE);
            }
        } else {
            mCurrentViewersWrapper.setVisibility(View.GONE);

            mForward.setOnClickListener(v -> {
                seeking = true;
                mProgressBar.setProgress(currentProgress + 10000);
                seeking = false;
                ChatManager.updateVodProgress(currentProgress, false);
            });

            mBackward.setOnClickListener(v -> {
                seeking = true;
                mProgressBar.setProgress(currentProgress - 10000);
                seeking = false;
                streamFragmentCallback.onSeek();
                ChatManager.updateVodProgress(currentProgress, true);
            });

            mCurrentProgressView.setOnClickListener(v -> showSeekDialog());

            ChatManager.updateVodProgress(ChatManager.VOD_LOADING, true);

            TextView maxProgress = mRootView.findViewById(R.id.maxProgress);
            maxProgress.setText(Service.calculateTwitchVideoLength(vodLength));

            mProgressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    if (progress == vodLength) {
                        pauseStream();
                    }

                    if (vodId != null && !seeking && !fromUser) {
                        ChatManager.updateVodProgress(progress, false);
                    }

                    if ((fromUser || seeking) && !audioViewVisible) {
                        player.seekTo(progress);
                        showVideoInterface();

                        if (progress > 0) {
                            settings.setVodProgress(vodId, progress / 1000);
                        }
                    }
                    currentProgress = progress;
                    mCurrentProgressView.setText(Service.calculateTwitchVideoLength(currentProgress / 1000));
                }

                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {
                    seeking = true;
                }

                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {
                    seeking = false;
                    delayHiding();

                    if (vodId != null) {
                        ChatManager.updateVodProgress(currentProgress, true);
                        streamFragmentCallback.onSeek();
                    }
                }
            });
            seeking = true;
            mProgressBar.setMax(vodLength * 1000);
            seeking = false;

            checkVodProgress();
        }

        keepScreenOn();

        if (autoPlay || vodId != null) {
            startStreamWithQuality(settings.getPrefStreamQuality());
        }

        headsetIntentReceiver = new HeadsetPlugIntentReceiver();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            getActivity().registerReceiver(headsetIntentReceiver, new IntentFilter(AudioManager.ACTION_HEADSET_PLUG));
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            mRootView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    DisplayCutout displayCutout = getDisplayCutout();
                    if (displayCutout != null) {
                        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
                            totalVerticalInset = displayCutout.getSafeInsetLeft() + displayCutout.getSafeInsetRight();
                        } else {
                            totalVerticalInset = displayCutout.getSafeInsetTop() + displayCutout.getSafeInsetBottom();
                        }

                        setVideoViewLayout();
                        setupLandscapeChat();
                        streamFragmentCallback.refreshLayout();
                    }
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                }
            });
        }

        return mRootView;
    }

    @RequiresApi(Build.VERSION_CODES.P)
    private DisplayCutout getDisplayCutout() {
        Activity activity = getActivity();
        if (activity != null) {
            WindowInsets windowInsets = activity.getWindow().getDecorView().getRootWindowInsets();
            if (windowInsets != null) {
                return windowInsets.getDisplayCutout();
            }
        }

        return null;
    }

    private void initializePlayer() {
        if (player == null) {
            player = new SimpleExoPlayer.Builder(getContext()).build();
            player.addListener(this);
            mVideoView.setPlayer(player);
            mVideoView.setPlaybackPreparer(this);

            if (currentMediaSource != null)
                player.prepare(currentMediaSource);

            mediaSession = new MediaSessionCompat(getContext(), getContext().getPackageName());
            MediaSessionConnector mediaSessionConnector = new MediaSessionConnector(mediaSession);
            mediaSessionConnector.setPlayer(player);
            mediaSession.setActive(true);

            progressHandler.postDelayed(progressRunnable, 1000);
        }
    }

    private void releasePlayer() {
        if (player != null) {
            mediaSession.release();

            player.release();
            player = null;
        }
    }

    @Override
    public void preparePlayback() {
        player.retry();
    }

    @Override
    public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
        if (playbackState == Player.STATE_READY) {
            mBufferingView.stop();
            hideVideoInterface();
            delayHiding();

            Log.d(LOG_TAG, "Render Start");
            if (!previewInbackGround) {
                hidePreview();
            }
        } else if (playbackState == Player.STATE_BUFFERING) {
            mBufferingView.start();
            delayAnimationHandler.removeCallbacks(hideAnimationRunnable);
            showVideoInterface();

            Log.d(LOG_TAG, "Render stop. Buffering start");
        }
    }

    @Override
    public void onPlayerError(ExoPlaybackException exception) {
        Log.e(LOG_TAG, "Something went wrong playing the stream for " + mChannelInfo.getDisplayName() + " - Exception: " + exception);

        playbackFailed();
    }

    /**
     * Hides the preview image and updates the state
     */
    private void hidePreview() {
        mPreview.setVisibility(View.INVISIBLE);
        previewInbackGround = true;
    }

    public void backPressed() {
        mVideoView.setVisibility(View.INVISIBLE);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        // Checks the orientation of the screen
        if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            isLandscape = true;
        } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
            isLandscape = false;
        }

        checkShowChatButtonVisibility();
        updateUI();
    }

    @Override
    public void onStart() {
        super.onStart();
        if (Util.SDK_INT > 23) {
            initializePlayer();
        }
    }

    @Override
    public void onResume() {
        super.onResume();

        if (Util.SDK_INT <= 23 || player == null) {
            initializePlayer();
        }

        originalMainToolbarPadding = mToolbar.getPaddingRight();
        originalCtrlToolbarPadding = mControlToolbar.getPaddingRight();

        if (audioViewVisible && !isAudioOnlyModeEnabled()) {
            disableAudioOnlyView();
            startStreamWithQuality(settings.getPrefStreamQuality());
        } else if (!castingViewVisible && !audioViewVisible && hasPaused && settings.getStreamPlayerAutoContinuePlaybackOnReturn()) {
            startStreamWithQuality(settings.getPrefStreamQuality());
        }

        registerAudioOnlyDelegate();

        if (!chatOnlyViewVisible) {
            showVideoInterface();
            updateUI();
        }

        player.seekTo(currentProgress);
    }

    @Override
    public void onPause() {
        super.onPause();

        Log.d(LOG_TAG, "Stream Fragment paused");
        if (pictureInPictureEnabled)
            return;

        hasPaused = true;

        if (mQualityBottomSheet != null)
            mQualityBottomSheet.dismiss();

        if (mProfileBottomSheet != null)
            mProfileBottomSheet.dismiss();

        if (Util.SDK_INT <= 23) {
            releasePlayer();
        }

        ChatManager.setPreviousProgress();
    }

    @Override
    public void onStop() {
        Log.d(LOG_TAG, "Stream Fragment Stopped");
        super.onStop();

        mBufferingView.stop();

        if (!castingViewVisible && !audioViewVisible) {
            pauseStream();
        }

        if (vodId != null) {
            settings.setVodProgress(vodId, currentProgress / 1000);
            settings.setVodLength(vodId, vodLength);
            Log.d(LOG_TAG, "Saving Current progress: " + currentProgress);
        }

        if (Util.SDK_INT > 23) {
            releasePlayer();
        }
    }

    @Override
    public void onDestroy() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getActivity() != null) {
            getActivity().unregisterReceiver(headsetIntentReceiver);
        }
        Log.d(LOG_TAG, "Destroying");
        if (fetchViewCountRunnable != null) {
            fetchViewCountHandler.removeCallbacks(fetchViewCountRunnable);
        }

        progressHandler.removeCallbacks(progressRunnable);
        super.onDestroy();
    }

    private void startFetchingCurrentChatters() {
        Runnable fetchChattersRunnable = new Runnable() {
            @Override
            public void run() {
                GetStreamChattersTask task = new GetStreamChattersTask(
                        new GetStreamChattersTask.GetStreamChattersTaskDelegate() {
                            @Override
                            public void onChattersFetched(ArrayList<String> chatters) {

                            }

                            @Override
                            public void onChattersFetchFailed() {

                            }
                        }, mChannelInfo.getStreamerName()
                );

                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

                if (!StreamFragment.this.isDetached()) {
                    fetchChattersHandler.postDelayed(this, fetchChattersDelay);
                }
            }
        };

        fetchChattersHandler.post(fetchChattersRunnable);
    }

    /**
     * Starts fetching current viewers for the current stream
     */
    private void startFetchingViewers() {
        fetchViewCountRunnable = new Runnable() {
            @Override
            public void run() {
                GetStreamViewersTask task = new GetStreamViewersTask(
                        new GetStreamViewersTask.GetStreamViewersTaskDelegate() {
                            @Override
                            public void onViewersFetched(Integer currentViewers) {
                                try {
                                    Log.d(LOG_TAG, "Fetching viewers");

                                    mCurrentViewersView.setText("" + currentViewers);
                                } catch (Exception e) {
                                    e.printStackTrace();
                                }
                            }

                            @Override
                            public void onViewersFetchFailed() {
                                // WELP
                            }
                        }, mChannelInfo.getUserId()
                );

                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

                if (!StreamFragment.this.isDetached()) {
                    fetchViewCountHandler.postDelayed(this, fetchViewCountDelay);
                }
            }
        };


        fetchViewCountHandler.post(fetchViewCountRunnable);
    }

    /**
     * Sets up the show chat button.
     * Sets the correct visibility and the onclicklistener
     */
    private void setupShowChatButton() {

        checkShowChatButtonVisibility();
        mShowChatButton.setOnClickListener(view -> {
            if (!isVideoInterfaceShowing()) {
                showVideoInterface();
                delayHiding();
            }

            if (view.getRotation() == 0f) {
                showLandscapeChat();
            } else {
                hideLandscapeChat();
            }
        });
    }

    /**
     * Sets the correct visibility of the show chat button.
     * If the screen is in landscape it is show, else it is shown
     */
    private void checkShowChatButtonVisibility() {
        if (isLandscape && settings.isChatInLandscapeEnabled() && !pictureInPictureEnabled) {
            mShowChatButton.setVisibility(View.VISIBLE);
        } else {
            mShowChatButton.setVisibility(View.GONE);
        }
    }

    private void profileButtonClicked() {
        mProfileBottomSheet.show();
    }

    private void sleepButtonClicked() {
        if (sleepTimer == null) {
            sleepTimer = new SleepTimer(new SleepTimer.SleepTimerDelegate() {
                @Override
                public void onTimesUp() {
                    stopAudioOnly();
                    pauseStream();
                }

                @Override
                public void onStart(String message) {
                    showSnackbar(message);
                }

                @Override
                public void onStop(String message) {
                    showSnackbar(message);
                }
            }, getContext());
        }

        sleepTimer.show(getActivity());
    }

    private void showSeekDialog() {
        DialogService.getSeekDialog(getActivity(), (dialog, which) -> {
                    if (which == DialogAction.NEGATIVE)
                        return;

                    View customView = dialog.getCustomView();
                    MaterialNumberPicker hourPicker = customView.findViewById(R.id.hour_picker);
                    MaterialNumberPicker minutePicker = customView.findViewById(R.id.minute_picker);
                    MaterialNumberPicker secondPicker = customView.findViewById(R.id.second_picker);

                    seeking = true;
                    mProgressBar.setProgress((hourPicker.getValue() * 3600 + minutePicker.getValue() * 60 + secondPicker.getValue()) * 1000);
                    seeking = false;
                    streamFragmentCallback.onSeek();
                    ChatManager.updateVodProgress(currentProgress, true);
                },
                currentProgress / 1000,
                vodLength)
                .show();
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        optionsMenuItem = menu.findItem(R.id.menu_item_options);
        optionsMenuItem.setVisible(false);
        optionsMenuItem.setOnMenuItemClickListener(menuItem -> {
            if (mQualityButton != null) {
                mQualityButton.performClick();
            }
            return true;
        });
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (!isVideoInterfaceShowing()) {
            mVideoWrapper.performClick();
            return true;
        }

        switch (item.getItemId()) {
            case R.id.menu_item_sleep:
                sleepButtonClicked();
                return true;
            case R.id.menu_item_profile:
                profileButtonClicked();
                return true;
            case R.id.menu_item_external:
                playWithExternalPlayer();
                return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void setupLandscapeChat() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && settings.isChatLandscapeSwipable() && settings.isChatInLandscapeEnabled()) {
            final int width = getScreenRect(getActivity()).height();

            View.OnTouchListener touchListener = new View.OnTouchListener() {
                private int downPosition = width;
                private int widthOnDown = width;

                public boolean onTouch(View view, MotionEvent event) {
                    if (isLandscape) {
                        final int X = (int) event.getRawX();
                        switch (event.getAction() & MotionEvent.ACTION_MASK) {
                            case MotionEvent.ACTION_DOWN:
                                ConstraintLayout.LayoutParams lParams = (ConstraintLayout.LayoutParams) mVideoWrapper.getLayoutParams();
                                if (lParams.width > 0)
                                    widthOnDown = lParams.width;

                                downPosition = (int) event.getRawX();
                                break;
                            case MotionEvent.ACTION_UP:
                                int upPosition = (int) event.getRawX();
                                int deltaPosition = upPosition - downPosition;

                                if (deltaPosition < 20 && deltaPosition > -20) {
                                    return false;
                                }

                                if (upPosition < downPosition) {
                                    showLandscapeChat();
                                } else {
                                    hideLandscapeChat();
                                }

                                break;
                            case MotionEvent.ACTION_MOVE:
                                ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) mVideoWrapper.getLayoutParams();
                                int newWidth;

                                if (X > downPosition) { // Swiping right
                                    newWidth = widthOnDown + (X - downPosition);
                                } else { // Swiping left
                                    newWidth = widthOnDown - (downPosition - X);
                                }

                                layoutParams.width = Math.max(Math.min(newWidth, width), width - getLandscapeChatTargetWidth());

                                mVideoWrapper.setLayoutParams(layoutParams);
                                break;

                        }
                        rootView.invalidate();
                    }
                    return false;
                }
            };

            mVideoWrapper.setOnTouchListener(touchListener);
            mClickIntercepter.setOnTouchListener(touchListener);
        }
    }

    /**
     * Show the landscape chat with an animation that changes the width of the videoview wrapper.
     * The ShowChatButton is also rotated
     */
    private void showLandscapeChat() {
        int width = getScreenRect(getActivity()).height();
        ResizeWidthAnimation resizeWidthAnimation = new ResizeWidthAnimation(mVideoWrapper, (width - getLandscapeChatTargetWidth()));
        resizeWidthAnimation.setDuration(250);
        mVideoWrapper.startAnimation(resizeWidthAnimation);
        mShowChatButton.animate().rotation(180f).start();
    }

    /**
     * hides the landscape chat with an animation that changes the width of the videoview wrapper to the width of the screen.
     * The ShowChatButton is also rotated
     */
    private void hideLandscapeChat() {
        int width = getScreenRect(getActivity()).height();
        ResizeWidthAnimation resizeWidthAnimation = new ResizeWidthAnimation(mVideoWrapper, width);
        resizeWidthAnimation.setDuration(250);
        mVideoWrapper.startAnimation(resizeWidthAnimation);
        mShowChatButton.animate().rotation(0f).start();
    }

    private int getLandscapeChatTargetWidth() {
        return (int) (getScreenRect(getActivity()).height() * (settings.getChatLandscapeWidth() / 100.0));
    }

    private void initCastingView() {
        castingViewVisible = true;
        //auto.setVisibility(View.GONE); // Auto does not work on chromecast
        mVideoView.setVisibility(View.INVISIBLE);
        mBufferingView.setVisibility(View.GONE);
        previewInbackGround = false;
        castingTextView.setVisibility(View.VISIBLE);
        //castingTextView.setText(getString(R.string.stream_chromecast_connecting));
        showVideoInterface();
    }

    private void disableCastingView() {
        castingViewVisible = false;
        //auto.setVisibility(View.VISIBLE);
        mVideoView.setVisibility(View.VISIBLE);
        Service.bringToBack(mPreview);
        mBufferingView.setVisibility(View.VISIBLE);
        previewInbackGround = true;
        castingTextView.setVisibility(View.INVISIBLE);
        showVideoInterface();
    }

    private String getBestCastQuality(Map<String, Quality> castQualities, String quality, Integer numberOfTries) {
        if (numberOfTries > GetLiveStreamURL.CAST_QUALITIES.length - 1) {
            return null;
        }

        if (quality.equals(GetLiveStreamURL.QUALITY_AUTO) || quality.equals(GetLiveStreamURL.QUALITY_AUDIO_ONLY)) {
            quality = GetLiveStreamURL.QUALITY_MEDIUM;
        }

        if (castQualities.containsKey(quality)) {
            return quality;
        } else {
            numberOfTries++;
            List<String> qualityList = Arrays.asList(GetLiveStreamURL.CAST_QUALITIES);
            int next = qualityList.indexOf(quality) - 1;
            if (next < 0) {
                quality = GetLiveStreamURL.QUALITY_SOURCE;
            } else {
                quality = qualityList.get(next);
            }
            return getBestCastQuality(castQualities, quality, numberOfTries);
        }
    }

    /**
     * Checks if the activity was started with a shared view in high API levels.
     */
    private void setPreviewAndCheckForSharedTransition() {
        final Intent intent = getActivity().getIntent();
        if (intent.hasExtra(getString(R.string.stream_preview_url))) {
            String imageUrl = intent.getStringExtra(getString(R.string.stream_preview_url));

            if (imageUrl == null || imageUrl.isEmpty()) {
                return;
            }

            Glide.with(getContext())
                    .asBitmap()
                    .load(imageUrl)
                    .signature(new ObjectKey(System.currentTimeMillis() / TimeUnit.MINUTES.toMillis(5))) // Refresh preview images every 5 minutes
                    .into(mPreview);
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && intent.getBooleanExtra(getString(R.string.stream_shared_transition), false)) {
            mPreview.setTransitionName(getString(R.string.stream_preview_transition));

            final View[] viewsToHide = {mVideoView, mToolbar, mControlToolbar};
            for (View view : viewsToHide) {
                view.setVisibility(View.INVISIBLE);
            }

            getActivity().getWindow().getEnterTransition().addListener(new Transition.TransitionListener() {

                @Override
                public void onTransitionEnd(Transition transition) {
                    TransitionManager.beginDelayedTransition(
                            mVideoWrapper,
                            new Fade()
                                    .setDuration(340)
                                    .excludeTarget(mVideoView, true)
                                    .excludeTarget(mPreview, true)
                    );

                    for (View view : viewsToHide) {
                        view.setVisibility(View.VISIBLE);
                    }
                }

                @Override
                public void onTransitionCancel(Transition transition) {
                    onTransitionEnd(transition);
                }

                public void onTransitionStart(Transition transition) {
                }

                public void onTransitionPause(Transition transition) {
                }

                public void onTransitionResume(Transition transition) {
                }
            });
        }

    }

    /**
     * Checks if the user is currently in progress of watching a VOD. If so seek forward to where the user left off.
     */
    private void checkVodProgress() {
        if (vodId != null) {
            if (currentProgress == 0) {
                currentProgress = settings.getVodProgress(vodId) * 1000;
                ChatManager.updateVodProgress(currentProgress, true);
                player.seekTo(currentProgress);
                Log.d(LOG_TAG, "Current progress: " + currentProgress);
            } else {
                ChatManager.updateVodProgress(currentProgress, false);
                player.seekTo(currentProgress);
                Log.d(LOG_TAG, "Seeking to " + currentProgress);
            }
        }
    }

    /**
     * Call to make sure the UI is shown correctly
     */
    private void updateUI() {
        setAndroidUiMode();
        keepControlIconsInView();
        setVideoViewLayout();
    }

    /**
     * This makes sure that the System UI automatically hides when the user changes focus by opening the navigation drawer.
     *
     * @param hasFocus
     */
    public void onWindowFocusChanged(boolean hasFocus) {
        Log.d(LOG_TAG, "WindowFocusChanged to " + hasFocus + " - isLandscape " + isLandscape);
        setAndroidUiMode();
    }

    /**
     * Sets the System UI visibility so that the status- and navigation bar automatically hides if the app is current in fullscreen or in landscape.
     * But they will automatically show when the user touches the screen.
     */
    private void setAndroidUiMode() {
        if (getActivity() == null) {
            return;
        }

        View decorView = getActivity().getWindow().getDecorView();
        if (isLandscape || isFullscreen) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // Hide navigation bar
                        | View.SYSTEM_UI_FLAG_FULLSCREEN // Hide Status bar
                        | View.SYSTEM_UI_FLAG_IMMERSIVE);
            } else {
                decorView.setSystemUiVisibility(
                        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // Hide navigation bar
                                | View.SYSTEM_UI_FLAG_FULLSCREEN // Hide Status bar

                );
            }

        } else {
            decorView.setSystemUiVisibility(0); // Remove all flags.
        }
    }

    private void setVideoViewLayout() {
        ViewGroup.LayoutParams layoutParams = rootView.getLayoutParams();
        layoutParams.height = isLandscape ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT;

        ConstraintLayout.LayoutParams layoutWrapper = (ConstraintLayout.LayoutParams) mVideoWrapper.getLayoutParams();
        if (isLandscape && !pictureInPictureEnabled) {
            layoutWrapper.width = mShowChatButton.getRotation() == 0 ? ConstraintLayout.LayoutParams.MATCH_PARENT : getScreenRect(getActivity()).height() - getLandscapeChatTargetWidth();
        } else {
            layoutWrapper.width = ConstraintLayout.LayoutParams.MATCH_PARENT;
        }
        mVideoWrapper.setLayoutParams(layoutWrapper);

        AspectRatioFrameLayout contentFrame = mVideoWrapper.findViewById(R.id.exo_content_frame);
        if (isLandscape) {
            contentFrame.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
        } else {
            contentFrame.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH);
        }
    }

    /**
     * Delays the hiding for the Video control interface.
     */
    private void delayHiding() {
        delayAnimationHandler.postDelayed(hideAnimationRunnable, HIDE_ANIMATION_DELAY);
    }

    /**
     * Checks if the video interface is fully showing
     *
     * @return
     */
    public boolean isVideoInterfaceShowing() {
        return mControlToolbar.getAlpha() == 1f;
    }

    /**
     * Hides the video control interface with animations
     */
    private void hideVideoInterface() {
        if (mToolbar != null && !audioViewVisible && !chatOnlyViewVisible) {
            mToolbar.animate().alpha(0f).setInterpolator(new AccelerateDecelerateInterpolator()).start();
            mControlToolbar.animate().alpha(0f).setInterpolator(new AccelerateDecelerateInterpolator()).start();
            mPlayPauseWrapper.animate().alpha(0f).setInterpolator(new AccelerateDecelerateInterpolator()).start();
            mShowChatButton.animate().alpha(0f).setInterpolator(new AccelerateDecelerateInterpolator()).start();
            mForward.animate().alpha(0f).setInterpolator(new AccelerateDecelerateInterpolator()).start();
            mBackward.animate().alpha(0f).setInterpolator(new AccelerateDecelerateInterpolator()).start();
            changeVideoControlClickablity(false);
        }
    }

    /**
     * Shows the video control interface with animations
     */
    private void showVideoInterface() {
        int MaintoolbarY = 0, CtrlToolbarY = 0;
        if ((isFullscreen || isLandscape) && isDeviceBelowKitkat()) {
            MaintoolbarY = getStatusBarHeight();
        }
        if ((isFullscreen || isLandscape) && Service.isTablet(getContext()) && isDeviceBelowKitkat()) {
            CtrlToolbarY = getNavigationBarHeight();
        }

        mControlToolbar.setTranslationY(-CtrlToolbarY);
        mControlToolbar.animate().alpha(1f).start();
        mToolbar.setTranslationY(MaintoolbarY);
        mToolbar.animate().alpha(1f).start();
        mPlayPauseWrapper.animate().alpha(1f).setInterpolator(new AccelerateDecelerateInterpolator()).start();
        mShowChatButton.animate().alpha(1f).setInterpolator(new AccelerateDecelerateInterpolator()).start();
        mForward.animate().alpha(1f).setInterpolator(new AccelerateDecelerateInterpolator()).start();
        mBackward.animate().alpha(1f).setInterpolator(new AccelerateDecelerateInterpolator()).start();
        changeVideoControlClickablity(true);
    }

    private void changeVideoControlClickablity(boolean clickable) {
        mClickIntercepter.setVisibility(clickable ? View.GONE : View.VISIBLE);
        mClickIntercepter.setOnClickListener(view -> mVideoWrapper.performClick());
    }

    /**
     * Keeps the rightmost icons on the toolbars in view when the device is in landscape.
     * Otherwise the icons would be covered my the navigationbar
     */
    private void keepControlIconsInView() {
        if (isDeviceBelowKitkat() || settings.getStreamPlayerShowNavigationBar()) {
            int ctrlPadding = originalCtrlToolbarPadding;
            int mainPadding = originalMainToolbarPadding;
            int delta = getNavigationBarHeight();

            if ((isFullscreen || isLandscape) && !Service.isTablet(getContext())) {
                ctrlPadding += delta;
                mainPadding += delta;
            }

            mShowChatButton.setPadding(0, 0, ctrlPadding, 0);
            mToolbar.setPadding(0, 0, mainPadding, 0);
            mControlToolbar.setPadding(0, 0, ctrlPadding, 0);
        }
    }

    /**
     * Returns the height of the statusbar
     */
    private int getStatusBarHeight() {
        int result = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    /**
     * Returns the height of the navigation bar.
     * If the device doesn't have a navigaion bar (Such as Samsung Galaxy devices) the height is 0
     */
    private int getNavigationBarHeight() {
        Resources resources = getResources();
        int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
        if (resourceId > 0) {
            return resources.getDimensionPixelSize(resourceId);
        }
        return 0;
    }

    /**
     * If the device isn't currently in fullscreen a request is sent to turn the device into landscape.
     * Otherwise if the device is in fullscreen then is releases the lock by requesting for an unspecified orientation
     * After and update to the VideoView layout is requested.
     */
    public void toggleFullscreen() {
        isFullscreen = !isFullscreen;
        if (isFullscreen) {
            getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
        } else {
            getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        }
        updateFullscreenButtonState();
        setVideoViewLayout();
    }

    /**
     * Sets the icon drawable of the fullscreen button depending on the current state.
     * If the app is currently in full screen an "exit fullscreen" icon will appear,
     * else and "enter fullscreen" icon will.
     */
    private void updateFullscreenButtonState() {
        if (isFullscreen) {
            mFullScreenButton.setImageResource(R.drawable.ic_fullscreen_exit_24dp);
        } else {
            mFullScreenButton.setImageResource(R.drawable.ic_fullscreen_24dp);
        }
    }

    /**
     * Pauses and stops the playback of the Video Stream
     */
    private void pauseStream() {
        Log.d(LOG_TAG, "Chat, pausing stream");
        showPlayIcon();

        delayAnimationHandler.removeCallbacks(hideAnimationRunnable);

        if (isAudioOnlyModeEnabled()) {
            Log.d(LOG_TAG, "Pausing audio");
        } else if (player != null) {
            player.setPlayWhenReady(false);
        }
        releaseScreenOn();
    }

    /**
     * Goes forward to live and starts playback of the VideoView
     */
    private void resumeStream() {
        showPauseIcon();

        if (isAudioOnlyModeEnabled()) {
        } else {
            if (vodId == null) {
                player.seekToDefaultPosition(); // Go forward to live
            }

            player.setPlayWhenReady(true);
        }

        keepScreenOn();
    }

    /**
     * Tries playing stream with a quality.
     * If the given quality doesn't exist for the stream the try the next best quality option.
     * If no Quality URLS have yet been created then try to start stream with an aync task.
     *
     * @param quality
     */
    private void startStreamWithQuality(String quality) {
        if (qualityURLs == null) {
            startStreamWithTask();
        } else {
            if (qualityURLs.containsKey(quality)) {
                if (chatOnlyViewVisible || audioViewVisible) {
                    return;
                }

                playUrl(qualityURLs.get(quality).URL);
                showQualities();
                updateSelectedQuality(quality);
                showPauseIcon();
                Log.d(LOG_TAG, "Starting Stream With a quality on " + quality + " for " + mChannelInfo.getDisplayName());
                Log.d(LOG_TAG, "URLS: " + qualityURLs.keySet().toString());
            } else if (!qualityURLs.isEmpty()) {
                Log.d(LOG_TAG, "Quality unavailable for this stream -  " + quality + ". Trying next best");
                tryNextBestQuality(quality);
            }
        }
    }

    /**
     * Starts and Aync task that fetches all available Stream URLs for a stream,
     * then tries to start stream with the latest user defined quality setting.
     * If no URLs are available for the stream, the user is notified.
     */
    private void startStreamWithTask() {
        GetLiveStreamURL.AsyncResponse callback = url -> {
            try {
                if (!url.isEmpty()) {
                    updateQualitySelections(url);
                    qualityURLs = url;

                    if (!checkForAudioOnlyMode()) {
                        startStreamWithQuality(new Settings(getContext()).getPrefStreamQuality());
                    }
                } else {
                    playbackFailed();
                }
            } catch (IllegalStateException | NullPointerException e) {
                e.printStackTrace();
            }
        };

        if (vodId == null) {
            GetLiveStreamURL task = new GetLiveStreamURL(callback);
            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mChannelInfo.getStreamerName());
        } else {
            GetLiveStreamURL task = new GetVODStreamURL(callback);
            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, vodId.substring(1));
        }
    }

    /**
     * Connects to the Twitch API to fetch the live stream url and quality selection urls.
     * If the task is successful the quality selector views' click listeners will be updated.
     */
    private void updateQualitySelectorsWithTask() {
        GetLiveStreamURL.AsyncResponse delegate = url -> {
            try {
                if (!url.isEmpty()) {
                    updateQualitySelections(url);
                    qualityURLs = url;
                }
            } catch (IllegalStateException | NullPointerException e) {
                e.printStackTrace();
            }
        };

        if (vodId == null) {
            GetLiveStreamURL task = new GetLiveStreamURL(delegate);
            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mChannelInfo.getStreamerName());
        } else {
            GetLiveStreamURL task = new GetVODStreamURL(delegate);
            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, vodId.substring(1));
        }
    }

    /**
     * Stops the buffering and notifies the user that the stream could not be played
     */
    private void playbackFailed() {
        mBufferingView.stop();
        if (vodId == null) {
            showSnackbar(getString(R.string.stream_playback_failed), "Retry", v -> startStreamWithTask());
        } else {
            showSnackbar(getString(R.string.vod_playback_failed), "Retry", v -> startStreamWithTask());
        }
    }

    private void showSnackbar(String message) {
        showSnackbar(message, null, null);
    }

    private void showSnackbar(String message, String actionText, View.OnClickListener action) {
        if (getActivity() != null && !isDetached()) {
            View mainView = ((StreamActivity) getActivity()).getMainContentLayout();

            if ((snackbar == null || !snackbar.isShown()) && mainView != null) {
                snackbar = Snackbar.make(mainView, message, 4000);
                if (actionText != null)
                    snackbar.setAction(actionText, action);
                snackbar.show();
            }
        }

    }

    private void tryNextBestQuality(String quality) {
        if (triesForNextBest < qualityURLs.size() - 1) { // Subtract 1 as we don't count AUDIO ONLY as a quality
            triesForNextBest++;
            List<String> qualityList = new ArrayList<>(qualityURLs.keySet());
            int next = qualityList.indexOf(quality) + 1;
            if (next >= qualityList.size() - 1) {
                startStreamWithQuality(GetLiveStreamURL.QUALITY_SOURCE);
            } else {
                startStreamWithQuality(qualityList.get(next));
            }
        } else {
            playbackFailed();
        }
    }

    /**
     * Sets the URL to the VideoView and ChromeCast and starts playback.
     *
     * @param url
     */
    private void playUrl(String url) {
        DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(getContext(), getString(R.string.app_name));
        MediaSource mediaSource = new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(url));
        currentMediaSource = mediaSource;
        player.prepare(mediaSource);

        checkVodProgress();
        resumeStream();
    }

    private void playWithExternalPlayer() {
        Toast errorToast = Toast.makeText(getContext(), R.string.error_external_playback_failed, Toast.LENGTH_LONG);
        if (qualityURLs == null) {
            errorToast.show();
            return;
        }

        String castQuality = getBestCastQuality(qualityURLs, settings.getPrefStreamQuality(), 0);
        if (castQuality == null) {
            errorToast.show();
            return;
        }

        updateSelectedQuality(castQuality);
        String url = qualityURLs.get(castQuality).URL;

        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(Uri.parse(url), "video/*");
        startActivity(Intent.createChooser(intent, getString(R.string.stream_external_play_using)));
    }

    /**
     * Sets up audio mode and starts playback of audio, while pausing any playing video
     */
    private void playAudioOnly() {
    }

    private void registerAudioOnlyDelegate() {
    }

    /**
     * Notifies the system that the screen though not timeout and fade to black.
     */
    private void keepScreenOn() {
        getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }

    /**
     * Notifies the system that the screen can now time out.
     */
    private void releaseScreenOn() {
        getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }

    private void updateSelectedQuality(String quality) {
        //TODO: Bad design
        if (quality == null) {
            resetQualityViewBackground(null);
        } else {
            resetQualityViewBackground(QualityOptions.get(quality));
        }
    }

    /**
     * Resets the background color of all the select quality views in the bottom dialog
     */
    private void resetQualityViewBackground(TextView selected) {
        for (TextView v : QualityOptions.values()) {
            if (v.equals(selected)) {
                v.setBackgroundColor(Service.getColorAttribute(R.attr.navigationDrawerHighlighted, R.color.grey_300, getContext()));
            } else {
                v.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.transparent));
            }
        }
    }

    /**
     * Adds the available qualities for a stream to the spinner menu
     *
     * @param availableQualities
     */
    private void updateQualitySelections(LinkedHashMap<String, Quality> availableQualities) {
        for (TextView view : QualityOptions.values()) {
            mQualityWrapper.removeView((MaterialRippleLayout) view.getParent());
        }

        for (Map.Entry<String, Quality> entry : availableQualities.entrySet()) {
            Quality quality = entry.getValue();
            String qualityKey = entry.getKey();
            if (qualityKey.equals("audio_only"))
                continue;

            MaterialRippleLayout layout = (MaterialRippleLayout) LayoutInflater.from(getContext()).inflate(R.layout.quality_item, null);
            TextView textView = ((TextView) layout.getChildAt(0));
            textView.setText(quality.Name);

            setQualityOnClick(textView, qualityKey);
            QualityOptions.put(qualityKey, textView);
            mQualityWrapper.addView(layout);
        }
    }

    /**
     * Sets an OnClickListener on a select quality view (From bottom dialog).
     * The Listener starts the stream with a new quality setting and updates the background for the select quality views in the bottom dialog
     *
     * @param qualityView
     */
    private void setQualityOnClick(final TextView qualityView, String quality) {
        qualityView.setOnClickListener(v -> {
            settings.setPrefStreamQuality(quality);
            startStreamWithQuality(quality);
            resetQualityViewBackground(qualityView);
            mQualityBottomSheet.dismiss();
        });
    }

    private BottomSheetBehavior getDefaultBottomSheetBehaviour(View bottomSheetView) {
        BottomSheetBehavior behavior = BottomSheetBehavior.from((View) bottomSheetView.getParent());
        behavior.setPeekHeight(getContext().getResources().getDisplayMetrics().heightPixels / 3);
        return behavior;
    }

    private void setupProfileBottomSheet() {
        View v = LayoutInflater.from(getContext()).inflate(R.layout.stream_profile_preview, null);
        mProfileBottomSheet = new BottomSheetDialog(getContext());
        mProfileBottomSheet.setContentView(v);
        final BottomSheetBehavior behavior = getDefaultBottomSheetBehaviour(v);

        mProfileBottomSheet.setOnDismissListener(dialogInterface -> behavior.setState(BottomSheetBehavior.STATE_COLLAPSED));

        TextView mNameView = mProfileBottomSheet.findViewById(R.id.twitch_name);
        TextView mFollowers = mProfileBottomSheet.findViewById(R.id.txt_followers);
        TextView mViewers = mProfileBottomSheet.findViewById(R.id.txt_viewers);
        ImageView mFollowButton = mProfileBottomSheet.findViewById(R.id.follow_unfollow_icon);
        ImageView mFullProfileButton = mProfileBottomSheet.findViewById(R.id.full_profile_icon);
        RecyclerView mPanelsRecyclerView = mProfileBottomSheet.findViewById(R.id.panel_recyclerview);

        mNameView.setText(mChannelInfo.getDisplayName());
        mFollowers.setText(mChannelInfo.getFollowers() + "");
        mViewers.setText(mChannelInfo.getViews() + "");

        mFullProfileButton.setOnClickListener(view -> {
            mProfileBottomSheet.dismiss();

            final Intent intent = new Intent(getContext(), ChannelActivity.class);
            intent.putExtra(getContext().getResources().getString(R.string.channel_info_intent_object), mChannelInfo);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

            getContext().startActivity(intent);
        });

        setupFollowButton(mFollowButton);
        setupPanels(mPanelsRecyclerView);
    }

    private void setupFollowButton(final ImageView imageView) {
        final FollowHandler mFollowHandler = new FollowHandler(
                mChannelInfo,
                getContext(),
                new FollowHandler.Delegate() {
                    @Override
                    public void streamerIsFollowed() {
                    }

                    @Override
                    public void streamerIsNotFollowed() {
                    }

                    @Override
                    public void userIsNotLoggedIn() {
                        imageView.setVisibility(View.GONE);
                    }

                    @Override
                    public void followSuccess() {
                    }

                    @Override
                    public void followFailure() {
                    }

                    @Override
                    public void unfollowSuccess() {
                    }

                    @Override
                    public void unfollowFailure() {
                    }
                }
        );
        updateFollowIcon(imageView, mFollowHandler.isStreamerFollowed());

        imageView.setOnClickListener(view -> {
            final boolean isFollowed = mFollowHandler.isStreamerFollowed();
            if (isFollowed) {
                mFollowHandler.unfollowStreamer();
            } else {
                mFollowHandler.followStreamer();
            }

            final int ANIMATION_DURATION = 240;

            imageView.animate()
                    .setDuration(ANIMATION_DURATION)
                    .alpha(0f)
                    .start();

            new Handler().postDelayed(() -> {
                updateFollowIcon(imageView, !isFollowed);
                imageView.animate().alpha(1f).setDuration(ANIMATION_DURATION).start();
            }, ANIMATION_DURATION);
        });
    }

    private void updateFollowIcon(ImageView imageView, boolean isFollowing) {
        @DrawableRes int imageRes = isFollowing
                ? R.drawable.ic_heart_broken_24dp
                : R.drawable.ic_heart_24dp;
        imageView.setImageResource(imageRes);
    }

    private void setupPanels(RecyclerView recyclerView) {
        final PanelAdapter mPanelAdapter = new PanelAdapter(getActivity());
        recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, false));
        recyclerView.setAdapter(mPanelAdapter);

        GetPanelsTask mTask = new GetPanelsTask(mChannelInfo.getStreamerName(), mPanelAdapter::addPanels);
        mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    /**
     * Setups the Quality Select spinner.
     * Automatically hides the text of the selected Quality
     */
    private void setupSpinner() {
        mQualityButton.setOnClickListener(v -> mQualityBottomSheet.show());

        View v = LayoutInflater.from(getContext()).inflate(R.layout.stream_settings, null);
        mQualityBottomSheet = new BottomSheetDialog(getContext());
        mQualityBottomSheet.setContentView(v);

        final BottomSheetBehavior behavior = getDefaultBottomSheetBehaviour(v);

        mQualityBottomSheet.setOnDismissListener(dialogInterface -> behavior.setState(BottomSheetBehavior.STATE_COLLAPSED));

        mQualityWrapper = mQualityBottomSheet.findViewById(R.id.quality_wrapper);
        mAudioOnlySelector = mQualityBottomSheet.findViewById(R.id.audio_only_selector);
        mChatOnlySelector = mQualityBottomSheet.findViewById(R.id.chat_only_selector);
        TextView optionsTitle = mQualityBottomSheet.findViewById(R.id.options_text);

        if (optionsTitle != null) {
            optionsTitle.setVisibility(View.VISIBLE);
        }

        if (vodId == null) {
            mChatOnlySelector.setVisibility(View.VISIBLE);
        }

        // Audio Only is currently broken, so let's not show it
        mAudioOnlySelector.setVisibility(View.GONE);
        /*
        mAudioOnlySelector.setVisibility(View.VISIBLE);
        mAudioOnlySelector.setOnClickListener(view -> {
            mQualityBottomSheet.dismiss();
            audioOnlyClicked();
        });
        */

        mChatOnlySelector.setOnClickListener(view -> {
            mQualityBottomSheet.dismiss();
            chatOnlyClicked();
        });
    }

    private void initAudioOnlyView() {
        if (!audioViewVisible) {
            audioViewVisible = true;
            mVideoView.setVisibility(View.INVISIBLE);
            mBufferingView.start();
            //mBufferingView.setVisibility(View.GONE);
            previewInbackGround = false;
            castingTextView.setVisibility(View.VISIBLE);
            castingTextView.setText(getString(R.string.stream_audio_only_active));

            showVideoInterface();
            updateSelectedQuality(null);
            hideQualities();
        }
    }

    private void disableAudioOnlyView() {
        if (audioViewVisible) {
            mAudioOnlySelector.setChecked(false);
            audioViewVisible = false;
            mVideoView.setVisibility(View.VISIBLE);
            mBufferingView.setVisibility(View.VISIBLE);
            Service.bringToBack(mPreview);
            previewInbackGround = true;
            castingTextView.setVisibility(View.INVISIBLE);

            showVideoInterface();
            startStreamWithQuality(settings.getPrefStreamQuality());
        }
    }

    private boolean checkForAudioOnlyMode() {
        boolean isAudioOnly = isAudioOnlyModeEnabled();
        if (isAudioOnly) {
            mAudioOnlySelector.setChecked(true);
            playAudioOnly();
        }

        return isAudioOnly;
    }

    private boolean isAudioOnlyModeEnabled() {
        return false;
    }

    private void audioOnlyClicked() {
        mAudioOnlySelector.setChecked(!mAudioOnlySelector.isChecked());
        if (mAudioOnlySelector.isChecked()) {
            playAudioOnly();
        } else {
            stopAudioOnly();
        }
    }

    private void stopAudioOnly() {
        disableAudioOnlyView();
        showPlayIcon();
        //startStreamWithQuality(settings.getPrefStreamQuality());
    }

    private void stopAudioOnlyNoServiceCall() {
        disableAudioOnlyView();
    }

    private void chatOnlyClicked() {
        mChatOnlySelector.setChecked(!mChatOnlySelector.isChecked());
        if (mChatOnlySelector.isChecked()) {
            initChatOnlyView();
        } else {
            disableChatOnlyView();
        }
    }

    private void initChatOnlyView() {
        if (!chatOnlyViewVisible) {
            chatOnlyViewVisible = true;
            if (isFullscreen) {
                toggleFullscreen();
            }

            getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

            videoHeightBeforeChatOnly = mVideoWrapper.getHeight();
            ResizeHeightAnimation heightAnimation = new ResizeHeightAnimation(mVideoWrapper, (int) getResources().getDimension(R.dimen.main_toolbar_height));
            heightAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
            heightAnimation.setDuration(240);
            mVideoWrapper.startAnimation(heightAnimation);

            mPlayPauseWrapper.setVisibility(View.GONE);
            mControlToolbar.setVisibility(View.GONE);
            mToolbar.setBackgroundColor(Service.getColorAttribute(R.attr.colorPrimary, R.color.primary, getContext()));

            releasePlayer();
            optionsMenuItem.setVisible(true);

            showVideoInterface();
            updateSelectedQuality(null);
            hideQualities();
        }
    }

    private void disableChatOnlyView() {
        if (chatOnlyViewVisible) {
            chatOnlyViewVisible = false;
            getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);

            ResizeHeightAnimation heightAnimation = new ResizeHeightAnimation(mVideoWrapper, videoHeightBeforeChatOnly);
            heightAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
            heightAnimation.setDuration(240);
            heightAnimation.setFillAfter(false);
            mVideoWrapper.startAnimation(heightAnimation);

            mControlToolbar.setVisibility(View.VISIBLE);
            mPlayPauseWrapper.setVisibility(View.VISIBLE);
            mToolbar.setBackgroundColor(Service.getColorAttribute(R.attr.streamToolbarColor, R.color.black_transparent, getContext()));

            if (!castingViewVisible) {
                startStreamWithQuality(settings.getPrefStreamQuality());
            }

            optionsMenuItem.setVisible(false);

            showVideoInterface();
        }
    }

    public void prePictureInPicture() {
        pictureInPictureEnabled = true;

        int width = getScreenRect(getActivity()).height();
        ResizeWidthAnimation resizeWidthAnimation = new ResizeWidthAnimation(mVideoWrapper, width);
        resizeWidthAnimation.setDuration(250);
        mVideoWrapper.startAnimation(resizeWidthAnimation);
    }

    @Override
    public void onPictureInPictureModeChanged(boolean enabled) {
        View[] views = new View[] {  mToolbar, mControlToolbar, mPlayPauseWrapper, mShowChatButton, mForward, mBackward };
        for (View view : views) {
            view.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE);
        }

        pictureInPictureEnabled = enabled;
    }

    /**
     * Setups the toolbar by giving it a bit of extra right padding (To make sure the icons are 16dp from right)
     * Also adds the main toolbar as the support actionbar
     */
    private void setupToolbar() {
        mToolbar.setPadding(0, 0, Service.dpToPixels(getActivity(), 5), 0);
        setHasOptionsMenu(true);
        mActivity.setSupportActionBar(mToolbar);
        mActivity.getSupportActionBar().setTitle(mChannelInfo.getDisplayName());
        mActivity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        //mActivity.getSupportActionBar().setDefaultDisplayHomeAsUpEnabled(true);
        mToolbar.bringToFront();
    }

    /**
     * Rotates the Play Pause wrapper with an Rotation Animation.
     */
    private void rotatePlayPauseWrapper() {
        RotateAnimation rotate = new RotateAnimation(mPlayPauseWrapper.getRotation(),
                360, Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        int PLAY_PAUSE_ANIMATION_DURATION = 500;
        rotate.setDuration(PLAY_PAUSE_ANIMATION_DURATION);
        rotate.setInterpolator(new AccelerateDecelerateInterpolator());
        mPlayPauseWrapper.startAnimation(rotate);
    }

    /**
     * Checks if the device is below SDK API 19 (Kitkat)
     *
     * @return the result
     */
    private boolean isDeviceBelowKitkat() {
        return Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT;
    }

    private void showPauseIcon() {
        if (mPauseIcon.getAlpha() == 0f) {
            rotatePlayPauseWrapper();
            mPauseIcon.animate().alpha(1f).start();
            mPlayIcon.animate().alpha(0f).start();
        }
    }

    private void showPlayIcon() {
        if (mPauseIcon.getAlpha() != 0f) {
            rotatePlayPauseWrapper();
            mPauseIcon.animate().alpha(0f).start();
            mPlayIcon.animate().alpha(1f).start();
        }
    }

    private void showQualities() {
        mQualityWrapper.setVisibility(View.VISIBLE);
    }

    private void hideQualities() {
        mQualityWrapper.setVisibility(View.GONE);
    }

    public interface StreamFragmentListener {
        void onSeek();
        void refreshLayout();
    }

    /**
     * Broadcast class for detecting when the user plugs or unplug a headset.
     */
    private class HeadsetPlugIntentReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
                int state = intent.getIntExtra("state", -1);
                switch (state) {
                    case 0:
                        if (player.isPlaying()) {
                            Log.d(LOG_TAG, "Chat, pausing from headsetPlug");
                            showVideoInterface();
                            pauseStream();
                        }
                        break;
                    case 1:
                        showVideoInterface();
                        break;
                    default:

                }
            }
        }
    }
}