/*
 * CS Ranging Watch App - Minimal
 *
 * Simple BLE Central that connects to Silicon Labs CS SoC.
 * The SoC handles all CS Initiator logic - we just connect.
 */

package com.novelbits.csrangingwatch;

import android.Manifest;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;

import java.util.HashSet;
import java.util.Set;

@SuppressLint("MissingPermission")
public class MainActivity extends Activity {
    private static final String TAG = "CS_Watch";
    private static final int REQUEST_PERMISSIONS = 100;
    private static final int POST_PAIRING_DELAY_MS = 500;

    // Device name to scan for
    private static final String TARGET_DEVICE_NAME = "Silabs Example";

    // Icon colors for each state
    private static final int COLOR_IDLE = 0xFF757575;       // Gray
    private static final int COLOR_BLUETOOTH_OFF = 0xFFFF7043; // Orange
    private static final int COLOR_SCANNING = 0xFF42A5F5;   // Blue
    private static final int COLOR_CONNECTING = 0xFF1E88E5; // Darker blue
    private static final int COLOR_CONNECTED = 0xFF66BB6A;  // Green
    private static final int COLOR_DISCONNECTED = 0xFFEF5350; // Red
    private static final int COLOR_PAIRING_FAILED = 0xFFEF5350; // Red

    // Delay before returning to idle after disconnect/failure
    private static final int DISCONNECT_DISPLAY_MS = 1500;
    private static final int PAIRING_FAILED_DISPLAY_MS = 4000;  // Longer to read message
    private static final int CONNECTION_TIMEOUT_MS = 15000;     // 15 second timeout
    private static final int QUICK_DISCONNECT_THRESHOLD_MS = 3000; // Disconnect within 3s = bond failure

    // UI
    private FrameLayout mRootLayout;
    private ImageView mStatusIcon;
    private TextView mStatusText;
    private TextView mDeviceNameText;

    // Bluetooth
    private BluetoothManager mBluetoothManager;
    private BluetoothAdapter mBluetoothAdapter;
    private BluetoothLeScanner mScanner;
    private BluetoothGatt mGattClient;
    private BluetoothDevice mConnectedDevice;  // Track the connected device
    private Set<String> mSeenDevices = new HashSet<>();
    private BluetoothDevice mPendingBondDevice;

    // State
    private enum State { IDLE, BLUETOOTH_OFF, SCANNING, CONNECTING, CONNECTED, DISCONNECTED, PAIRING_FAILED }
    private State mState = State.IDLE;

    // Connection timeout runnable
    private Runnable mConnectionTimeoutRunnable;

    // Track when connection was established (to detect quick disconnects)
    private long mConnectionTimestamp = 0;

    // Handler for delayed operations
    private Handler mHandler = new Handler(Looper.getMainLooper());

    // Animation
    private AnimatorSet mCurrentAnimation;

    // Bond state receiver
    private BroadcastReceiver mBondReceiver;

    // Bluetooth state receiver (for BT toggle detection)
    private BroadcastReceiver mBluetoothStateReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Initialize UI
        mRootLayout = findViewById(R.id.root_layout);
        mStatusIcon = findViewById(R.id.status_icon);
        mStatusText = findViewById(R.id.status_text);
        mDeviceNameText = findViewById(R.id.device_name_text);

        // Initialize Bluetooth
        mBluetoothManager = getSystemService(BluetoothManager.class);
        mBluetoothAdapter = mBluetoothManager.getAdapter();
        mScanner = mBluetoothAdapter.getBluetoothLeScanner();

        // Setup receivers
        setupBondReceiver();
        setupBluetoothStateReceiver();

        // Tap anywhere to start/stop
        mRootLayout.setOnClickListener(v -> onScreenTap());

