/*
 The MIT License (MIT)

 Copyright (c) 2017 Nedim Cholich

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 */
package co.frontyard.cordova.plugin.exoplayer;

import android.graphics.Color;
import android.util.Log;
import android.app.*;
import android.content.*;
import android.content.pm.PackageManager;
import android.media.*;
import android.net.*;
import android.view.*;
import android.widget.*;
import android.os.Handler;

import androidx.annotation.NonNull;
import androidx.appcompat.widget.ContentFrameLayout;


import com.google.android.exoplayer2.*;
import com.google.android.exoplayer2.source.*;
import com.google.android.exoplayer2.source.dash.*;
import com.google.android.exoplayer2.source.hls.*;
import com.google.android.exoplayer2.source.smoothstreaming.*;
import com.google.android.exoplayer2.ui.*;
import com.google.android.exoplayer2.upstream.*;
import com.google.android.exoplayer2.util.*;
import com.google.android.exoplayer2.Player.PositionInfo;
import com.google.android.exoplayer2.video.*;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
import com.google.android.exoplayer2.trackselection.TrackSelectionOverride;
import com.google.common.collect.ImmutableList;

import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.DefaultRenderersFactory;

import java.lang.*;
import java.util.*;
import org.apache.cordova.*;
import org.json.*;

public class Player {
    public static final String TAG = "ExoPlayerPlugin";
    private final Activity activity;
    private final CallbackContext callbackContext;
    private final Configuration config;
    private Dialog dialog;
    private ExoPlayer exoPlayer;
    private StyledPlayerView exoView;
    private CordovaWebView webView;
    private int controllerVisibility;
    private boolean paused = false;
    private AudioManager audioManager;
    private ContentFrameLayout parentLayout;
    private Handler timeHandler;
    private Runnable timeRunnable;
    private LinearLayout backgroundLinearLayout;

