package com.foxx.androidcast.receiver;

/*********************************************************************
 * ReceiverPlaybackActivity.java
 * Created at: Fri 15 May 2026 16:49:02 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class ReceiverPlaybackActivity
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (14 commits, 648 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	38e5f45548413568ec9d715352930b3e934665f48cea81e45fb22d80f47875c0
**********************************************************************/
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.widget.ImageButton;
import android.widget.Toast;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.WindowManager;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.ImageView;
import android.widget.TextView;

import com.foxx.androidcast.AppPreferences;
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.DrawerHostActivity;
import com.foxx.androidcast.CastThemeHelper;
import com.foxx.androidcast.PermissionHelper;
import com.foxx.androidcast.PictureInPictureHelper;
import com.foxx.androidcast.ICastReceiverService;
import com.foxx.androidcast.ICastStatusCallback;
import com.foxx.androidcast.R;
import com.foxx.androidcast.receiver.av.ReceiverVideoGlHub;
import com.foxx.androidcast.ui.AspectRatioFrameLayout;
import com.foxx.androidcast.ui.PlaybackTextureTransform;
import com.foxx.androidcast.ui.ZoomPanContainer;

import androidx.core.text.HtmlCompat;

/**
 * Fullscreen display: dancing-robot overlay while listening / waiting;
 * video surface shown only after the first decoded frame.
 */
public class ReceiverPlaybackActivity extends DrawerHostActivity {
    public static final String EXTRA_WIDTH = "video_width";
    public static final String EXTRA_HEIGHT = "video_height";
    public static final String EXTRA_FINISH = "finish_playback";
    public static final String EXTRA_SHOW_AWAITING = "show_awaiting";
    public static final String EXTRA_AWAITING_MESSAGE = "awaiting_message";
    public static final String EXTRA_AUTO_START_LISTENING = "auto_start_listening";

    private AspectRatioFrameLayout aspectContainer;
    private TextureView textureView;
    private View awaitingOverlay;
    private TextView diagnosticsText;
    private ImageView robotView;
    private TextView awaitingMessage;
    private ImageButton pipButton;
    private final Handler mainHandler = new Handler(Looper.getMainLooper());
    /** Sender connected or actively streaming — PiP allowed after permissions. */
    private boolean castSessionActive;
    private boolean inPictureInPicture;
    private SurfaceTexture surfaceTexture;
    private int videoWidth;
    private int videoHeight;
    private boolean streamRendering;
    private boolean surfaceSentToService;
    private AnimatorSet robotAnimator;

    private ICastReceiverService receiverService;
    private boolean bound;
    private boolean autoStartListening;
    private boolean listeningCancelled;

    private final ICastStatusCallback.Stub statusCallback = new ICastStatusCallback.Stub() {
        @Override
        public void onStatus(String status) {}

        @Override
        public void onAuthenticated(String senderName) {
            runOnUiThread(() -> {
                onCastSessionStarted();
                showAwaitingOverlay(R.string.playback_awaiting_stream);
            });
        }

        @Override
        public void onStreamStarted(int width, int height) {
            runOnUiThread(() -> updateVideoSize(width, height));
        }

        @Override
        public void onVideoRendering() {
            runOnUiThread(() -> {
                streamRendering = true;
                hideAwaitingOverlay();
                ZoomPanContainer zoom = findViewById(R.id.zoom_container);
                if (zoom != null) {
                    zoom.resetTransform();
                }
                applyTextureFitCenter();
                updateDiagnosticsVisibility();
            });
        }

        @Override
        public void onStreamIdle() {
            runOnUiThread(() -> {
                streamRendering = false;
                surfaceSentToService = false;
                showAwaitingOverlay(R.string.playback_awaiting_stream);
                updateDiagnosticsVisibility();
            });
        }

        @Override
        public void onDisconnected() {
            runOnUiThread(() -> {
                onCastSessionEnded();
                onStreamLost();
            });
        }
    };

    private final Runnable diagnosticsRefresh = new Runnable() {
        @Override
        public void run() {
            refreshDiagnostics();
            mainHandler.postDelayed(this, 500);
        }
    };

