package com.foxx.androidcast.sender;

/*********************************************************************
 * SenderActivity.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 SenderActivity
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (11 commits, 530 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	082e2d349e90cdbd8693d4c8b6ec2b074963fe63626c33e8305353007871389c
**********************************************************************/
import android.app.Activity;
import android.content.Intent;
import android.media.projection.MediaProjectionConfig;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import androidx.activity.OnBackPressedCallback;
import androidx.appcompat.app.AlertDialog;

import com.foxx.androidcast.AppPreferences;
import com.foxx.androidcast.DrawerHostActivity;
import com.foxx.androidcast.CastActiveState;
import com.foxx.androidcast.CastActivityTracker;
import com.foxx.androidcast.CastConfig;
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.PermissionHelper;
import com.foxx.androidcast.PlatformCastSupport;
import com.foxx.androidcast.R;
import com.foxx.androidcast.live.LiveCastPeerTracker;
import com.foxx.androidcast.discovery.DiscoveryManager;
import com.foxx.androidcast.discovery.PinnedReceiversStore;
import com.foxx.androidcast.discovery.ReceiverCompatibility;
import com.google.android.material.switchmaterial.SwitchMaterial;

public class SenderActivity extends DrawerHostActivity {
    private static final int REQUEST_MEDIA_PROJECTION = 2001;
    private static final int REQUEST_PREVIEW_PROJECTION = 2002;

    private final CastSettings castSettings = new CastSettings();
    private final Handler handler = new Handler(Looper.getMainLooper());
    private DiscoveryManager discovery;
    private DeviceListAdapter deviceAdapter;
    private SenderCapturePreview capturePreview;
    private SenderScreenPreview screenPreview;

    private TextView statusText;
    private ProgressBar discoveryProgress;
    private EditText pinInput;
    private SwitchMaterial muteSwitch;
    private Button startButton;
    private boolean pinLocked;

    private enum DiscoveryPhase {
        IDLE, SCANNING, PAUSED
    }

    private DiscoveryPhase discoveryPhase = DiscoveryPhase.IDLE;
    private int discoverySecondsLeft;
    private boolean lastScanBurst;
    private final Runnable autoScanRunnable = () -> beginScanPass(false);

    private String pendingPin;
    private String pendingHost;
    private int pendingPort;
    private String pendingDeviceName;
    private java.util.ArrayList<CastReceiverTarget> pendingTargets;

    private boolean pendingCameraLaunch;
    private boolean previewConsentDeclined;
    private boolean previewConsentInFlight;
    private final java.util.List<DiscoveryManager.DiscoveredDevice> lastDiscovered =
            new java.util.ArrayList<>();

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        PermissionHelper.request(this);

        castSettings.copyFrom(AppPreferences.loadSenderDefaults(this));
        screenPreview = new SenderScreenPreview(this);

        statusText = findViewById(R.id.text_status);
        pinInput = findViewById(R.id.edit_pin);
        muteSwitch = findViewById(R.id.switch_mute);
        startButton = findViewById(R.id.btn_start_cast);
        discoveryProgress = findViewById(R.id.progress_discovery);

        pinInput.setText(AppPreferences.getPin(this));
        muteSwitch.setChecked(!castSettings.isAudioRequested());
        muteSwitch.setOnCheckedChangeListener((btn, muted) ->
                castSettings.setAudioEnabled(!muted));