        // Set initial state based on Bluetooth status
        updateStateForBluetoothStatus();
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume - state: " + mState);
        // Check Bluetooth and connection state when app comes to foreground
        updateStateForBluetoothStatus();
        verifyConnectionState();
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "onPause - state: " + mState);
        // Note: Not stopping scan here - letting Android manage background scanning
    }

    /**
     * Update state based on Bluetooth adapter status.
     */
    private void updateStateForBluetoothStatus() {
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            setState(State.BLUETOOTH_OFF);
        } else if (mState == State.BLUETOOTH_OFF) {
            // BT is now on, transition to IDLE
            setState(State.IDLE);
        }
    }

    /**
     * Check if we think we're connected but actually aren't.
     * This handles cases where disconnect happened while app was backgrounded.
     */
    private void verifyConnectionState() {
        if (mState == State.CONNECTED && mConnectedDevice != null) {
            int actualState = mBluetoothManager.getConnectionState(mConnectedDevice, BluetoothProfile.GATT);
            if (actualState != BluetoothProfile.STATE_CONNECTED) {
                Log.d(TAG, "Connection lost while backgrounded, resetting state");
                cleanupGatt();
                setState(State.IDLE);
            }
        }
    }

    /**
     * Clean up GATT client and connection state.
     */
    private void cleanupGatt() {
        if (mGattClient != null) {
            mGattClient.close();
            mGattClient = null;
        }
        mConnectedDevice = null;
        mConnectionTimestamp = 0;
    }

    /**
     * Setup receiver to detect Bluetooth adapter state changes (on/off).
     */
    private void setupBluetoothStateReceiver() {
        mBluetoothStateReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) return;

                int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
                if (state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_TURNING_OFF) {
                    Log.d(TAG, "Bluetooth turned off");
                    cleanupGatt();
                    stopScan();
                    setState(State.BLUETOOTH_OFF);
                } else if (state == BluetoothAdapter.STATE_ON) {
                    Log.d(TAG, "Bluetooth turned on");
                    // Bluetooth back on - reinitialize scanner and go to IDLE
                    mScanner = mBluetoothAdapter.getBluetoothLeScanner();
                    setState(State.IDLE);
                }
            }
        };

        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        registerReceiver(mBluetoothStateReceiver, filter, Context.RECEIVER_EXPORTED);
    }

    private void onScreenTap() {
        if (mState == State.IDLE) {
            if (checkPermissions()) {
                startScan();
            }
        }
        // Ignore taps while scanning, connecting, or connected
        // Disconnection happens at link layer - we don't control it
    }

    private void setState(State newState) {
        Log.d(TAG, "setState: " + mState + " -> " + newState);
        mState = newState;
        runOnUiThread(() -> {
            // Stop any existing animation
            stopAnimation();

            switch (newState) {
                case IDLE:
                    mStatusText.setTextSize(18);  // Reset to default size
                    mStatusText.setText("TAP TO START");
                    mStatusIcon.setColorFilter(COLOR_IDLE);
                    mDeviceNameText.setVisibility(View.GONE);
                    startBreathingAnimation();
                    break;
                case BLUETOOTH_OFF:
                    mStatusText.setText("Turn on\nBluetooth");
                    mStatusIcon.setColorFilter(COLOR_BLUETOOTH_OFF);
                    mDeviceNameText.setVisibility(View.GONE);
                    break;
                case SCANNING:
                    mStatusText.setText("Scanning...");
                    mStatusIcon.setColorFilter(COLOR_SCANNING);
                    mDeviceNameText.setVisibility(View.GONE);
                    break;
                case CONNECTING:
                    mStatusText.setText("Connecting...");
                    mStatusIcon.setColorFilter(COLOR_CONNECTING);
                    mDeviceNameText.setVisibility(View.GONE);
                    break;
                case CONNECTED:
                    mStatusText.setText("Connected");
                    mStatusIcon.setColorFilter(COLOR_CONNECTED);
                    if (mConnectedDevice != null) {
                        String name = mConnectedDevice.getName();
                        if (name != null && !name.isEmpty()) {
                            mDeviceNameText.setText(name);
                            mDeviceNameText.setVisibility(View.VISIBLE);
                        }
                    }
                    startHeartbeatAnimation();
                    break;
                case DISCONNECTED:
                    mStatusText.setText("Disconnected");
                    mStatusIcon.setColorFilter(COLOR_DISCONNECTED);
                    mDeviceNameText.setVisibility(View.GONE);
                    break;
                case PAIRING_FAILED:
                    mStatusText.setTextSize(14);
                    mStatusText.setText("Pairing Failed\n\nTry removing device\nfrom Bluetooth settings");
                    mStatusIcon.setColorFilter(COLOR_PAIRING_FAILED);
                    mDeviceNameText.setVisibility(View.GONE);
                    break;
            }
        });
    }

    /**
     * Slow breathing animation for idle state - subtle scale pulse
     */
    private void startBreathingAnimation() {
        ObjectAnimator scaleX = ObjectAnimator.ofFloat(mStatusIcon, "scaleX", 1f, 1.08f, 1f);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(mStatusIcon, "scaleY", 1f, 1.08f, 1f);
        ObjectAnimator alpha = ObjectAnimator.ofFloat(mStatusIcon, "alpha", 1f, 0.7f, 1f);

        mCurrentAnimation = new AnimatorSet();
        mCurrentAnimation.playTogether(scaleX, scaleY, alpha);
        mCurrentAnimation.setDuration(3000);
        mCurrentAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
        mCurrentAnimation.addListener(new android.animation.AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(android.animation.Animator animation) {
                if (mState == State.IDLE && mCurrentAnimation != null) {
                    mCurrentAnimation.start();
                }
            }
        });
        mCurrentAnimation.start();
    }

    /**
     * Gentle heartbeat animation for connected state
     */
    private void startHeartbeatAnimation() {
        ObjectAnimator scaleX = ObjectAnimator.ofFloat(mStatusIcon, "scaleX", 1f, 1.1f, 1f, 1.05f, 1f);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(mStatusIcon, "scaleY", 1f, 1.1f, 1f, 1.05f, 1f);

        mCurrentAnimation = new AnimatorSet();
        mCurrentAnimation.playTogether(scaleX, scaleY);
        mCurrentAnimation.setDuration(1500);
        mCurrentAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
        mCurrentAnimation.setStartDelay(500);
        mCurrentAnimation.addListener(new android.animation.AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(android.animation.Animator animation) {
                if (mState == State.CONNECTED && mCurrentAnimation != null) {
                    mCurrentAnimation.setStartDelay(1000);
                    mCurrentAnimation.start();
                }
            }
        });
        mCurrentAnimation.start();
    }

    /**
     * Stop current animation and reset icon scale
     */
    private void stopAnimation() {
        if (mCurrentAnimation != null) {
            mCurrentAnimation.cancel();
            mCurrentAnimation = null;
        }
        mStatusIcon.setScaleX(1f);
        mStatusIcon.setScaleY(1f);
        mStatusIcon.setAlpha(1f);
    }

    private void setupBondReceiver() {
        mBondReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (!BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) return;

                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
                int prevBondState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1);

                String bondStr = bondState == BluetoothDevice.BOND_NONE ? "NONE" :
                                bondState == BluetoothDevice.BOND_BONDING ? "BONDING" :
                                bondState == BluetoothDevice.BOND_BONDED ? "BONDED" : "UNKNOWN";
                String prevBondStr = prevBondState == BluetoothDevice.BOND_NONE ? "NONE" :
                                    prevBondState == BluetoothDevice.BOND_BONDING ? "BONDING" :
                                    prevBondState == BluetoothDevice.BOND_BONDED ? "BONDED" : "UNKNOWN";
                Log.d(TAG, "Bond state changed: " + device.getName() + " [" + device.getAddress() + "] " + prevBondStr + " -> " + bondStr);

                if (mPendingBondDevice == null || !device.getAddress().equals(mPendingBondDevice.getAddress())) {
                    Log.d(TAG, "Ignoring bond change - not our pending device");
                    return;
                }

                if (bondState == BluetoothDevice.BOND_BONDED) {
                    Log.d(TAG, "Bond completed successfully");
                    // Bonded - connect GATT
                    BluetoothDevice deviceToConnect = mPendingBondDevice;
                    mPendingBondDevice = null;
                    connectGatt(deviceToConnect);
                } else if (bondState == BluetoothDevice.BOND_NONE && prevBondState == BluetoothDevice.BOND_BONDING) {
                    // Pairing done but no bond stored (SoC has ALLOW_BONDING=0)
                    Log.d(TAG, "Pairing done (no bond stored), connecting after delay");
                    final BluetoothDevice deviceToConnect = mPendingBondDevice;
                    mPendingBondDevice = null;
                    mHandler.postDelayed(() -> connectGatt(deviceToConnect), POST_PAIRING_DELAY_MS);
                } else if (bondState == BluetoothDevice.BOND_NONE && prevBondState != BluetoothDevice.BOND_BONDING) {
                    // Pairing failed (user rejected or error)
                    Log.d(TAG, "Bond failed - prevState was not BONDING, treating as failure");
                    cancelConnectionTimeout();
                    mPendingBondDevice = null;
                    showPairingFailed();
                } else {
                    Log.d(TAG, "Unhandled bond state transition");
                }
            }
        };

        IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
        registerReceiver(mBondReceiver, filter, Context.RECEIVER_EXPORTED);
    }

    private boolean checkPermissions() {
        String[] permissions = {
                Manifest.permission.BLUETOOTH_CONNECT,
                Manifest.permission.BLUETOOTH_SCAN,
                Manifest.permission.ACCESS_FINE_LOCATION
        };

        for (String perm : permissions) {
            if (ActivityCompat.checkSelfPermission(this, perm) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
                return false;
            }
        }
        return true;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_PERMISSIONS) {
            for (int result : grantResults) {
                if (result != PackageManager.PERMISSION_GRANTED) {
                    return;
                }
            }
            startScan();
        }
    }

    private void startScan() {
        Log.d(TAG, "startScan called");
        setState(State.SCANNING);
        mSeenDevices.clear();

        ScanSettings settings = new ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                .build();

        mScanner.startScan(null, settings, mScanCallback);
        Log.d(TAG, "Scan started");
    }

    private void stopScan() {
        Log.d(TAG, "stopScan called - state: " + mState);
        if (mScanner != null && mState == State.SCANNING) {
            mScanner.stopScan(mScanCallback);
            Log.d(TAG, "Scan stopped");
        }
    }

    private final ScanCallback mScanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            BluetoothDevice device = result.getDevice();
            String name = device.getName();
            String address = device.getAddress();

            if (mSeenDevices.contains(address)) return;
            mSeenDevices.add(address);

            Log.d(TAG, "Scan result: " + (name != null ? name : "null") + " [" + address + "]");

            // Check if device name matches our target
            boolean isTarget = TARGET_DEVICE_NAME.equalsIgnoreCase(name);

            if (isTarget) {
                Log.d(TAG, "Target device found!");
                stopScan();
                connectToDevice(device);
            }
        }

        @Override
        public void onScanFailed(int errorCode) {
            Log.e(TAG, "Scan failed: " + errorCode);
            setState(State.IDLE);
        }
    };

    private void connectToDevice(BluetoothDevice device) {
        String bondStateStr = device.getBondState() == BluetoothDevice.BOND_NONE ? "NONE" :
                             device.getBondState() == BluetoothDevice.BOND_BONDING ? "BONDING" :
                             device.getBondState() == BluetoothDevice.BOND_BONDED ? "BONDED" : "UNKNOWN";
        Log.d(TAG, "connectToDevice: " + device.getName() + " [" + device.getAddress() + "], bondState=" + bondStateStr);

        setState(State.CONNECTING);
        startConnectionTimeout();

        int bondState = device.getBondState();
        if (bondState == BluetoothDevice.BOND_NONE) {
            // Need to pair first
            Log.d(TAG, "Starting pairing (createBond)");
            mPendingBondDevice = device;
            device.createBond();
        } else {
            // Already bonded or bonding
            Log.d(TAG, "Already bonded, connecting GATT directly");
            connectGatt(device);
        }
    }

    private void connectGatt(BluetoothDevice device) {
        Log.d(TAG, "connectGatt: " + device.getName() + " [" + device.getAddress() + "]");
        mConnectedDevice = device;
        mGattClient = device.connectGatt(this, false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
    }

    /**
     * Start a timeout for the connection attempt.
     * If we don't connect within the timeout, show pairing failed.
     */
    private void startConnectionTimeout() {
        cancelConnectionTimeout();
        mConnectionTimeoutRunnable = () -> {
            if (mState == State.CONNECTING) {
                Log.d(TAG, "Connection timeout - pairing/connection failed");
                cleanupGatt();
                mPendingBondDevice = null;
                showPairingFailed();
            }
        };
        mHandler.postDelayed(mConnectionTimeoutRunnable, CONNECTION_TIMEOUT_MS);
    }

    private void cancelConnectionTimeout() {
        if (mConnectionTimeoutRunnable != null) {
            mHandler.removeCallbacks(mConnectionTimeoutRunnable);
            mConnectionTimeoutRunnable = null;
        }
    }

    /**
     * Show pairing failed message, then return to idle.
     */
    private void showPairingFailed() {
        setState(State.PAIRING_FAILED);
        mHandler.postDelayed(() -> setState(State.IDLE), PAIRING_FAILED_DISPLAY_MS);
    }

    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            String stateStr = newState == BluetoothProfile.STATE_CONNECTED ? "CONNECTED" :
                             newState == BluetoothProfile.STATE_DISCONNECTED ? "DISCONNECTED" :
                             newState == BluetoothProfile.STATE_CONNECTING ? "CONNECTING" :
                             newState == BluetoothProfile.STATE_DISCONNECTING ? "DISCONNECTING" : "UNKNOWN";
            Log.d(TAG, "GATT onConnectionStateChange: status=" + status + ", newState=" + stateStr + " (" + newState + "), appState=" + mState);

            cancelConnectionTimeout();

            if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
                mGattClient = gatt;
                mConnectionTimestamp = System.currentTimeMillis();
                gatt.requestMtu(512);
                gatt.discoverServices();
                setState(State.CONNECTED);
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                gatt.close();
                mGattClient = null;

                // Check if this was a quick disconnect (likely bond/encryption failure)
                long connectionDuration = System.currentTimeMillis() - mConnectionTimestamp;
                boolean wasQuickDisconnect = mConnectionTimestamp > 0 &&
                        connectionDuration < QUICK_DISCONNECT_THRESHOLD_MS;
                mConnectionTimestamp = 0;

                if (mState == State.CONNECTING || wasQuickDisconnect) {
                    // Connection failed during connecting, or disconnected immediately after
                    // connecting (stale bond / encryption failure)
                    Log.d(TAG, "Connection/bond failed - status: " + status +
                            ", quickDisconnect: " + wasQuickDisconnect +
                            ", duration: " + connectionDuration + "ms");
                    showPairingFailed();
                } else {
                    // Normal disconnect from stable connected state
                    setState(State.DISCONNECTED);
                    mHandler.postDelayed(() -> setState(State.IDLE), DISCONNECT_DISPLAY_MS);
                }
            }
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        stopAnimation();
        try {
            unregisterReceiver(mBondReceiver);
        } catch (Exception e) { }
        try {
            unregisterReceiver(mBluetoothStateReceiver);
        } catch (Exception e) { }
        if (mHandler != null) {
            mHandler.removeCallbacksAndMessages(null);
        }
        stopScan();
        cleanupGatt();
    }
}