    private final ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            receiverService = ICastReceiverService.Stub.asInterface(service);
            bound = true;
            try {
                receiverService.registerCallback(statusCallback);
            } catch (RemoteException ignored) {
            }
            attachSurfaceIfReady();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            receiverService = null;
            bound = false;
        }
    };

    @Override
    protected void applyActivityTheme() {
        CastThemeHelper.applyTheme(this);
        setTheme(R.style.Theme_AndroidCast_Fullscreen);
    }

    @Override
    protected int getContentLayoutId() {
        return R.layout.activity_playback;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (handleCommandIntent(getIntent())) {
            return;
        }
        applyFullscreen();

        readSizeFromIntent(getIntent());

        ZoomPanContainer zoomContainer = findViewById(R.id.zoom_container);
        aspectContainer = findViewById(R.id.aspect_container);
        textureView = findViewById(R.id.surface_playback);
        zoomContainer.setOnZoomChangedListener(PlaybackViewState::setZoomPercent);
        awaitingOverlay = findViewById(R.id.overlay_awaiting);
        diagnosticsText = findViewById(R.id.text_diagnostics);
        robotView = findViewById(R.id.image_robot);
        awaitingMessage = findViewById(R.id.text_awaiting_message);
        pipButton = findViewById(R.id.button_pip);
        if (pipButton != null) {
            pipButton.setOnClickListener(v -> tryEnterPictureInPicture(true));
        }
        textureView.setSurfaceTextureListener(surfaceListener);
        applyAspectRatio();
        updatePipUi();
        if (AppPreferences.isReceiverPipEnabled(this) && PictureInPictureHelper.isSupported()) {
            PictureInPictureHelper.applyParams(this, 16, 9);
        }

        autoStartListening = getIntent().getBooleanExtra(EXTRA_AUTO_START_LISTENING, false);
        if (autoStartListening) {
            startListeningService();
            showAwaitingOverlay(R.string.playback_listening);
        } else if (getIntent().getBooleanExtra(EXTRA_SHOW_AWAITING, true)) {
            int msg = getIntent().getIntExtra(EXTRA_AWAITING_MESSAGE, R.string.playback_listening);
            showAwaitingOverlay(msg);
        }
    }

    private void startListeningService() {
        String pin = AppPreferences.getPin(this);
        boolean allowAudio = AppPreferences.isPlayIncomingAudio(this);
        ReceiverPrefs.setAllowAudio(this, allowAudio);
        CastSettings settings = AppPreferences.loadReceiverDefaults(this);
        Intent intent = new Intent(this, ReceiverCastService.class);
        intent.setAction(ReceiverCastService.ACTION_START);
        intent.putExtra(ReceiverCastService.EXTRA_PIN, pin);
        intent.putExtra(ReceiverCastService.EXTRA_ALLOW_AUDIO, allowAudio);
        intent.putExtra(CastSettings.EXTRA, settings);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForegroundService(intent);
        } else {
            startService(intent);
        }
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
        if (handleCommandIntent(intent)) {
            return;
        }
        readSizeFromIntent(intent);
        if (videoWidth > 0 && videoHeight > 0 && !streamRendering) {
            surfaceSentToService = false;
        }
        updateVideoSize(videoWidth, videoHeight);
        if (intent.getBooleanExtra(EXTRA_SHOW_AWAITING, false)) {
            int msg = intent.getIntExtra(EXTRA_AWAITING_MESSAGE, R.string.playback_listening);
            showAwaitingOverlay(msg);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (!inPictureInPicture) {
            PermissionHelper.remindForReceiverPlayback(this);
        }
        if (awaitingOverlay != null && awaitingOverlay.getVisibility() == View.VISIBLE) {
            startRobotAnimation();
        }
        mainHandler.post(diagnosticsRefresh);
        updateDiagnosticsVisibility();
        updatePipUi();
    }

    @Override
    public void onUserLeaveHint() {
        super.onUserLeaveHint();
        if (shouldAutoEnterPipOnLeave()) {
            tryEnterPictureInPicture(false);
        }
    }

    @Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode);
        inPictureInPicture = isInPictureInPictureMode;
        applyPipChromeVisibility();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == PermissionHelper.REQUEST_RECEIVER_PLAYBACK) {
            updatePipUi();
        }
    }

    @Override
    protected void onPause() {
        mainHandler.removeCallbacks(diagnosticsRefresh);
        stopRobotAnimation();
        super.onPause();
    }

    private boolean handleCommandIntent(Intent intent) {
        if (intent != null && intent.getBooleanExtra(EXTRA_FINISH, false)) {
            finish();
            return true;
        }
        return false;
    }

    private void readSizeFromIntent(Intent intent) {
        videoWidth = intent.getIntExtra(EXTRA_WIDTH, 0);
        videoHeight = intent.getIntExtra(EXTRA_HEIGHT, 0);
    }

    @Override
    protected void onStart() {
        super.onStart();
        setPlaybackUiVisible(true);
        bindService(new Intent(this, ReceiverCastService.class), connection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        setPlaybackUiVisible(false);
        if (autoStartListening && !listeningCancelled && !castSessionActive && !streamRendering
                && bound && receiverService != null) {
            try {
                receiverService.stopReceiving();
                listeningCancelled = true;
            } catch (RemoteException ignored) {
            }
        }
        if (bound && receiverService != null) {
            try {
                receiverService.unregisterCallback(statusCallback);
            } catch (RemoteException ignored) {
            }
        }
        detachSurface();
        if (bound) {
            unbindService(connection);
            bound = false;
            receiverService = null;
        }
        super.onStop();
    }

    private void setPlaybackUiVisible(boolean visible) {
        Intent intent = new Intent(this, ReceiverCastService.class);
        intent.setAction(ReceiverCastService.ACTION_PLAYBACK_UI);
        intent.putExtra(ReceiverCastService.EXTRA_PLAYBACK_UI_VISIBLE, visible);
        startService(intent);
    }

    private void onStreamLost() {
        streamRendering = false;
        showAwaitingOverlay(R.string.playback_stream_lost);
    }

    private void showAwaitingOverlay(int messageResId) {
        streamRendering = false;
        PlaybackViewState.reset();
        if (awaitingOverlay == null) {
            return;
        }
        awaitingOverlay.setVisibility(View.VISIBLE);
        awaitingOverlay.bringToFront();
        if (awaitingMessage != null) {
            awaitingMessage.setText(messageResId);
        }
        startRobotAnimation();
    }

    private void hideAwaitingOverlay() {
        if (awaitingOverlay != null) {
            awaitingOverlay.setVisibility(View.GONE);
        }
        stopRobotAnimation();
        updateDiagnosticsVisibility();
    }

    private void updateDiagnosticsVisibility() {
        if (diagnosticsText == null) {
            return;
        }
        boolean show = AppPreferences.isShowReceiverDebugOverlay(this) && streamRendering;
        diagnosticsText.setVisibility(show ? View.VISIBLE : View.GONE);
        if (show) {
            diagnosticsText.bringToFront();
        }
    }

    private void refreshDiagnostics() {
        if (diagnosticsText == null || !AppPreferences.isShowReceiverDebugOverlay(this)
                || !bound || receiverService == null) {
            return;
        }
        if (!streamRendering) {
            return;
        }
        try {
            String text = receiverService.getDiagnosticsText();
            if (text != null) {
                diagnosticsText.setText(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY));
            }
        } catch (RemoteException ignored) {
        }
    }

    private void startRobotAnimation() {
        if (robotView == null) {
            return;
        }
        stopRobotAnimation();
        ObjectAnimator bounce = ObjectAnimator.ofFloat(robotView, View.TRANSLATION_Y, 0f, -32f);
        bounce.setDuration(500);
        bounce.setInterpolator(new AccelerateDecelerateInterpolator());
        bounce.setRepeatMode(ValueAnimator.REVERSE);
        bounce.setRepeatCount(ValueAnimator.INFINITE);
        ObjectAnimator wiggle = ObjectAnimator.ofFloat(robotView, View.ROTATION, -12f, 12f);
        wiggle.setDuration(400);
        wiggle.setInterpolator(new AccelerateDecelerateInterpolator());
        wiggle.setRepeatMode(ValueAnimator.REVERSE);
        wiggle.setRepeatCount(ValueAnimator.INFINITE);
        robotAnimator = new AnimatorSet();
        robotAnimator.playTogether(bounce, wiggle);
        robotAnimator.start();
    }

    private void stopRobotAnimation() {
        if (robotAnimator != null) {
            robotAnimator.cancel();
            robotAnimator = null;
        }
        if (robotView != null) {
            robotView.animate().cancel();
            robotView.setTranslationY(0f);
            robotView.setRotation(0f);
        }
    }

    private void updateVideoSize(int width, int height) {
        if (width <= 0 || height <= 0) {
            return;
        }
        videoWidth = width;
        videoHeight = height;
        if (castSessionActive && AppPreferences.isReceiverPipEnabled(this)
                && PermissionHelper.hasReceiverPlaybackReady(this)) {
            PictureInPictureHelper.applyParams(this, videoWidth, videoHeight);
        }
        applyAspectRatio();
        applyTextureFitCenter();
        if (surfaceTexture != null) {
            surfaceTexture.setDefaultBufferSize(videoWidth, videoHeight);
            if (surfaceSentToService) {
                detachSurface();
            }
            attachSurfaceIfReady();
        }
    }

    private void applyTextureFitCenter() {
        if (textureView == null || videoWidth <= 0 || videoHeight <= 0) {
            return;
        }
        if (textureView.getWidth() == 0 || textureView.getHeight() == 0) {
            textureView.post(this::applyTextureFitCenter);
            return;
        }
        PlaybackTextureTransform.applyFitCenter(textureView, videoWidth, videoHeight);
    }

    private void applyFullscreen() {
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        getWindow().getDecorView().setSystemUiVisibility(
                View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                        | View.SYSTEM_UI_FLAG_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
        if (getSupportActionBar() != null) {
            getSupportActionBar().hide();
        }
    }

    private void applyAspectRatio() {
        // Full-screen TextureView; letterboxing via transform only (avoids double-scaling).
        aspectContainer.setAspectRatio(0, 0);
    }

    private final TextureView.SurfaceTextureListener surfaceListener =
            new TextureView.SurfaceTextureListener() {
                @Override
                public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
                    surfaceTexture = texture;
                    if (videoWidth > 0 && videoHeight > 0) {
                        texture.setDefaultBufferSize(videoWidth, videoHeight);
                    }
                    surfaceSentToService = false;
                    attachSurfaceIfReady();
                }

                @Override
                public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {
                    if (videoWidth > 0 && videoHeight > 0) {
                        texture.setDefaultBufferSize(videoWidth, videoHeight);
                    }
                    applyTextureFitCenter();
                    // Rotation often recreates the texture; re-bind decoder to the new Surface.
                    surfaceSentToService = false;
                    attachSurfaceIfReady();
                }

                @Override
                public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
                    surfaceSentToService = false;
                    detachSurface();
                    surfaceTexture = null;
                    return true;
                }

                @Override
                public void onSurfaceTextureUpdated(SurfaceTexture texture) {
                }
            };

    private void attachSurfaceIfReady() {
        if (surfaceSentToService || !bound || receiverService == null || surfaceTexture == null) {
            return;
        }
        try {
            int w = videoWidth > 0 ? videoWidth : 1280;
            int h = videoHeight > 0 ? videoHeight : 720;
            Surface surface = ReceiverVideoGlHub.bindDisplay(this, surfaceTexture, w, h);
            receiverService.attachVideoSurface(surface);
            surfaceSentToService = true;
        } catch (RemoteException ignored) {
        }
    }

    private void detachSurface() {
        if (!surfaceSentToService && receiverService == null) {
            ReceiverVideoGlHub.release(this);
            return;
        }
        if (receiverService != null) {
            try {
                receiverService.detachVideoSurface();
            } catch (RemoteException ignored) {
            }
        }
        surfaceSentToService = false;
        ReceiverVideoGlHub.release(this);
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus && !inPictureInPicture) {
            applyFullscreen();
        }
    }

    private void onCastSessionStarted() {
        castSessionActive = true;
        if (AppPreferences.isReceiverPipEnabled(this)) {
            PermissionHelper.remindForReceiverPlayback(this);
        }
        updatePipUi();
    }

    private void onCastSessionEnded() {
        castSessionActive = false;
        updatePipUi();
    }

    private void updatePipUi() {
        if (pipButton == null) {
            return;
        }
        boolean sessionVisible = castSessionActive || streamRendering
                || (awaitingOverlay != null && awaitingOverlay.getVisibility() == View.VISIBLE);
        boolean show = sessionVisible && AppPreferences.isReceiverPipEnabled(this)
                && PictureInPictureHelper.isSupported()
                && PermissionHelper.hasReceiverPlaybackReady(this);
        pipButton.setVisibility(show ? View.VISIBLE : View.GONE);
        if (show) {
            pipButton.bringToFront();
        }
    }

    /** Auto PiP when leaving the app during an active cast, not while waiting for a sender. */
    private boolean shouldAutoEnterPipOnLeave() {
        if (!AppPreferences.isReceiverPipEnabled(this)
                || !PermissionHelper.hasReceiverPlaybackReady(this)) {
            return false;
        }
        return castSessionActive || streamRendering;
    }

    private void tryEnterPictureInPicture(boolean userInitiated) {
        if (!AppPreferences.isReceiverPipEnabled(this)) {
            return;
        }
        if (!castSessionActive && !streamRendering
                && (awaitingOverlay == null || awaitingOverlay.getVisibility() != View.VISIBLE)) {
            if (userInitiated) {
                Toast.makeText(this, R.string.pip_unavailable, Toast.LENGTH_SHORT).show();
            }
            return;
        }
        if (!PermissionHelper.hasReceiverPlaybackReady(this)) {
            if (userInitiated) {
                PermissionHelper.remindForReceiverPlayback(this);
            }
            return;
        }
        if (!PictureInPictureHelper.isSupported()) {
            if (userInitiated) {
                Toast.makeText(this, R.string.pip_unavailable, Toast.LENGTH_SHORT).show();
            }
            return;
        }
        if (!PictureInPictureHelper.isAllowedForApp(this)) {
            if (userInitiated) {
                PictureInPictureHelper.showEnableInSettingsDialog(this);
            }
            return;
        }
        if (!PictureInPictureHelper.enter(this, videoWidth, videoHeight) && userInitiated) {
            PictureInPictureHelper.showEnableInSettingsDialog(this);
        }
    }

    private void applyPipChromeVisibility() {
        View handle = findViewById(R.id.settings_drawer_handle);
        if (handle != null) {
            handle.setVisibility(inPictureInPicture ? View.GONE : View.VISIBLE);
        }
        if (pipButton != null) {
            boolean showPip = (castSessionActive || streamRendering
                    || (awaitingOverlay != null && awaitingOverlay.getVisibility() == View.VISIBLE))
                    && AppPreferences.isReceiverPipEnabled(this)
                    && PictureInPictureHelper.isSupported()
                    && PermissionHelper.hasReceiverPlaybackReady(this);
            pipButton.setVisibility(inPictureInPicture || !showPip ? View.GONE : View.VISIBLE);
        }
        if (inPictureInPicture) {
            if (diagnosticsText != null) {
                diagnosticsText.setVisibility(View.GONE);
            }
            if (awaitingOverlay != null && streamRendering) {
                awaitingOverlay.setVisibility(View.GONE);
            }
            stopRobotAnimation();
        } else {
            updateDiagnosticsVisibility();
            if (!streamRendering && awaitingOverlay != null
                    && awaitingOverlay.getVisibility() == View.VISIBLE) {
                startRobotAnimation();
            }
            if (castSessionActive) {
                applyFullscreen();
            }
            updatePipUi();
        }
    }
}