        capturePreview = new SenderCapturePreview(
                findViewById(R.id.texture_preview),
                findViewById(R.id.text_preview_label),
                screenPreview);
        View.OnClickListener requestPreviewConsent = v -> {
            if (screenPreview.needsProjection(castSettings.getCaptureMode())) {
                previewConsentDeclined = false;
                maybeRequestPreviewConsent();
            }
        };
        findViewById(R.id.text_preview_label).setOnClickListener(requestPreviewConsent);
        findViewById(R.id.texture_preview).setOnClickListener(requestPreviewConsent);
        ListView listView = findViewById(R.id.list_devices);
        listView.setChoiceMode(CastConfig.MULTI_RECEIVER_ENABLED
                ? ListView.CHOICE_MODE_MULTIPLE : ListView.CHOICE_MODE_SINGLE);
        deviceAdapter = new DeviceListAdapter(this, CastConfig.MULTI_RECEIVER_ENABLED);
        listView.setAdapter(deviceAdapter);
        listView.setOnItemClickListener((parent, view, position, id) -> {
            deviceAdapter.setSelectedPosition(position);
            java.util.List<DiscoveryManager.DiscoveredDevice> selectedList =
                    deviceAdapter.getSelectedDevices();
            if (selectedList.isEmpty()) {
                return;
            }
            if (!CastConfig.MULTI_RECEIVER_ENABLED) {
                for (DiscoveryManager.DiscoveredDevice selected : selectedList) {
                    if (!castSettings.getTransport().equals(selected.transport)) {
                        Toast.makeText(this, R.string.transport_mismatch, Toast.LENGTH_LONG).show();
                        break;
                    }
                }
            }
            if (selectedList.size() == 1) {
                DiscoveryManager.DiscoveredDevice selected = selectedList.get(0);
                statusText.setText(getString(R.string.device_selected, selected.name, selected.host,
                        selected.transport.toUpperCase()));
            } else {
                statusText.setText(getString(R.string.devices_selected, selectedList.size()));
            }
        });

        discovery = new DiscoveryManager(this);
        discovery.setListener(new DiscoveryManager.Listener() {
            @Override
            public void onDevicesUpdated(java.util.List<DiscoveryManager.DiscoveredDevice> deviceList) {
                lastDiscovered.clear();
                lastDiscovered.addAll(deviceList);
                publishFilteredDevices();
            }

            @Override
            public void onScanStarted() {
                discoveryPhase = DiscoveryPhase.SCANNING;
                long passMs = lastScanBurst
                        ? CastConfig.DISCOVERY_BURST_MS : CastConfig.DISCOVERY_SCAN_PASS_MS;
                discoverySecondsLeft = Math.max(1, (int) (passMs / 1000));
                discoveryProgress.setVisibility(View.VISIBLE);
            }

            @Override
            public void onScanEnded() {
                discoveryPhase = DiscoveryPhase.PAUSED;
                discoverySecondsLeft = (int) (CastConfig.DISCOVERY_PAUSE_MS / 1000);
                discoveryProgress.setVisibility(View.GONE);
                handler.removeCallbacks(autoScanRunnable);
                handler.postDelayed(autoScanRunnable, CastConfig.DISCOVERY_PAUSE_MS);
            }
        });

        getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
            @Override
            public void handleOnBackPressed() {
                if (CastActiveState.isSenderCasting()) {
                    Toast.makeText(SenderActivity.this, R.string.cast_in_progress_pin_locked,
                            Toast.LENGTH_SHORT).show();
                } else {
                    finish();
                }
            }
        });

        startButton.setOnClickListener(v ->
                PlatformCastSupport.runWithAudioPolicy(this, castSettings, this::startCastFlow));

        findViewById(R.id.panel_preview).post(() -> {
            refreshPreview();
            maybeRequestPreviewConsent();
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        castSettings.copyFrom(AppPreferences.loadSenderDefaults(this));
        publishFilteredDevices();
        muteSwitch.setChecked(!castSettings.isAudioRequested());
        pinLocked = CastActiveState.isSenderCasting();
        pinInput.setEnabled(!pinLocked);
        startButton.setEnabled(!pinLocked);
        muteSwitch.setEnabled(!pinLocked);
        if (pinLocked) {
            Toast.makeText(this, R.string.cast_in_progress_pin_locked, Toast.LENGTH_SHORT).show();
        }
        refreshPreview();
        if (screenPreview.needsProjection(castSettings.getCaptureMode()) && screenPreview.hasConsent()) {
            findViewById(R.id.panel_preview).post(this::refreshPreview);
        } else if (!previewConsentDeclined && !previewConsentInFlight) {
            findViewById(R.id.panel_preview).post(this::maybeRequestPreviewConsent);
        }
        PermissionHelper.remindIfMissing(this, castSettings.isAudioEnabled());
        LiveCastPeerTracker.start(this, (deviceIds, usernames) -> runOnUiThread(() -> {
            if (deviceAdapter != null) {
                deviceAdapter.setLiveIndicators(deviceIds, usernames);
            }
        }));
    }

    private void publishFilteredDevices() {
        if (deviceAdapter == null) {
            return;
        }
        java.util.LinkedHashMap<String, DiscoveryManager.DiscoveredDevice> merged =
                new java.util.LinkedHashMap<>();
        for (DiscoveryManager.DiscoveredDevice d : PinnedReceiversStore.load(this)) {
            if (ReceiverCompatibility.isCompatible(d, castSettings)) {
                merged.put(d.host + ":" + d.port, d);
            }
        }
        for (DiscoveryManager.DiscoveredDevice d : lastDiscovered) {
            if (!ReceiverCompatibility.isCompatible(d, castSettings)) {
                continue;
            }
            String key = d.host + ":" + d.port;
            DiscoveryManager.DiscoveredDevice prev = merged.get(key);
            if (prev != null && prev.pinnedRecent) {
                d.pinnedRecent = true;
            }
            merged.put(key, d);
        }
        java.util.List<DiscoveryManager.DiscoveredDevice> compatible =
                new java.util.ArrayList<>(merged.values());
        deviceAdapter.setDevices(compatible);
        if (statusText == null) {
            return;
        }
        statusText.setText(compatible.isEmpty()
                ? (lastDiscovered.isEmpty()
                        ? getString(R.string.searching)
                        : getString(R.string.no_compatible_receivers))
                : getString(R.string.found_receivers, compatible.size()));
    }

    private void refreshPreview() {
        if (capturePreview == null) {
            return;
        }
        capturePreview.updateForMode(castSettings.getCaptureMode());
    }

    private void maybeRequestPreviewConsent() {
        if (previewConsentDeclined || previewConsentInFlight) {
            return;
        }
        CastSettings.CaptureMode mode = castSettings.getCaptureMode();
        if (!screenPreview.needsProjection(mode) || screenPreview.hasConsent()) {
            return;
        }
        previewConsentInFlight = true;
        MediaProjectionManager mgr = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
        startActivityForResult(createScreenCaptureIntent(mgr), REQUEST_PREVIEW_PROJECTION);
    }

    @Override
    protected void onPause() {
        LiveCastPeerTracker.stop();
        if (capturePreview != null) {
            capturePreview.pause();
        }
        super.onPause();
    }

    @Override
    protected void onDestroy() {
        if (capturePreview != null) {
            capturePreview.stop();
        }
        if (screenPreview != null) {
            screenPreview.releaseConsent();
        }
        super.onDestroy();
    }

    @Override
    protected void onStart() {
        super.onStart();
        beginScanPass(false);
    }

    @Override
    protected void onStop() {
        handler.removeCallbacks(autoScanRunnable);
        if (discovery != null) {
            discovery.stop();
        }
        discoveryPhase = DiscoveryPhase.IDLE;
        super.onStop();
    }

    private void beginScanPass(boolean userInitiated) {
        if (discovery != null && discovery.isBrowsing()) {
            return;
        }
        if (userInitiated) {
            deviceAdapter.clearDevices();
            statusText.setText(R.string.rescanning);
        } else if (deviceAdapter.getCount() == 0) {
            statusText.setText(R.string.searching);
        }
        if (discovery != null) {
            discovery.stop();
            if (userInitiated) {
                discovery.clearDevices();
            }
        }
        lastScanBurst = userInitiated;
        discovery.startBrowsing(userInitiated);
    }

    private void startCastFlow() {
        PermissionHelper.remindIfMissing(this, castSettings.isAudioEnabled());
        if (!PermissionHelper.hasAll(this)) {
            return;
        }
        if (castSettings.isPassthrough()) {
            Toast.makeText(this, R.string.codec_passthrough_unavailable, Toast.LENGTH_LONG).show();
            return;
        }

        String pin = pinInput.getText().toString().trim();
        if (!AppPreferences.getPin(this).equals(pin)) {
            AppPreferences.setPin(this, pin);
        }

        java.util.List<DiscoveryManager.DiscoveredDevice> selectedList =
                deviceAdapter.getSelectedDevices();
        if (selectedList.isEmpty()) {
            Toast.makeText(this, R.string.select_receiver, Toast.LENGTH_SHORT).show();
            return;
        }
        for (DiscoveryManager.DiscoveredDevice d : selectedList) {
            if (!d.pinMatches(pin)) {
                showPinMismatchDialog(selectedList);
                return;
            }
        }

        if (CastConfig.MULTI_RECEIVER_ENABLED && selectedList.size() > CastConfig.MAX_RECEIVERS) {
            Toast.makeText(this, getString(R.string.too_many_receivers, CastConfig.MAX_RECEIVERS),
                    Toast.LENGTH_LONG).show();
            return;
        }

        pendingTargets = new java.util.ArrayList<>();
        for (DiscoveryManager.DiscoveredDevice d : selectedList) {
            if (!CastConfig.MULTI_RECEIVER_ENABLED
                    && !castSettings.getTransport().equals(d.transport)) {
                Toast.makeText(this, R.string.transport_mismatch, Toast.LENGTH_LONG).show();
                return;
            }
            PinnedReceiversStore.recordSuccess(this, d);
            pendingTargets.add(new CastReceiverTarget(d.name, d.host, d.port, d.transport,
                    d.videoCodecs, d.audioCodecs, d.placeholderId));
        }
        DiscoveryManager.DiscoveredDevice first = selectedList.get(0);
        pendingHost = first.host;
        pendingPort = first.port;
        pendingDeviceName = selectedList.size() > 1
                ? first.name + " +" + (selectedList.size() - 1) : first.name;
        pendingPin = pin;

        proceedCaptureConsent();
    }

    private void showPinMismatchDialog(java.util.List<DiscoveryManager.DiscoveredDevice> selectedList) {
        final EditText input = new EditText(this);
        input.setHint(R.string.pin_session_hint);
        input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER
                | android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD);
        new AlertDialog.Builder(this)
                .setTitle(R.string.pin_mismatch_title)
                .setMessage(R.string.pin_mismatch_message)
                .setView(input)
                .setPositiveButton(android.R.string.ok, (d, w) -> {
                    String sessionPin = input.getText().toString().trim();
                    pinInput.setText(sessionPin);
                    for (DiscoveryManager.DiscoveredDevice dev : selectedList) {
                        if (!dev.pinMatches(sessionPin)) {
                            Toast.makeText(this, R.string.pin_mismatch_title, Toast.LENGTH_LONG).show();
                            return;
                        }
                    }
                    pendingPin = sessionPin;
                    pendingTargets = new java.util.ArrayList<>();
                    for (DiscoveryManager.DiscoveredDevice dev : selectedList) {
                        pendingTargets.add(new CastReceiverTarget(dev.name, dev.host, dev.port,
                                dev.transport, dev.videoCodecs, dev.audioCodecs, dev.placeholderId));
                    }
                    DiscoveryManager.DiscoveredDevice first = selectedList.get(0);
                    pendingHost = first.host;
                    pendingPort = first.port;
                    pendingDeviceName = selectedList.size() > 1
                            ? first.name + " +" + (selectedList.size() - 1) : first.name;
                    proceedCaptureConsent();
                })
                .setNegativeButton(android.R.string.cancel, null)
                .show();
    }

    private void proceedCaptureConsent() {
        if (castSettings.getCaptureMode() == CastSettings.CaptureMode.CALIBRATION_TEST) {
            launchCastService(null, Activity.RESULT_OK);
            return;
        }
        if (castSettings.isCameraCapture()) {
            if (androidx.core.content.ContextCompat.checkSelfPermission(this,
                    android.Manifest.permission.CAMERA) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
                androidx.core.app.ActivityCompat.requestPermissions(this,
                        new String[] {android.Manifest.permission.CAMERA}, PermissionHelper.REQUEST_CODE);
                pendingCameraLaunch = true;
                return;
            }
            launchCastService(null, Activity.RESULT_OK);
            return;
        }
        if (screenPreview.hasConsent()) {
            screenPreview.pauseForCastStart();
            launchCastService(screenPreview.getProjectionData(), screenPreview.getProjectionResultCode());
            return;
        }
        MediaProjectionManager mgr = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
        startActivityForResult(createScreenCaptureIntent(mgr), REQUEST_MEDIA_PROJECTION);
    }

    private android.content.Intent createScreenCaptureIntent(MediaProjectionManager mgr) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
                && castSettings.getCaptureMode() == CastSettings.CaptureMode.USER_CHOICE) {
            MediaProjectionConfig config = MediaProjectionConfig.createConfigForUserChoice();
            return mgr.createScreenCaptureIntent(config);
        }
        return mgr.createScreenCaptureIntent();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == PermissionHelper.REQUEST_CODE && pendingCameraLaunch) {
            pendingCameraLaunch = false;
            if (grantResults.length > 0 && grantResults[0] == android.content.pm.PackageManager.PERMISSION_GRANTED) {
                launchCastService(null, Activity.RESULT_OK);
            } else {
                Toast.makeText(this, R.string.capture_camera_permission_denied, Toast.LENGTH_LONG).show();
            }
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_PREVIEW_PROJECTION) {
            previewConsentInFlight = false;
            if (resultCode == Activity.RESULT_OK && data != null) {
                screenPreview.setProjectionResult(resultCode, data);
                previewConsentDeclined = false;
                findViewById(R.id.panel_preview).post(() -> {
                    refreshPreview();
                    findViewById(R.id.panel_preview).postDelayed(this::refreshPreview, 200);
                });
            } else {
                previewConsentDeclined = true;
                refreshPreview();
            }
            return;
        }
        if (requestCode != REQUEST_MEDIA_PROJECTION) {
            return;
        }
        if (resultCode != Activity.RESULT_OK || pendingHost == null) {
            Toast.makeText(this, R.string.capture_denied, Toast.LENGTH_SHORT).show();
            return;
        }
        screenPreview.setProjectionResult(resultCode, data);
        launchCastService(data, resultCode);
    }

    private void launchCastService(Intent projectionData, int resultCode) {
        if (screenPreview != null) {
            screenPreview.pauseForCastStart();
        }
        pinLocked = true;
        pinInput.setEnabled(false);
        startButton.setEnabled(false);
        muteSwitch.setEnabled(false);

        Intent serviceIntent = new Intent(this, ScreenCastService.class);
        serviceIntent.putExtra(ScreenCastService.EXTRA_RESULT_CODE, resultCode);
        if (projectionData != null) {
            serviceIntent.putExtra(ScreenCastService.EXTRA_RESULT_DATA, new Intent(projectionData));
        }
        serviceIntent.putExtra(ScreenCastService.EXTRA_HOST, pendingHost);
        serviceIntent.putExtra(ScreenCastService.EXTRA_PORT, pendingPort);
        serviceIntent.putExtra(ScreenCastService.EXTRA_PIN, pendingPin != null ? pendingPin : "");
        serviceIntent.putExtra(ScreenCastService.EXTRA_DEVICE_NAME, pendingDeviceName);
        serviceIntent.putExtra(ScreenCastService.EXTRA_SENDER_NAME, AppPreferences.getUsername(this));
        serviceIntent.putExtra(CastSettings.EXTRA, castSettings);
        if (castSettings.getCaptureMode() == CastSettings.CaptureMode.CALIBRATION_TEST) {
            serviceIntent.putExtra(ScreenCastService.EXTRA_CALIBRATION, true);
        }
        if (pendingTargets != null && !pendingTargets.isEmpty()) {
            serviceIntent.putExtra(ScreenCastService.EXTRA_TARGETS, pendingTargets);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForegroundService(serviceIntent);
        } else {
            startService(serviceIntent);
        }
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        Toast.makeText(this, R.string.sender_check_notification, Toast.LENGTH_LONG).show();
        moveTaskToBack(true);
    }
}