    public Player(Configuration config, Activity activity, CallbackContext callbackContext, CordovaWebView webView) {
        this.config = config;
        this.activity = activity;
        this.callbackContext = callbackContext;
        this.webView = webView;
        this.audioManager = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE);
    }

    private ExoPlayer.Listener playerEventListener = new ExoPlayer.Listener() {
        @Override
        public void onIsLoadingChanged(boolean isLoading) {
            JSONObject payload = Payload.loadingEvent(Player.this.exoPlayer, isLoading);
            new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
        }

        @Override
        public void onPlaybackParametersChanged(@NonNull PlaybackParameters playbackParameters) {
            Log.i(TAG, "Playback parameters changed");
        }

        @Override
        public void onPlayerError(@NonNull PlaybackException error) {
            JSONObject payload = Payload.playerErrorEvent(Player.this.exoPlayer, error, null);
            new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.ERROR, payload, true);
        }

        @Override
        public void onPlaybackStateChanged(int playbackState) {
            if (config.getShowBuffering()) {
                LayoutProvider.setBufferingVisibility(exoView, activity, playbackState == ExoPlayer.STATE_BUFFERING);
            }
            JSONObject payload = Payload.stateEvent(Player.this.exoPlayer, playbackState, Player.this.controllerVisibility == View.VISIBLE);
            new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
        }

        @Override
        public void onPositionDiscontinuity(@NonNull PositionInfo oldPosition, @NonNull PositionInfo newPosition, int reason) {
            JSONObject payload = Payload.positionDiscontinuityEvent(Player.this.exoPlayer, reason);
            new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
        }

        @Override
        public void onRepeatModeChanged(int newRepeatMode) {
            // Need to see if we want to send this to Cordova.
        }

        @Override
        public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
        }

        @Override
        public void onTimelineChanged(@NonNull Timeline timeline, int reason) {
            JSONObject payload = Payload.timelineChangedEvent(Player.this.exoPlayer, timeline);
            new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
        }

        @Override
        public void onVideoSizeChanged(VideoSize videoSize) {
            // JSONObject payload = Payload.videoSizeChangedEvent(Player.this.exoPlayer, videoSize);
            // new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
        }

        @Override
        public void onTracksChanged(Tracks tracks) {
            // // Find the audio track group and select the first track
            // for (Tracks.Group trackGroup : tracks.getGroups()) {
            //     if (trackGroup.getType() == C.TRACK_TYPE_AUDIO) {
            //         TrackSelectionOverride override = new TrackSelectionOverride(
            //             trackGroup.getMediaTrackGroup(), 0 // Select the first track (index 0)
            //         );
            //         exoPlayer.setTrackSelectionParameters(
            //             exoPlayer.getTrackSelectionParameters().buildUpon()
            //                 .setOverrideForType(override)
            //                 .build()
            //         );
            //         break; // Only need to set for one audio group
            //     }
            // }

            if (null != Player.this.exoPlayer && Payload.playbackStateToString(Player.this.exoPlayer.getPlaybackState()) == "STATE_BUFFERING") {
                JSONObject payload = Payload.tracksChangedEvent(Player.this.exoPlayer, tracks);
                new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);    
            }

        }
    };

    private DialogInterface.OnDismissListener dismissListener = new DialogInterface.OnDismissListener() {
        @Override
        public void onDismiss(DialogInterface dialog) {
            Log.i(TAG, "Player dialog dismissed");

            if (exoPlayer != null) {
                exoPlayer.release();
            }
            exoPlayer = null;
            JSONObject payload = Payload.stopEvent(null);
            new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
        }
    };

    private DialogInterface.OnKeyListener onKeyListener = new DialogInterface.OnKeyListener() {
        @Override
        public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
            String key = KeyEvent.keyCodeToString(event.getKeyCode());
            Log.i(TAG, "onKey listener " + keyCode + "/ '" + key + "'");

            // We need android to handle these key events
            if (key.equals("KEYCODE_VOLUME_UP") ||
                    key.equals("KEYCODE_VOLUME_DOWN") ||
                    key.equals("KEYCODE_VOLUME_MUTE")) {
                return false;
            }
            else {
                JSONObject payload = Payload.keyEvent(event);
                new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
                return true;
            }
        }
    };

    private View.OnTouchListener onTouchListener = new View.OnTouchListener() {
        int previousAction = -1;

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            int eventAction = event.getAction();
            if (previousAction != eventAction) {
                previousAction = eventAction;
                JSONObject payload = Payload.touchEvent(event);
                new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
            }
            return true;
        }
    };

    private StyledPlayerView.ControllerVisibilityListener playbackControlVisibilityListener = new StyledPlayerView.ControllerVisibilityListener() {
        @Override
        public void onVisibilityChanged(int visibility) {
            Player.this.controllerVisibility = visibility;
        }
    };

    private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
        public void onAudioFocusChange(int focusChange) {
            if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
                JSONObject payload = Payload.audioFocusEvent(Player.this.exoPlayer, "AUDIOFOCUS_LOSS_TRANSIENT");
                new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
            }
            else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
                JSONObject payload = Payload.audioFocusEvent(Player.this.exoPlayer, "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK");
                new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
            }
            else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                JSONObject payload = Payload.audioFocusEvent(Player.this.exoPlayer, "AUDIOFOCUS_GAIN");
                new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
            }
            else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                JSONObject payload = Payload.audioFocusEvent(Player.this.exoPlayer, "AUDIOFOCUS_LOSS");
                new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
            }
        }
    };

    public void createPlayer() {
        Log.i(TAG, "Playing " + config.getUri());

        if (config.useInlineView()) {
            // Using a dialog doesn't work for us, as controls are drawn in HTML view (cordova ui)
            // createPlayerInCordovaUI();
            createTransparentPlayer();

        } else if (!config.isAudioOnly()) {
            createDialog();
        }
        preparePlayer(config.getUri());

        if (config.useInlineView()) {
            setController(config.getController());
        }
    }

    public void createDialog() {
        dialog = new Dialog(this.activity, android.R.style.Theme_Black_NoTitleBar_Fullscreen);
        dialog.setOnKeyListener(onKeyListener);
        dialog.getWindow().getAttributes().windowAnimations = android.R.style.Animation_Dialog;
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        View decorView = dialog.getWindow().getDecorView();
        int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_FULLSCREEN
                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
        decorView.setSystemUiVisibility(uiOptions);
        dialog.setCancelable(true);
        dialog.setOnDismissListener(dismissListener);

        FrameLayout mainLayout = LayoutProvider.getMainLayout(this.activity);
        exoView = LayoutProvider.getExoPlayerView(this.activity, config);
        exoView.setControllerVisibilityListener(playbackControlVisibilityListener);

        mainLayout.addView(exoView);
        dialog.setContentView(mainLayout);
        dialog.show();

        dialog.getWindow().setAttributes(LayoutProvider.getDialogLayoutParams(activity, config, dialog));
        exoView.requestFocus();
        exoView.setOnTouchListener(onTouchListener);
        LayoutProvider.setupController(exoView, activity, config.getController());
    }

    public void createPlayerInCordovaUI() {
        exoView = LayoutProvider.getExoPlayerView(this.activity, config);
        exoView.setControllerVisibilityListener(playbackControlVisibilityListener);

        exoView.setElevation(99);
        exoView.setVisibility(View.VISIBLE);
        exoView.setBackgroundColor(Color.TRANSPARENT);

        try {
            View webViewImpl = webView.getEngine().getView();
            Object webViewParent = webViewImpl.getParent();

            Log.d(TAG, "Have a " + (webViewImpl == null ? "empty" : "valid") + " parent View");
            if (webViewImpl != null) {
                Log.d(TAG, "parentView is a " + webViewImpl.getClass().getCanonicalName());
            }

            // Cordova webview is in a ContentFrameLayout (for plugin version 12 it is)
            parentLayout = (ContentFrameLayout)webViewParent;
            parentLayout.addView(exoView);

            // Keep controls on top of player.
            webViewImpl.setElevation(101);
        }
        catch (Exception e) {
            Log.e(TAG, "Problem adding exoplayer to cordova's webview containers: " + e.getMessage());
        }

        exoView.requestFocus();
        exoView.setOnTouchListener(onTouchListener);
    }

    private int setupAudio() {
        activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
        return audioManager.requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
    }

    private void preparePlayer(Uri uri) {
        int audioFocusResult = setupAudio();
        String audioFocusString = audioFocusResult == AudioManager.AUDIOFOCUS_REQUEST_FAILED ?
                "AUDIOFOCUS_REQUEST_FAILED" :
                "AUDIOFOCUS_REQUEST_GRANTED";
        DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(this.activity).build();
        //TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveVideoTrackSelection.Factory(bandwidthMeter);


        /* Build with extyentions ffmpeg */
        RenderersFactory renderersFactory = new DefaultRenderersFactory(this.activity)
            .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON);

        // // /* lunch normal exoplayer player */
        // exoPlayer = new ExoPlayer.Builder(this.activity, renderersFactory).build();

        // // 1. Create a DefaultTrackSelector
        DefaultTrackSelector trackSelector = new DefaultTrackSelector(this.activity);
        // 2. Build parameters to disable text tracks
        DefaultTrackSelector.Parameters parameters = new DefaultTrackSelector.ParametersBuilder(this.activity)
            // .setRendererDisabled(C.TRACK_TYPE_TEXT, true) // Disable the text renderer
            // .clearOverridesOfType(C.TRACK_TYPE_TEXT) // remove any explicit subtitle overrides
            .setDisabledTextTrackSelectionFlags(1) // 1 disables all automatic text track selection
            
            .clearOverridesOfType(C.TRACK_TYPE_AUDIO) // remove any explicit subtitle overrides
            // .clearSelectionOverrides(C.TRACK_TYPE_AUDIO)
            .setPreferredAudioLanguage(null)    
    
            .build();
        // 3. Apply the parameters to the track selector
        trackSelector.setParameters(parameters);
        // 4. Build the ExoPlayer instance with the configured track selector
        exoPlayer = new ExoPlayer.Builder(this.activity, renderersFactory)
            .setTrackSelector(trackSelector)
            .build();


        // // 1. Create a DefaultTrackSelector with custom parameters.
        // DefaultTrackSelector trackSelector = new DefaultTrackSelector(this.activity);

        // // 2. Create the ParametersBuilder from the track selector.
        // DefaultTrackSelector.ParametersBuilder parametersBuilder = trackSelector.buildUponParameters();

        // // 3. Disable the text (subtitle) renderer.
        // parametersBuilder.setRendererDisabled(C.TRACK_TYPE_TEXT, true);

        // // 4. (Optional but good practice) Clear any previous audio selection overrides.
        // // This ensures that the default behavior (selecting the first audio track) will apply.
        // parametersBuilder.clearSelectionOverrides(C.TRACK_TYPE_AUDIO);

        // // 5. Apply the new parameters to the track selector.
        // trackSelector.setParameters(parametersBuilder);

        // // 6. Build the ExoPlayer instance with the configured track selector.
        // exoPlayer = new SimpleExoPlayer.Builder(this.activity)
        //         .setTrackSelector(trackSelector)
        //         .build();

        exoPlayer.addListener(playerEventListener);
        if (null != exoView) {
            exoView.setPlayer(new ForwardingPlayer(exoPlayer) {
                @Override
                public long getSeekForwardIncrement() {
                    Log.d(TAG, "ForwardingPlayer::getSeekForwardIncrement: " + config.getForwardTimeMs());
                    return config.getForwardTimeMs();
                }

                @Override
                public long getSeekBackIncrement() {
                    Log.d(TAG, "ForwardingPlayer::getSeekBackIncrement: " + config.getRewindTimeMs());
                    return config.getRewindTimeMs();
                }
            });
        }

        MediaSource mediaSource = getMediaSource(uri, bandwidthMeter);
        if (mediaSource != null) {
            long startTimeMS = config.getSeekTo();
            boolean autoPlay = config.autoPlay();
            if (startTimeMS > 0) {
                exoPlayer.setMediaSource(mediaSource, startTimeMS);
            } else {
                exoPlayer.setMediaSource(mediaSource);
            }
            exoPlayer.prepare();

            exoPlayer.setPlayWhenReady(autoPlay);
            paused = !autoPlay;

            JSONObject payload = Payload.startEvent(exoPlayer, audioFocusString);
            new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
            startTimeUpdates();
        }
        else {
            sendError("Failed to construct mediaSource for " + uri);
        }
    }

    private MediaSource getMediaSource(Uri uri, DefaultBandwidthMeter bandwidthMeter) {
        String userAgent = Util.getUserAgent(this.activity, config.getUserAgent());
        int connectTimeout = config.getConnectTimeout();
        int readTimeout = config.getReadTimeout();

        HttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory()
                .setUserAgent(userAgent)
                .setTransferListener(bandwidthMeter)
                .setConnectTimeoutMs(connectTimeout)
                .setReadTimeoutMs(readTimeout)
                .setAllowCrossProtocolRedirects(true);
        DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this.activity, httpDataSourceFactory)
            .setTransferListener(bandwidthMeter);
        MediaSource mediaSource;
        int type = Util.inferContentType(uri);
        switch (type) {
            case C.CONTENT_TYPE_DASH:
                mediaSource = new DashMediaSource.Factory(dataSourceFactory)
                    .createMediaSource(new MediaItem.Builder()
                        .setUri(uri)
                        .setMimeType(MimeTypes.APPLICATION_MPD)
                        .build());
                break;
            case C.CONTENT_TYPE_HLS:
                mediaSource = new HlsMediaSource.Factory(dataSourceFactory)
                    .createMediaSource(new MediaItem.Builder()
                        .setUri(uri)
                        .setMimeType(MimeTypes.APPLICATION_M3U8)
                        .build());
                break;
            case C.CONTENT_TYPE_SS:
                mediaSource = new SsMediaSource.Factory(dataSourceFactory)
                    .createMediaSource(MediaItem.fromUri(uri));
                break;
            default:
                mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
                    .createMediaSource(MediaItem.fromUri(uri));
                break;
        }

        String subtitleUrl = config.getSubtitleUrl();
        if (null != subtitleUrl) {
            Uri subtitleUri = Uri.parse(subtitleUrl);
            String subtitleType = inferSubtitleType(subtitleUri);
            Log.i(TAG, "Subtitle present: " + subtitleUri + ", type=" + subtitleType);
            MediaSource subtitleSource = new SingleSampleMediaSource.Factory(httpDataSourceFactory)
                .createMediaSource(
                    new MediaItem.SubtitleConfiguration.Builder(uri)
                            .setMimeType(subtitleType)
                            .setLanguage("en")
                            .setSelectionFlags(C.SELECTION_FLAG_AUTOSELECT)
                            .build(),
                    C.TIME_UNSET);
            return new MergingMediaSource(mediaSource, subtitleSource);
        }
        else {
            return mediaSource;
        }
    }

    private static String inferSubtitleType(Uri uri) {
        String fileName = uri.getPath().toLowerCase();

        if (fileName.endsWith(".vtt")) {
            return MimeTypes.TEXT_VTT;
        }
        else {
            // Assume it's srt.
            return MimeTypes.APPLICATION_SUBRIP;
        }
    }

    public void close() {
        Log.i(TAG, "closing stream");
        FrameLayout wrapperFrameLayout = (FrameLayout) this.webView.getView().getParent();
        audioManager.abandonAudioFocus(audioFocusChangeListener);
        if (exoPlayer != null) {
            exoPlayer.setPlayWhenReady(false);
            exoPlayer.stop();
            exoPlayer.release();
            exoPlayer = null;
        }
        if (this.dialog != null) {
            dialog.dismiss();
        }

        if (parentLayout != null) {
            parentLayout.removeView(exoView);
        }

        if (backgroundLinearLayout != null) {
            backgroundLinearLayout.removeAllViews();
            wrapperFrameLayout.removeView(backgroundLinearLayout);

            // ViewGroup parent = (ViewGroup) backgroundLinearLayout.getParent();
            // if (parent != null) {
            //     parent.removeView(backgroundLinearLayout);
            // }
            // backgroundLinearLayout.removeAllViews();
            // backgroundLinearLayout = null;
        }
    }

    public void setStream(Uri uri, JSONObject controller) {
        if (null != uri && null != exoPlayer) {
            DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(null).build();
            MediaSource mediaSource = getMediaSource(uri, bandwidthMeter);
            exoPlayer.setMediaSource(mediaSource);
            exoPlayer.prepare();
            play();
        }
        setController(controller);
    }

    public void playPause() {
        if (this.paused) {
            play();
        }
        else {
            pause();
        }
    }

    public void pause() {

        if (null != exoPlayer) {
            paused = true;
            exoPlayer.setPlayWhenReady(false);
            stopTimeUpdates();
        }
    }

    public void play() {
        paused = false;
        exoPlayer.setPlayWhenReady(true);
        startTimeUpdates();
    }

    public void stop() {
        Log.i(TAG, "STOP" +  ( (null == exoPlayer) ? " exoPlayer not yet initialized" : ""));

        paused = false;
        exoPlayer.stop();

        stopTimeUpdates();
    }

    private long normalizeOffset(long newTime) {
        long duration = exoPlayer.getDuration();
        if (duration == C.TIME_UNSET) return newTime;

        return Math.min(Math.max(0, newTime), duration);
    }

    public JSONObject seekTo(long timeMillis) {
        long newTime = normalizeOffset(timeMillis);
        Log.i(TAG, "SEEK (to) " +  timeMillis  + " / " + newTime + " (normalized)");

        exoPlayer.seekTo(newTime);
        return Payload.seekEvent(this.exoPlayer, newTime);
    }

    public JSONObject seekBy(long timeMillis) {
        long newTime = normalizeOffset(exoPlayer.getCurrentPosition() + timeMillis);
        Log.i(TAG, "SEEK (by)" +  timeMillis  + " / " + newTime + " (normalized)");

        exoPlayer.seekTo(newTime);
        return Payload.seekEvent(this.exoPlayer, newTime);
    }

    public JSONObject getPlayerState() {
        return Payload.stateEvent(exoPlayer,
                null != exoPlayer ? exoPlayer.getPlaybackState() : com.google.android.exoplayer2.Player.STATE_ENDED,
                Player.this.controllerVisibility == View.VISIBLE);
    }

    public void showController() {
        if (null != exoView) {
            exoView.showController();
        }
    }

    public void hideController() {
        if (null != exoView) {
            exoView.hideController();
        }
    }

    public void setController(JSONObject controller) {
        if (null != exoView) {
            LayoutProvider.setupController(exoView, activity, controller);
        }
    }

    private void sendError(String msg) {
        Log.e(TAG, msg);
        JSONObject payload = Payload.playerErrorEvent(Player.this.exoPlayer, null, msg);
        new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.ERROR, payload, true);
    }

    public void setZIndex(int index) {
        Log.i(TAG, "setZIndex: " + index);

        exoView.setElevation(index);
        exoView.invalidate();
        this.parentLayout.invalidate();
    }

    public void createTransparentPlayer() {
        webView.getView().setBackgroundColor(Color.TRANSPARENT);

        FrameLayout mainLayout = LayoutProvider.getMainLayout(this.activity);
        exoView = LayoutProvider.getExoPlayerView(this.activity, config);
        exoView.setControllerVisibilityListener(playbackControlVisibilityListener);
        mainLayout.addView(exoView);

        backgroundLinearLayout = LayoutProvider.getBackgroundLinearLayout(this.activity);
        backgroundLinearLayout.addView(mainLayout);

        FrameLayout wrapperFrameLayout = (FrameLayout) this.webView.getView().getParent();
        wrapperFrameLayout.addView(backgroundLinearLayout, 0);

        exoView.requestFocus();
        exoView.setOnTouchListener(onTouchListener);
    }

    // Enable audio by track index
    public JSONObject setAudioTrack(int trackId) {
        JSONObject response = new JSONObject();

        // try {
            DefaultTrackSelector trackSelector = (DefaultTrackSelector) exoPlayer.getTrackSelector();
            Tracks currentTracks = exoPlayer.getCurrentTracks();
            
            if (currentTracks == null) {
                try {
                    response.put("success", true);
                    response.put("message", "No current tracks found");

                } catch (JSONException e) {
                    Log.e(TAG, "Error creating response JSON", e);
                }

                return response;
            }

            // // Clear existing audio track overrides
            // builder.clearOverrides();

            for (Tracks.Group group : currentTracks.getGroups()) {
                if (group.getType() == C.TRACK_TYPE_AUDIO) {
                    for (int i = 0; i < group.length; i++) {
                        try {
                            Format format = group.getTrackFormat(i);
                            int format_id = Integer.parseInt(format.id);

                            if (trackId == format_id) {
                                // response.put("Founded " + i, format.toString());
                                // Create track selection override
                                TrackSelectionParameters.Builder builder = exoPlayer.getTrackSelectionParameters().buildUpon();
                                builder.clearOverridesOfType(C.TRACK_TYPE_AUDIO);
                                TrackSelectionOverride override = new TrackSelectionOverride(group.getMediaTrackGroup(), i);
                                builder.addOverride(override);
                                
                                // Apply the selection
                                exoPlayer.setTrackSelectionParameters(builder.build());
                            
                                response.put("success", true);
                                response.put("message", "Audio track set successfully");

                                return response;
                            }
                        } catch (JSONException e) {
                            try {
                                response.put("success", false);
                                response.put("error", "Failed to check audio track: " + e.getMessage());
                                Log.e(TAG, "Error creating response JSON", e);
                            } catch (JSONException je) {
                                Log.e(TAG, "Error creating response JSON", je);
                            }
                        }
                    }
                }       
            }

            try {
                response.put("success", false);
                response.put("message", "Audio track not found " + trackId);
            } catch (JSONException e) {
                Log.e(TAG, "Error creating response JSON", e);
            }

        return response;
    }

    // Enable subtitle by track index - CORRECTED VERSION
    public JSONObject setSubtitleTrack(int trackId) {
        JSONObject response = new JSONObject();


            DefaultTrackSelector trackSelector = (DefaultTrackSelector) exoPlayer.getTrackSelector();
            Tracks currentTracks = exoPlayer.getCurrentTracks();
            
            if (currentTracks == null) {
                try {
                    response.put("success", true);
                    response.put("message", "No current tracks found");                    
                } catch (JSONException e) {
                    Log.e(TAG, "Error creating response JSON", e);
                }

                return response;
            }

            for (Tracks.Group group : currentTracks.getGroups()) {
                if (group.getType() == C.TRACK_TYPE_TEXT) {
                    for (int i = 0; i < group.length; i++) {
                        Format format = group.getTrackFormat(i);
                        int formatId = Integer.parseInt(format.id);

                        try {                            
                            if (trackId == formatId) {
                                // Found the target track - create selection override
                                DefaultTrackSelector.Parameters.Builder parametersBuilder = trackSelector.getParameters()
                                        .buildUpon()
                                        .setRendererDisabled(C.TRACK_TYPE_TEXT, false); // Enable text renderer
                                
                                // Clear any existing text track overrides
                                parametersBuilder.clearOverridesOfType(C.TRACK_TYPE_TEXT);
                                
                                // Set the new track selection override
                                parametersBuilder.addOverride(
                                    new TrackSelectionOverride(group.getMediaTrackGroup(), ImmutableList.of(i))
                                );
                                
                                trackSelector.setParameters(parametersBuilder.build());
            
            
                                response.put("success", true);
                                response.put("message", "Subtitle track set successfully");
                                // response.put("selectedTrack", format.toString());
                                return response;
                            }
                        } catch (JSONException e) {
                            try {
                                response.put("success", false);
                                response.put("error", "Failed to check subtitle track: " + e.getMessage());
                                Log.e(TAG, "Error creating response JSON", e);
                            } catch (JSONException je) {
                                Log.e(TAG, "Error creating response JSON", je);
                            }
                        }
                    }
                }
            }

            try {
                response.put("success", false);
                response.put("message", "Subtitle track not found " + trackId);
            } catch (JSONException e) {
                Log.e(TAG, "Error creating response JSON", e);
            }


        return response;
    }

    // Disable all subtitle tracks
    public JSONObject subTitleOff() {
        JSONObject response = new JSONObject();
        try {
            // DefaultTrackSelector trackSelector = (DefaultTrackSelector) exoPlayer.getTrackSelector();
            
            // DefaultTrackSelector.Parameters.Builder parametersBuilder = trackSelector.getParameters()
            //         .buildUpon()
            //         .setRendererDisabled(C.TRACK_TYPE_TEXT, true); // Disable text renderer
            
            // // Clear any existing text track overrides
            // parametersBuilder.clearOverridesOfType(C.TRACK_TYPE_TEXT);
            // trackSelector.setParameters(parametersBuilder.build());

            // exoPlayer.setTrackSelectionParameters(
            //     exoPlayer.getTrackSelectionParameters()
            //         .buildUpon()
            //         .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, /* disabled= */ true)
            //         .build());

            // Disable subtitles
            TrackSelectionParameters newParameters = exoPlayer.getTrackSelectionParameters()
                    .buildUpon()
                    // .setDisabledTextTrackSelectionFlags(TrackSelectionParameters.FLAG_OVERRIDE_TRACK_SELECTION)
                    .clearOverridesOfType(C.TRACK_TYPE_TEXT) // Clear any subtitle selections
                    .build();

            exoPlayer.setTrackSelectionParameters(newParameters);

            response.put("success", true);
            response.put("message", "Subtitles disabled successfully");
        
        } catch (JSONException e) {
            try {
                response.put("success", false);
                response.put("error", "Failed to disable subtitles: " + e.getMessage());
            } catch (JSONException je) {
                Log.e(TAG, "Error creating response JSON", je);
            }
        }

        return response;
    }

    // Additional method to set comprehensive subtitle styling
    public JSONObject setSubtitleStyle(String subtitle_color, String subtitle_background, String edgeColor, int edgeType) {
        JSONObject response = new JSONObject();
        
        try {
            if (exoView == null) {
                response.put("success", false);
                response.put("message", "Player view not initialized");
                return response;
            }

            SubtitleView subtitleView = exoView.getSubtitleView();
            
            if (subtitleView == null) {
                response.put("success", false);
                response.put("message", "Subtitle view not found");
                return response;
            }

            // // Parse colors
            int textColorInt = getColor(subtitle_color);
            int backgroundColorInt = getColor(subtitle_background);
            int edgeColorInt = getColor(edgeColor);

            // Validate edge type (0=none, 1=outline, 2=drop_shadow, 3=raised, 4=depressed)
            if (edgeType < 0 || edgeType > 4) {
                edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE;
            }

            CaptionStyleCompat captionStyle = new CaptionStyleCompat(
                textColorInt,       // foregroundColor
                backgroundColorInt, // backgroundColor
                Color.TRANSPARENT,  // windowColor
                edgeType,          // edgeType
                edgeColorInt,      // edgeColor
                null               // typeface
            );

            subtitleView.setStyle(captionStyle);

            response.put("success", true);
            response.put("message", "Subtitle style applied successfully");
            response.put("textColor", textColorInt);
            response.put("backgroundColor", backgroundColorInt);
            response.put("edgeColor", edgeColor);
            response.put("edgeType", edgeType);
            
        } catch (IllegalArgumentException e) {
            try {
                response.put("success", false);
                response.put("error", "Invalid color format: " + e.getMessage());
            } catch (JSONException je) {
                Log.e(TAG, "Error creating response JSON", je);
            }
        } catch (JSONException e) {
            try {
                response.put("success", false);
                response.put("error", "Failed to set subtitle style: " + e.getMessage());
            } catch (JSONException je) {
                Log.e(TAG, "Error creating response JSON", je);
            }
        }
        
        return response;
    }

    private int getColor(String color) {
        switch (color) {
            case "transparent":
            case "off":
                return Color.TRANSPARENT;
            case "black":
            case "dark":
                return Color.BLACK;
            case "blue":
                return Color.BLUE;
            case "cyan":
                return Color.CYAN;
            case "darkgray":
                return Color.DKGRAY;
            case "gray":
                return Color.GRAY;
            case "green":
                return Color.GREEN;
            case "lightgray":
                return Color.LTGRAY;
            case "magenta":
                return Color.MAGENTA;
            case "orange":
                // return Color.ORANGE;
                return Color.parseColor("#ffa500");
            case "pink":
                // return Color.PINK;
                return Color.parseColor("#ffc0cb");
            case "red":
                return Color.RED;
            case "white":
                return Color.WHITE;
            case "yellow":
                return Color.YELLOW;
            case "brown":
                return Color.parseColor("#a52a2a");
            default:
                return Color.parseColor(color);
        }
    }

    private void startTimeUpdates() {
        Map<String, String> map = new HashMap<String, String>();
        map.put("message", "startTimeUpdates started");
        JSONObject message = new JSONObject(map);
        
        new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, message, true);

        timeHandler = new Handler();
        timeRunnable = new Runnable() {
            @Override
            public void run() {
                // Map<String, String> map = new HashMap<String, String>();
                // map.put("message", "Timer running");
                // JSONObject message = new JSONObject(map);
                // new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, message, true);

                if (exoPlayer != null && Player.this.callbackContext != null) {
                    if (exoPlayer.getPlayWhenReady() == true) {
                        JSONObject payload = Payload.timeUpdateEvent(Player.this.exoPlayer);
                        new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, payload, true);
                    }
                }
                timeHandler.postDelayed(this, 1000);
            }
        };
        timeHandler.postDelayed(timeRunnable, 1000);
    }

    private void stopTimeUpdates() {
        if (timeHandler != null) {
            timeHandler.removeCallbacks(timeRunnable);
            timeHandler = null;

            Map<String, String> map = new HashMap<String, String>();
            map.put("message", "Timer stopped");
            JSONObject message = new JSONObject(map);
            new CallbackResponse(Player.this.callbackContext).send(PluginResult.Status.OK, message, true);
        }
    }

    protected void onStop() {
        // super.onStop();
        if (exoPlayer != null) {
            exoPlayer.pause(); // or player.pause();
            // If you want to release resources completely:
            // player.release(); 
            // player = null; // Set to null after releasing
        }

        // // Release the player when the activity is no longer visible
        // if (exoPlayer != null) {
        //     exoPlayer.release();
        //     exoPlayer = null; // Set to null after releasing
        // }
    }

    // protected void onCreate(Bundle savedInstanceState) {
    //     // super.onCreate(savedInstanceState);
    //     // Initialize ExoPlayer here
    //     // player = new ExoPlayer.Builder(this).build();
    // }

    protected void onDestroy() {
        // super.onDestroy();
        if (exoPlayer != null) {
            exoPlayer.release();
            exoPlayer = null;
        }
    }

    protected void onPause() {
        // super.onPause();
        if (exoPlayer != null) {
            exoPlayer.pause();
        }
    }

    protected void onResume() {
        // super.onResume();
        if (exoPlayer != null) {
            exoPlayer.play();
        }
    }

    // public void enableAudioByIndex(ExoPlayer player, int trackIndex) {
    //     DefaultTrackSelector trackSelector = (DefaultTrackSelector) player.getTrackSelector();
    //     Tracks currentTracks = player.getCurrentTracks();
        
    //     if (currentTracks == null) return;

    //     for (Tracks.Group group : currentTracks.getGroups()) {
    //         if (group.getType() == C.TRACK_TYPE_AUDIO && trackIndex < group.length) {
    //             // Build parameters to select this audio track
    //             TrackSelectionParameters params = new TrackSelectionParameters.Builder(player.getTrackSelectionParameters())
    //                     .setOverrideForType(new DefaultTrackSelector.SelectionOverride(group.getMediaTrackGroup(), trackIndex))
    //                     .build();
    //             trackSelector.setParameters(params);
    //             break;
    //         }
    //     }
    // }

    // public JSONObject getResolution() {
    //     Log.i(TAG, "getResolution");

    //         int videoWidth = exoPlayer.getVideoSize().width;
    //         int videoHeight = exoPlayer.getVideoSize().height;
    //         JSONObject payload = Payload.resolutionEvent(Player.this.exoPlayer, videoWidth, videoHeight);
    //         return payload;
    // }

    // // If you want to send callback response, create a separate method that takes CallbackContext
    // public static void checkDeviceTypeAndSendCallback(PackageManager packageManager, CallbackContext callbackContext) {
    //     boolean isTelevision = isRunningOnTv(packageManager);
        
    //     Map<String, Object> map = new HashMap<String, Object>();
    //     map.put("isTV", isTelevision);
    //     JSONObject message = new JSONObject(map);
        
    //     new CallbackResponse(callbackContext).send(PluginResult.Status.OK, message, false);
    // }

    // public JSONObject isTV(Context context, CallbackContext callbackContext) {
    //     PackageManager pm = context.getPackageManager();
    //     boolean isTelevision = pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
    //                            pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION);

    //     if (isTelevision) {
    //         Log.d(TAG, "Running on a TV Device");
    //     } else {
    //         Log.d(TAG, "Running on a non-TV Device");
    //     }


    //     Map<String, Object> map = new HashMap<String, Object>();
    //     map.put("isTV", isTelevision ? "true" : "false");
    //     JSONObject message = new JSONObject(map);
    //     new CallbackResponse(callbackContext).send(PluginResult.Status.OK, message, false);
    // }  
}
