package com.wytehat.btlogger;

import android.app.Activity;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothClass;
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.ClipData;
import android.content.ClipboardManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.ParcelUuid;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ArrayAdapter;
import android.widget.AdapterView;
import android.widget.Spinner;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.EditText;
import android.widget.Toast;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class DeviceManagerActivity extends Activity {
    private static class SignalEntry {
        BluetoothDevice device; int rssi; long seenAt; String broadcastName; String vendorName; String category; String broadcastType; String customName;
    }

    private BluetoothAdapter bluetooth;
    private BluetoothLeScanner leScanner;
    private ScanCallback scanCallback;
    private TrackerDatabase database;
    private BluetoothRadarView radar;
    private TextView scanStatus;
    private Button scanButton;
    private boolean scanning;
    private LinearLayout gridPanel;
    private ListView signalList;
    private SignalGridAdapter signalAdapter;
    private String signalSort = "Strongest first";
    private LinearLayout radarActions;
    private TextView radarSelection;
    private Button radarMapButton;
    private Button radarTrackButton;
    private SignalEntry selectedSignal;
    private PairedAdapter pairedAdapter;
    private TextView pairedHeader;
    private ListView pairedList;
    private final Handler handler = new Handler();
    private final LinkedHashMap<String, SignalEntry> signals =
        new LinkedHashMap<String, SignalEntry>();
    private final ArrayList<BluetoothDevice> paired = new ArrayList<BluetoothDevice>();
    private TrackerDatabase.Fix cachedObservationFix;
    private long cachedObservationFixAt;
    private String categoryFilter = "All";
    private static final String[] CATEGORY_ORDER = { "Trackers / Tags", "Audio", "Wearables", "Phones", "Computers", "Vehicles", "Other" };
    private final ArrayList<String> categoryValues = new ArrayList<String>();
    private final ArrayList<String> categoryLabels = new ArrayList<String>();
    private Spinner categorySpinner;
    private ArrayAdapter<String> categoryAdapter;
    private boolean receiverRegistered;

    private final BroadcastReceiver receiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
                loadPaired();
            } else if (BluetoothDevice.ACTION_FOUND.equals(intent.getAction())) {
                BluetoothDevice device = (BluetoothDevice) intent.getParcelableExtra(
                    BluetoothDevice.EXTRA_DEVICE);
                int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
                if (rssi != Short.MIN_VALUE) acceptSignal(device, rssi);
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(intent.getAction())) {
                if (scanning && Build.VERSION.SDK_INT < 21) {
                    try { bluetooth.startDiscovery(); } catch (Exception ignored) { }
                } else scanStatus.setText(signals.size() + " transmitting devices found");
            }
        }
    };

    protected void onCreate(Bundle state) {
        super.onCreate(state);
        database = new TrackerDatabase(this);
        bluetooth = BluetoothAdapter.getDefaultAdapter();

        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);
        root.setBackgroundColor(Color.rgb(238, 241, 245));
        root.addView(NavigationHelper.createBar(this, 1, "Bluetooth Scanner"));

        TextView help = new TextView(this);
        help.setText("Scan nearby transmitters • tap a device to select it");
        help.setTextColor(Color.DKGRAY);
        help.setPadding(dp(8), dp(1), dp(8), dp(1));
        help.setSingleLine(true);
        help.setTextSize(12);
        help.setEllipsize(android.text.TextUtils.TruncateAt.END);
        root.addView(help);

        scanButton = new Button(this);
        scanButton.setText("SCAN FOR TRANSMITTING DEVICES");
        root.addView(scanButton, new LinearLayout.LayoutParams(-1, -2));
        scanStatus = new TextView(this);
        scanStatus.setText("Tap scan to listen for BLE advertisements");
        scanStatus.setPadding(dp(8), dp(1), dp(8), dp(1));
        scanStatus.setSingleLine(true);
        scanStatus.setTextSize(12);
        scanStatus.setEllipsize(android.text.TextUtils.TruncateAt.END);
        root.addView(scanStatus);
        categorySpinner = new Spinner(this);
        categoryValues.add("All"); categoryLabels.add("All (0)");
        categoryAdapter = new ArrayAdapter<String>(this,
            android.R.layout.simple_spinner_item, categoryLabels);
        categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        categorySpinner.setAdapter(categoryAdapter);
        root.addView(categorySpinner, new LinearLayout.LayoutParams(-1, -2));
        categorySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                if (position >= 0 && position < categoryValues.size()) {
                    categoryFilter = categoryValues.get(position); updateRadar();
                }
            }
            public void onNothingSelected(AdapterView<?> parent) { }
        });

        LinearLayout viewModes = new LinearLayout(this);
        final Button radarMode = new Button(this); radarMode.setText("RADAR VIEW");
        final Button listMode = new Button(this); listMode.setText("LIVE DEVICE LIST");
        viewModes.addView(radarMode, new LinearLayout.LayoutParams(0, -2, 1));
        viewModes.addView(listMode, new LinearLayout.LayoutParams(0, -2, 1));
        root.addView(viewModes);
        radarMode.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                radar.setVisibility(View.VISIBLE);
                pairedHeader.setVisibility(View.VISIBLE); pairedList.setVisibility(View.VISIBLE);
                gridPanel.setVisibility(View.GONE);
                radarMode.setEnabled(false); listMode.setEnabled(true);
                if (selectedSignal != null) { refreshRadarToggleButtons(); radarActions.setVisibility(View.VISIBLE); }
            }
        });
        listMode.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                radar.setVisibility(View.GONE); radarActions.setVisibility(View.GONE);
                pairedHeader.setVisibility(View.GONE); pairedList.setVisibility(View.GONE);
                gridPanel.setVisibility(View.VISIBLE);
                radarMode.setEnabled(true); listMode.setEnabled(false);
            }
        });
        radar = new BluetoothRadarView(this);
        root.addView(radar, new LinearLayout.LayoutParams(-1, dp(310)));
        radar.setVisibility(View.GONE);
        radarActions = new LinearLayout(this);
        radarActions.setOrientation(LinearLayout.HORIZONTAL);
        radarActions.setPadding(dp(4), dp(1), dp(4), dp(1));
        radarActions.setBackgroundColor(Color.rgb(207, 216, 220));
        radarSelection = new TextView(this);
        radarSelection.setText("Tap a radar device");
        radarSelection.setTextSize(12);
        radarSelection.setSingleLine(true);
        radarSelection.setEllipsize(android.text.TextUtils.TruncateAt.END);
        radarSelection.setGravity(android.view.Gravity.CENTER_VERTICAL);
        radarActions.addView(radarSelection, new LinearLayout.LayoutParams(0, -1, 1));
        radarMapButton = new Button(this);
        radarMapButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) { if (selectedSignal != null) toggleMap(selectedSignal, radarMapButton); }
        }); radarActions.addView(radarMapButton, new LinearLayout.LayoutParams(dp(76), dp(36)));
        radarTrackButton = new Button(this);
        radarTrackButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) { if (selectedSignal != null) toggleTrack(selectedSignal, radarTrackButton); }
        }); radarActions.addView(radarTrackButton, new LinearLayout.LayoutParams(dp(76), dp(36)));
        styleToggle(radarMapButton, false, "MAP");
        styleToggle(radarTrackButton, false, "TRACK");
        radarActions.setVisibility(View.GONE);
        root.addView(radarActions, new LinearLayout.LayoutParams(-1, -2));
        gridPanel = new LinearLayout(this);
        gridPanel.setOrientation(LinearLayout.VERTICAL);
        TextView liveHeader = new TextView(this);
        liveHeader.setText("LIVE TRANSMITTERS — tap name or MAC to copy");
        liveHeader.setTextColor(Color.WHITE);
        liveHeader.setBackgroundColor(Color.rgb(55, 71, 79));
        liveHeader.setPadding(dp(12), dp(8), dp(8), dp(8));
        gridPanel.addView(liveHeader);
        Spinner sortSpinner = new Spinner(this);
        final String[] sortOptions = { "Strongest first", "Weakest first", "Name A-Z" };
        ArrayAdapter<String> sortAdapter = new ArrayAdapter<String>(this,
            android.R.layout.simple_spinner_item, sortOptions);
        sortAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        sortSpinner.setAdapter(sortAdapter);
        gridPanel.addView(sortSpinner, new LinearLayout.LayoutParams(-1, -2));
        signalList = new ListView(this); signalAdapter = new SignalGridAdapter();
        signalList.setAdapter(signalAdapter);
        gridPanel.addView(signalList, new LinearLayout.LayoutParams(-1, 0, 1));
        gridPanel.setVisibility(View.VISIBLE);
        listMode.setEnabled(false);
        root.addView(gridPanel, new LinearLayout.LayoutParams(-1, 0, 1));
        sortSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                signalSort = sortOptions[position]; signalAdapter.notifyDataSetChanged();
            }
            public void onNothingSelected(AdapterView<?> parent) { }
        });

        pairedHeader = new TextView(this);
        pairedHeader.setText("PAIRED DEVICES • broadcasting only");
        pairedHeader.setTextColor(Color.WHITE);
        pairedHeader.setTextSize(12);
        pairedHeader.setTypeface(null, android.graphics.Typeface.BOLD);
        pairedHeader.setPadding(dp(8), dp(2), dp(8), dp(2));
        pairedHeader.setSingleLine(true);
        pairedHeader.setBackgroundColor(Color.rgb(55, 71, 79));
        root.addView(pairedHeader);

        pairedList = new ListView(this);
        pairedAdapter = new PairedAdapter();
        pairedList.setAdapter(pairedAdapter);
        root.addView(pairedList, new LinearLayout.LayoutParams(-1, 0, 1));
        pairedHeader.setVisibility(View.GONE); pairedList.setVisibility(View.GONE);
        setContentView(root);
        LargeScreenLayout.constrain(this);

        IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
        filter.addAction(BluetoothDevice.ACTION_FOUND);
        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
        registerReceiver(receiver, filter);
        receiverRegistered = true;
        loadPaired();

        scanButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                if (scanning) { stopScan(); scanStatus.setText("Radar scan stopped"); }
                else startRadarScan();
            }
        });
        radar.setListener(new BluetoothRadarView.Listener() {
            public void onTargetSelected(String address) {
                SignalEntry entry = signals.get(address);
                if (entry != null) {
                    selectedSignal = entry;
                    radarSelection.setText(entryLabel(entry) + "  " + entry.rssi + " dBm");
                    refreshRadarToggleButtons();
                    radarActions.setVisibility(View.VISIBLE);
                }
            }
        });
        pairedList.setOnItemClickListener(new android.widget.AdapterView.OnItemClickListener() {
            public void onItemClick(android.widget.AdapterView<?> parent, View view,
                                    int position, long id) {
                BluetoothDevice device = paired.get(position);
                showActions(device, isBroadcasting(safeAddress(device)));
            }
        });
    }

    private void startRadarScan() {
        if (bluetooth == null) {
            Toast.makeText(this, "Bluetooth is unavailable", Toast.LENGTH_LONG).show(); return;
        }
        try {
            if (!bluetooth.isEnabled()) {
                startActivity(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)); return;
            }
            stopScan(); signals.clear(); refreshCategoryOptions(); scanning = true; radar.setScanning(true); scanButton.setText("STOP RADAR SCAN"); updateRadar();
            scanStatus.setText("Scanning… tap a dot when your item appears");
            if (Build.VERSION.SDK_INT >= 21) {
                leScanner = bluetooth.getBluetoothLeScanner();
                if (leScanner == null) throw new IllegalStateException();
                scanCallback = new ScanCallback() {
                    public void onScanResult(int type, ScanResult result) {
                        if (result != null) acceptScanResult(result);
                    }
                    public void onBatchScanResults(List<ScanResult> results) {
                        for (int i = 0; i < results.size(); i++)
                            acceptScanResult(results.get(i));
                    }
                    public void onScanFailed(int error) {
                        stopScan(); scanStatus.setText("BLE scan failed: " + error);
                    }
                };
                ScanSettings settings = new ScanSettings.Builder()
                    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
                leScanner.startScan(null, settings, scanCallback);
            } else bluetooth.startDiscovery();
            handler.post(pruneTask);
        } catch (Exception error) {
            stopScan();
            scanStatus.setText("Bluetooth scan permission is required");
        }
    }

	private void acceptScanResult(ScanResult result) {
        if (result == null) return;
        String advertisedName = null;
        String companyVendor = null;
        int appearance = -1;
        String broadcastType = null;
        int companyId = -1;
        String advertisementHex = null;
        try {
            if (result.getScanRecord() != null) {
                advertisedName = result.getScanRecord().getDeviceName();
                appearance = parseAppearance(result.getScanRecord().getBytes());
                List<ParcelUuid> services = result.getScanRecord().getServiceUuids();
                broadcastType = describeServices(services);
                BleFingerprintRegistry.Match fingerprint = BleFingerprintRegistry.first(services);
                advertisementHex = bytesToHex(result.getScanRecord().getBytes());
                SparseArray<byte[]> data = result.getScanRecord().getManufacturerSpecificData();
                if (data != null && data.size() > 0) {
                    int bestKey = data.keyAt(0);
                    int bestLength = data.valueAt(0) == null ? 0 : data.valueAt(0).length;
                    for (int i = 1; i < data.size(); i++) {
                        byte[] payload = data.valueAt(i);
                        int length = payload == null ? 0 : payload.length;
                        if (length > bestLength) { bestLength = length; bestKey = data.keyAt(i); }
                    }
                    companyId = bestKey;
                    companyVendor = database.lookupCompany(bestKey);
                }
                if (fingerprint != null && fingerprint.vendor != null) companyVendor = fingerprint.vendor;
            }
        } catch (Exception ignored) { }
        acceptSignal(result.getDevice(), result.getRssi(), advertisedName, companyVendor, appearance,
					 broadcastType, companyId, advertisementHex);
    }

	private String bytesToHex(byte[] bytes)
	{
		// TODO: Implement this method
		return null;
	}

    private synchronized void acceptSignal(BluetoothDevice device, int rssi) {
        acceptSignal(device, rssi, null, null, -1, null, -1, null);
    }

    private synchronized void acceptSignal(BluetoothDevice device, int rssi, String advertisedName,
                                           String companyVendor, int appearance, String broadcastType,
                                           int companyId, String advertisementHex) {
        if (device == null || rssi == 0 || rssi == 127) return;
        String address = safeAddress(device);
        SignalEntry entry = signals.get(address);
        if (entry == null) { entry = new SignalEntry(); signals.put(address, entry); }
        entry.device = device; entry.rssi = rssi; entry.seenAt = System.currentTimeMillis();
        String cachedName = safeName(device);
        if (useful(advertisedName)) entry.broadcastName = advertisedName;
        else if (!useful(entry.broadcastName) && useful(cachedName)) entry.broadcastName = cachedName;
        if (companyVendor != null) entry.vendorName = companyVendor;
        else if (entry.vendorName == null) entry.vendorName = database.lookupOui(address);
        if (broadcastType != null) entry.broadcastType = broadcastType;
        DeviceRecord savedRecord = database.get(address);
        if (savedRecord != null) entry.customName = savedRecord.customName;
        if (savedRecord != null && savedRecord.vendorManual == 1) entry.vendorName = savedRecord.vendorName;
        entry.category = classify(device, entry.broadcastName, (entry.vendorName == null ? "" : entry.vendorName) + " " + (entry.broadcastType == null ? "" : entry.broadcastType), appearance);
        if (savedRecord != null && savedRecord.categoryManual == 1) entry.category = savedRecord.category;
        database.telemetry(address, entry.broadcastName, null, Integer.valueOf(rssi),
						   entry.seenAt, null);
        database.setDetectedCategory(address, entry.category);
        database.recordObservation(address, entry.seenAt, rssi, entry.broadcastName,
								   entry.vendorName, entry.category, entry.broadcastType, appearance, companyId, advertisementHex, recentObservationFix());
        String vendor = entry.vendorName;
        if (vendor != null) database.setVendor(address, vendor);
        runOnUiThread(new Runnable() {
				public void run() { refreshCategoryOptions(); updateRadar(); pairedAdapter.notifyDataSetChanged(); }
			});
    }

    private TrackerDatabase.Fix recentObservationFix() {
        long now = System.currentTimeMillis();
        if (now - cachedObservationFixAt < 30000L) return cachedObservationFix;
        cachedObservationFixAt = now;
        try {
            LocationManager manager = (LocationManager)getSystemService(LOCATION_SERVICE);
            Location best = null;
            List<String> providers = manager.getProviders(true);
            for (int i = 0; i < providers.size(); i++) {
                Location candidate = manager.getLastKnownLocation(providers.get(i));
                if (candidate != null && (best == null || candidate.getTime() > best.getTime())) best = candidate;
            }
            if (best != null && now - best.getTime() <= 600000L)
                cachedObservationFix = new TrackerDatabase.Fix(best.getLatitude(), best.getLongitude(), best.getAccuracy());
            else cachedObservationFix = null;
        } catch (Exception ignored) { cachedObservationFix = null; }
        return cachedObservationFix;
    }

    private final Runnable pruneTask = new Runnable() {
        public void run() {
            long cutoff = System.currentTimeMillis() - 12000L;
            ArrayList<String> expired = new ArrayList<String>();
            synchronized (DeviceManagerActivity.this) {
                for (Map.Entry<String, SignalEntry> item : signals.entrySet())
                    if (item.getValue().seenAt < cutoff) expired.add(item.getKey());
                for (int i = 0; i < expired.size(); i++) signals.remove(expired.get(i));
                if (selectedSignal != null && !signals.containsValue(selectedSignal)) {
                    selectedSignal = null; radarActions.setVisibility(View.GONE);
                }
            }
            refreshCategoryOptions(); updateRadar(); pairedAdapter.notifyDataSetChanged();
            handler.postDelayed(this, 1000L);
        }
    };

    private synchronized void refreshCategoryOptions() {
        if (categoryAdapter == null || categorySpinner == null) return;
        int[] counts = new int[CATEGORY_ORDER.length];
        for (SignalEntry entry : signals.values()) {
            for (int i = 0; i < CATEGORY_ORDER.length; i++) {
                if (CATEGORY_ORDER[i].equals(entry.category)) { counts[i]++; break; }
            }
        }
        ArrayList<String> nextValues = new ArrayList<String>();
        ArrayList<String> nextLabels = new ArrayList<String>();
        nextValues.add("All"); nextLabels.add("All (" + signals.size() + ")");
        for (int i = 0; i < CATEGORY_ORDER.length; i++) {
            if (counts[i] > 0) {
                nextValues.add(CATEGORY_ORDER[i]);
                nextLabels.add(CATEGORY_ORDER[i] + " (" + counts[i] + ")");
            }
        }
        int selected = nextValues.indexOf(categoryFilter);
        if (selected < 0) { categoryFilter = "All"; selected = 0; }
        if (!nextLabels.equals(categoryLabels)) {
            categoryValues.clear(); categoryValues.addAll(nextValues);
            categoryLabels.clear(); categoryLabels.addAll(nextLabels);
            categoryAdapter.notifyDataSetChanged();
        }
        if (categorySpinner.getSelectedItemPosition() != selected) categorySpinner.setSelection(selected);
    }

    private synchronized void updateRadar() {
        if (selectedSignal != null && (!signals.containsValue(selectedSignal) ||
            (!"All".equals(categoryFilter) && !categoryFilter.equals(selectedSignal.category)))) {
            selectedSignal = null;
            if (radarActions != null) radarActions.setVisibility(View.GONE);
        }
        ArrayList<BluetoothRadarView.Target> targets = new ArrayList<BluetoothRadarView.Target>();
        for (Map.Entry<String, SignalEntry> item : signals.entrySet()) {
            SignalEntry entry = item.getValue();
            if (!"All".equals(categoryFilter) && !categoryFilter.equals(entry.category)) continue;
            targets.add(new BluetoothRadarView.Target(item.getKey(),
                entryLabel(entry) + (entry.broadcastType == null ? "" : " • " + entry.broadcastType) +
                " [" + entry.category + "]", entry.rssi));
        }
        if (radar != null) radar.setTargets(targets);
        if (signalAdapter != null) signalAdapter.notifyDataSetChanged();
        scanStatus.setText(targets.size() + " shown of " + signals.size() + " currently broadcasting");
    }

    private synchronized ArrayList<SignalEntry> visibleSignals() {
        ArrayList<SignalEntry> rows = new ArrayList<SignalEntry>();
        for (SignalEntry entry : signals.values()) {
            if ("All".equals(categoryFilter) || categoryFilter.equals(entry.category)) rows.add(entry);
        }
        Collections.sort(rows, new Comparator<SignalEntry>() {
            public int compare(SignalEntry a, SignalEntry b) {
                if ("Weakest first".equals(signalSort)) return a.rssi - b.rssi;
                if ("Name A-Z".equals(signalSort)) return entryLabel(a).compareToIgnoreCase(entryLabel(b));
                return b.rssi - a.rssi;
            }
        });
        return rows;
    }

    private void copyText(String label, String value) {
        ClipboardManager clipboard = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
        clipboard.setPrimaryClip(ClipData.newPlainText(label, value));
        Toast.makeText(this, label + " copied", Toast.LENGTH_SHORT).show();
    }

    private void editSignal(final SignalEntry entry) {
        final String address = safeAddress(entry.device);
        LinearLayout form = new LinearLayout(this); form.setOrientation(LinearLayout.VERTICAL);
        form.setPadding(dp(18), dp(8), dp(18), 0);
        final EditText name = new EditText(this);
        name.setHint("Custom device name"); name.setText(entryLabel(entry)); form.addView(name);
        final EditText vendor = new EditText(this);
        vendor.setHint("Manufacturer / vendor");
        vendor.setText(entry.vendorName == null ? "" : entry.vendorName); form.addView(vendor);
        final Spinner category = new Spinner(this);
        final String[] choices = { "Trackers / Tags", "Audio", "Wearables", "Phones", "Computers", "Vehicles", "Other" };
        ArrayAdapter<String> choicesAdapter = new ArrayAdapter<String>(this,
            android.R.layout.simple_spinner_item, choices);
        choicesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        category.setAdapter(choicesAdapter);
        int selected = 0; for (int i = 0; i < choices.length; i++) if (choices[i].equals(entry.category)) selected = i;
        category.setSelection(selected); form.addView(category);
        TextView facts = new TextView(this);
        facts.setText("MAC: " + address + "\nDetected manufacturer: " +
            (entry.vendorName == null ? "Not supplied / unresolved" : entry.vendorName) +
            "\nBroadcast: " + (entry.broadcastType == null ? "General advertisement" : entry.broadcastType));
        facts.setPadding(0, dp(10), 0, 0); form.addView(facts);
        new AlertDialog.Builder(this).setTitle("Edit transmitting device").setView(form)
            .setPositiveButton("SAVE", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    entry.customName = name.getText().toString().trim();
                    entry.category = choices[category.getSelectedItemPosition()];
                    database.setCustomName(address, entry.customName);
                    entry.vendorName = vendor.getText().toString().trim();
                    database.setManualVendor(address, entry.vendorName);
                    database.setCategory(address, entry.category);
                    database.markTracked(address, entryLabel(entry));
                    refreshCategoryOptions(); updateRadar();
                }
            }).setNegativeButton("CANCEL", null)
            .setNeutralButton("REMOVE TRACKING", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) { confirmRemoveSignal(entry); }
            }).show();
    }

    private void confirmRemoveSignal(final SignalEntry entry) {
        final String address = safeAddress(entry.device);
        new AlertDialog.Builder(this).setTitle("Remove from tracked items?")
            .setMessage("The device will remain in sighting history and will continue to appear in live scans when this Bluetooth ID broadcasts.")
            .setPositiveButton("REMOVE", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    database.removeFromTracking(address);
                    if (selectedSignal == entry) refreshRadarToggleButtons();
                    signalAdapter.notifyDataSetChanged();
                    sendBroadcast(new Intent(BluetoothTrackingService.ACTION_CONFIG_CHANGED).setPackage(getPackageName()));
                    Toast.makeText(DeviceManagerActivity.this, "Tracking removed; sighting history retained", Toast.LENGTH_LONG).show();
                }
            }).setNegativeButton("CANCEL", null).show();
    }

    private void showActions(final BluetoothDevice device, boolean broadcasting) {
        final String address = safeAddress(device);
        final boolean pairedNow = device.getBondState() == BluetoothDevice.BOND_BONDED;
        if (broadcasting) {
            new AlertDialog.Builder(this).setTitle(broadcasting && signals.get(address) != null ? entryLabel(signals.get(address)) : displayName(device))
                .setMessage(address + "\nBroadcast: " + (signals.get(address).broadcastType == null ? "General advertisement" : signals.get(address).broadcastType) + "\nCurrent signal: " + signals.get(address).rssi + " dBm")
                .setItems(new String[] { "✓ SHOW ON MAP", "LIVE TRACK", pairedNow ? "Already paired" :
                    "Pair and save item", "Request connect", "Request disconnect" },
                    new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            if (which == 0) addToMap(device);
                            else if (which == 1) startLive(device);
                            else if (which == 2 && !pairedNow) pair(device);
                            else if (which == 3) invoke(device, "connect");
                            else if (which == 4) invoke(device, "disconnect");
                        }
                    }).show();
        } else {
            new AlertDialog.Builder(this).setTitle(broadcasting && signals.get(address) != null ? entryLabel(signals.get(address)) : displayName(device))
                .setMessage("No current BLE RSSI signal. Live Find is unavailable until this device broadcasts.")
                .setItems(new String[] { pairedNow ? "Already paired" : "Pair item",
                    "Request connect", "Request disconnect" },
                    new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            if (which == 0 && !pairedNow) pair(device);
                            else if (which == 1) invoke(device, "connect");
                            else if (which == 2) invoke(device, "disconnect");
                        }
                    }).show();
        }
    }

    private void styleToggle(Button button, boolean enabled, String label) {
        button.setText(label + (enabled ? " ON" : " OFF"));
        styleActionButton(button, enabled ? Color.rgb(46, 125, 50) : Color.rgb(96, 125, 139));
    }

    private void styleActionButton(Button button, int color) {
        android.graphics.drawable.GradientDrawable shape = new android.graphics.drawable.GradientDrawable();
        shape.setColor(color);
        shape.setCornerRadius(dp(18));
        button.setBackgroundDrawable(shape);
        button.setTextColor(Color.WHITE);
        button.setTextSize(11);
        button.setMinWidth(0);
        button.setMinimumWidth(0);
        button.setMinHeight(0);
        button.setMinimumHeight(0);
        button.setPadding(dp(5), 0, dp(5), 0);
    }

    private LinearLayout.LayoutParams listActionButtonParams() {
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(dp(78), dp(40));
        params.setMargins(dp(2), dp(2), dp(2), dp(2));
        return params;
    }

    private void refreshRadarToggleButtons() {
        if (selectedSignal == null) return;
        DeviceRecord record = database.get(safeAddress(selectedSignal.device));
        styleToggle(radarMapButton, record != null && record.mapEnabled == 1, "MAP");
        styleToggle(radarTrackButton, record != null && record.liveEnabled == 1, "TRACK");
    }

    private void toggleMap(SignalEntry entry, Button button) {
        String address = safeAddress(entry.device);
        DeviceRecord record = database.get(address);
        boolean enable = record == null || record.mapEnabled != 1;
        if (!enable) {
            database.setMapEnabled(address, false); styleToggle(button, false, "MAP");
            Toast.makeText(this, "Removed from the tracked map", Toast.LENGTH_SHORT).show();
            return;
        }
        database.setMapEnabled(address, true); styleToggle(button, true, "MAP");
        addToMap(entry.device);
    }

    private void toggleTrack(SignalEntry entry, Button button) {
        String address = safeAddress(entry.device);
        DeviceRecord record = database.get(address);
        boolean enable = record == null || record.liveEnabled != 1;
        database.setLiveEnabled(address, enable); styleToggle(button, enable, "TRACK");
        if (enable) startLive(entry.device);
        else Toast.makeText(this, "Live tracking toggled off", Toast.LENGTH_SHORT).show();
    }

    private void addToMap(final BluetoothDevice device) {
        stopScan();
        final String address = safeAddress(device);
        final SignalEntry entry = signals.get(address);
        String selectedName = entry != null && useful(entry.broadcastName)
            ? entry.broadcastName : (entry != null ? entryLabel(entry) : displayName(device));
        database.markTracked(address, selectedName);
        database.setMapEnabled(address, true);
        if (entry != null && entry.vendorName != null) database.setVendor(address, entry.vendorName);
        if (entry != null) database.setDetectedCategory(address, entry.category);
        try {
            final LocationManager manager = (LocationManager)getSystemService(LOCATION_SERVICE);
            Location best = null;
            List<String> providers = manager.getProviders(true);
            for (int i = 0; i < providers.size(); i++) {
                Location candidate = manager.getLastKnownLocation(providers.get(i));
                if (candidate != null && (best == null || candidate.getTime() > best.getTime()))
                    best = candidate;
            }
            if (best != null && System.currentTimeMillis() - best.getTime() <= 120000L) {
                saveMapLocation(address, best); return;
            }
            final boolean[] saved = new boolean[] { false };
            LocationListener listener = new LocationListener() {
                public void onLocationChanged(Location location) {
                    if (saved[0]) return; saved[0] = true;
                    try { manager.removeUpdates(this); } catch (Exception ignored) { }
                    saveMapLocation(address, location);
                }
                public void onProviderEnabled(String provider) { }
                public void onProviderDisabled(String provider) { }
                public void onStatusChanged(String provider, int status, Bundle extras) { }
            };
            if (manager.isProviderEnabled(LocationManager.GPS_PROVIDER))
                manager.requestSingleUpdate(LocationManager.GPS_PROVIDER, listener, null);
            else if (manager.isProviderEnabled(LocationManager.NETWORK_PROVIDER))
                manager.requestSingleUpdate(LocationManager.NETWORK_PROVIDER, listener, null);
            else throw new IllegalStateException("Location is disabled");
            Toast.makeText(this, "Waiting for a GPS fix…", Toast.LENGTH_SHORT).show();
        } catch (Exception error) {
            Toast.makeText(this, "Location permission and GPS are required to place this item", Toast.LENGTH_LONG).show();
        }
    }

    private void saveMapLocation(String address, Location location) {
        if (location == null) return;
        database.updateLocation(address, new TrackerDatabase.Fix(location.getLatitude(),
            location.getLongitude(), location.getAccuracy()), System.currentTimeMillis());
        Intent map = new Intent(this, MapActivity.class);
        map.putExtra("focus", address);
        startActivity(map);
    }

    private void startLive(BluetoothDevice device) {
        String address = safeAddress(device);
        SignalEntry entry = signals.get(address);
        if (entry == null || System.currentTimeMillis() - entry.seenAt > 12000L) {
            Toast.makeText(this, "The signal is no longer being received", Toast.LENGTH_LONG).show();
            return;
        }
        String selectedName = useful(entry.broadcastName) ? entry.broadcastName : entryLabel(entry);
        database.setLiveEnabled(address, true);
        stopScan();
        database.markTracked(address, selectedName);
        database.telemetry(address, selectedName, null, Integer.valueOf(entry.rssi),
            System.currentTimeMillis(), null);
        Intent intent = new Intent(this, LiveRangeFinderActivity.class);
        intent.putExtra("address", address);
        startActivity(intent);
    }

    private void pair(BluetoothDevice device) {
        database.markTracked(safeAddress(device), safeName(device));
        database.setVendor(safeAddress(device), database.lookupOui(safeAddress(device)));
        try {
            if (Build.VERSION.SDK_INT >= 19) device.createBond();
            else BluetoothDevice.class.getMethod("createBond").invoke(device);
            Toast.makeText(this, "Pairing started", Toast.LENGTH_SHORT).show();
        } catch (Exception error) {
            Toast.makeText(this, "Pairing request failed", Toast.LENGTH_LONG).show();
        }
    }

    private void invoke(BluetoothDevice device, String action) {
        try {
            Method method = device.getClass().getMethod(action); method.invoke(device);
            Toast.makeText(this, action + " request sent", Toast.LENGTH_SHORT).show();
        } catch (Exception error) {
            Toast.makeText(this, "This device profile does not expose " + action + " to apps",
                Toast.LENGTH_LONG).show();
        }
    }

    private void loadPaired() {
        paired.clear();
        try { if (bluetooth != null) paired.addAll(bluetooth.getBondedDevices()); }
        catch (Exception ignored) { }
        if (pairedAdapter != null) pairedAdapter.notifyDataSetChanged();
    }

    private synchronized boolean isBroadcasting(String address) {
        SignalEntry entry = signals.get(address);
        return entry != null && System.currentTimeMillis() - entry.seenAt <= 12000L;
    }

    private String describeServices(List<ParcelUuid> services) {
        if (services == null || services.isEmpty()) return null;
        ArrayList<String> names = new ArrayList<String>();
        for (int i = 0; i < services.size() && names.size() < 3; i++) {
            String raw = services.get(i).toString();
            String shortUuid = BleFingerprintRegistry.shortUuid(raw);
            BleFingerprintRegistry.Match match = BleFingerprintRegistry.lookup(shortUuid);
            String name = match == null ? "Service " + (shortUuid.length() <= 8 ? shortUuid : raw.substring(0, 8)) :
                match.label + " [" + shortUuid + "]";
            if (!names.contains(name)) names.add(name);
        }
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < names.size(); i++) {
            if (i > 0) result.append(" + ");
            result.append(names.get(i));
        }
        return result.length() == 0 ? null : result.toString();
    }

    private int parseAppearance(byte[] bytes) {
        if (bytes == null) return -1;
        int index = 0;
        while (index < bytes.length) {
            int length = bytes[index] & 0xff;
            if (length == 0 || index + length >= bytes.length) break;
            if ((bytes[index + 1] & 0xff) == 0x19 && length >= 3)
                return (bytes[index + 2] & 0xff) | ((bytes[index + 3] & 0xff) << 8);
            index += length + 1;
        }
        return -1;
    }

    private String classify(BluetoothDevice device, String broadcast, String vendor, int appearance) {
        String text = ((broadcast == null ? "" : broadcast) + " " +
            (vendor == null ? "" : vendor)).toLowerCase(java.util.Locale.US);
        if (text.indexOf("tile") >= 0 || text.indexOf("airtag") >= 0 ||
            text.indexOf("smarttag") >= 0 || text.indexOf("tracker") >= 0 ||
            text.indexOf("key finder") >= 0 || text.indexOf("tag") >= 0 || text.indexOf("eddystone") >= 0 ||
            text.indexOf("immediate alert") >= 0 || text.indexOf("link loss") >= 0) return "Trackers / Tags";
        if (text.indexOf("airpod") >= 0 || text.indexOf("headphone") >= 0 ||
            text.indexOf("earbud") >= 0 || text.indexOf("speaker") >= 0 ||
            text.indexOf("bose") >= 0 || text.indexOf("jbl") >= 0) return "Audio";
        if (text.indexOf("watch") >= 0 || text.indexOf("band") >= 0 ||
            text.indexOf("fitbit") >= 0 || text.indexOf("wear") >= 0 || text.indexOf("heart rate") >= 0) return "Wearables";
        if (text.indexOf("hid / input") >= 0 || text.indexOf("keyboard") >= 0 ||
            text.indexOf("mouse") >= 0) return "Computers";
        if (text.indexOf("car") >= 0 || text.indexOf("vehicle") >= 0 ||
            text.indexOf("tesla") >= 0 || text.indexOf("auto") >= 0) return "Vehicles";
        int group = appearance & 0xffc0;
        if (group == 0x0200 || group == 0x0240) return "Trackers / Tags";
        if (group == 0x0280) return "Audio";
        if (group == 0x00c0 || group == 0x01c0 || group == 0x0340) return "Wearables";
        if (group == 0x0040) return "Phones";
        if (group == 0x0080) return "Computers";
        try {
            BluetoothClass value = device.getBluetoothClass();
            if (value != null) {
                int major = value.getMajorDeviceClass();
                if (major == BluetoothClass.Device.Major.AUDIO_VIDEO) return "Audio";
                if (major == BluetoothClass.Device.Major.PHONE) return "Phones";
                if (major == BluetoothClass.Device.Major.COMPUTER) return "Computers";
                if (major == BluetoothClass.Device.Major.WEARABLE || major == BluetoothClass.Device.Major.HEALTH) return "Wearables";
            }
        } catch (Exception ignored) { }
        return "Other";
    }

    private String entryLabel(SignalEntry entry) {
        if (entry == null) return "Unknown BLE Device";
        if (useful(entry.customName)) return entry.customName;
        if (useful(entry.broadcastName)) return entry.broadcastName;
        if (useful(entry.vendorName)) return entry.vendorName + " device";
        return uniqueFallback(safeAddress(entry.device));
    }

    private boolean useful(String value) {
        return value != null && value.trim().length() > 0 &&
            !"Unknown Device".equalsIgnoreCase(value) &&
            !"Unknown item".equalsIgnoreCase(value) &&
            !"BLE".equalsIgnoreCase(value.trim()) &&
            !"Bluetooth LE".equalsIgnoreCase(value.trim());
    }

    private String uniqueFallback(String address) {
        if (address == null) return "Unknown BLE Device";
        String compact = address.replace(":", "");
        String suffix = compact.length() > 6 ? compact.substring(compact.length() - 6) : compact;
        return "BLE Device " + suffix;
    }

    private String displayName(BluetoothDevice device) {
        String name = safeName(device);
        if (!"Unknown Device".equals(name)) return name;
        String vendor = database.lookupOui(safeAddress(device));
        DeviceRecord saved = database.get(safeAddress(device));
        if (saved != null && useful(saved.displayName())) return saved.displayName();
        return vendor == null ? uniqueFallback(safeAddress(device)) : vendor + " device";
    }

    private String safeName(BluetoothDevice device) {
        try { String value = device.getName(); return value == null || value.length() == 0
            ? "Unknown Device" : value; }
        catch (Exception error) { return "Unknown Device"; }
    }
    private String safeAddress(BluetoothDevice device) {
        try { return device.getAddress(); } catch (Exception error) { return "Unavailable"; }
    }

    private void stopScan() {
        scanning = false;
        if (radar != null) radar.setScanning(false);
        if (scanButton != null) scanButton.setText("START RADAR SCAN");
        handler.removeCallbacks(pruneTask);
        try { if (leScanner != null && scanCallback != null) leScanner.stopScan(scanCallback); }
        catch (Exception ignored) { }
        try { if (bluetooth != null && bluetooth.isDiscovering()) bluetooth.cancelDiscovery(); }
        catch (Exception ignored) { }
    }

    private int dp(int value) {
        return (int)(value * getResources().getDisplayMetrics().density + 0.5f);
    }

    protected void onPause() { stopScan(); super.onPause(); }
    protected void onDestroy() {
        stopScan(); handler.removeCallbacksAndMessages(null);
        if (receiverRegistered) unregisterReceiver(receiver);
        database.close(); super.onDestroy();
    }

    private class SignalGridAdapter extends BaseAdapter {
        public int getCount() { return visibleSignals().size(); }
        public Object getItem(int position) { return visibleSignals().get(position); }
        public long getItemId(int position) { return position; }
        public View getView(int position, View old, ViewGroup parent) {
            final SignalEntry entry = visibleSignals().get(position);
            final String address = safeAddress(entry.device);
            DeviceRecord toggleRecord = database.get(address);
            LinearLayout row = new LinearLayout(DeviceManagerActivity.this);
            row.setOrientation(LinearLayout.VERTICAL);
            row.setPadding(dp(12), dp(8), dp(10), dp(8));
            row.setBackgroundColor(position % 2 == 0 ? Color.WHITE : Color.rgb(224, 236, 247));
            TextView name = new TextView(DeviceManagerActivity.this);
            name.setText(entryLabel(entry) + "     " + entry.rssi + " dBm");
            name.setTextSize(17); name.setTypeface(null, android.graphics.Typeface.BOLD);
            name.setTextColor(Color.rgb(21, 101, 192));
            name.setOnClickListener(new View.OnClickListener() {
                public void onClick(View view) { copyText("Device name", entryLabel(entry)); }
            }); row.addView(name);
            TextView columns = new TextView(DeviceManagerActivity.this);
            columns.setText((entry.vendorName == null ? "Manufacturer unavailable" : entry.vendorName) +
                "  •  " + entry.category +
                (entry.broadcastType == null ? "" : "  •  " + entry.broadcastType));
            row.addView(columns);
            LinearLayout bottom = new LinearLayout(DeviceManagerActivity.this);
            bottom.setGravity(android.view.Gravity.CENTER_VERTICAL);
            TextView mac = new TextView(DeviceManagerActivity.this);
            mac.setText(address); mac.setTextColor(Color.rgb(0, 121, 107));
            mac.setGravity(android.view.Gravity.CENTER_VERTICAL);
            mac.setOnClickListener(new View.OnClickListener() {
                public void onClick(View view) { copyText("MAC address", address); }
            });
            bottom.addView(mac, new LinearLayout.LayoutParams(0, -1, 1));
            Button edit = new Button(DeviceManagerActivity.this); edit.setText("EDIT");
            styleActionButton(edit, Color.rgb(69, 90, 100));
            edit.setOnClickListener(new View.OnClickListener() {
                public void onClick(View view) { editSignal(entry); }
            }); bottom.addView(edit, listActionButtonParams());
            Button map = new Button(DeviceManagerActivity.this);
            styleToggle(map, toggleRecord != null && toggleRecord.mapEnabled == 1, "MAP");
            map.setOnClickListener(new View.OnClickListener() {
                public void onClick(View view) { toggleMap(entry, (Button)view); }
            }); bottom.addView(map, listActionButtonParams());
            Button track = new Button(DeviceManagerActivity.this);
            styleToggle(track, toggleRecord != null && toggleRecord.liveEnabled == 1, "TRACK");
            track.setOnClickListener(new View.OnClickListener() {
                public void onClick(View view) { toggleTrack(entry, (Button)view); }
            }); bottom.addView(track, listActionButtonParams());
            row.addView(bottom);
            return row;
        }
    }

    private class PairedAdapter extends BaseAdapter {
        public int getCount() { return paired.size(); }
        public Object getItem(int position) { return paired.get(position); }
        public long getItemId(int position) { return position; }
        public View getView(int position, View old, ViewGroup parent) {
            BluetoothDevice device = paired.get(position);
            LinearLayout row = new LinearLayout(DeviceManagerActivity.this);
            row.setOrientation(LinearLayout.VERTICAL);
            row.setPadding(dp(16), dp(10), dp(12), dp(10));
            row.setBackgroundColor(position % 2 == 0 ? Color.WHITE : Color.rgb(224, 236, 247));
            TextView name = new TextView(DeviceManagerActivity.this);
            String address = safeAddress(device);
            SignalEntry signal = signals.get(address);
            name.setText(signal == null ? displayName(device) : entryLabel(signal)); name.setTextSize(17);
            name.setTypeface(null, android.graphics.Typeface.BOLD); row.addView(name);
            TextView detail = new TextView(DeviceManagerActivity.this);
            detail.setText(address + (signal == null ? "  •  Paired, not broadcasting" :
                "  •  BROADCASTING " + signal.rssi + " dBm"));
            detail.setTextColor(signal == null ? Color.GRAY : Color.rgb(25, 118, 210));
            row.addView(detail); return row;
        }
    }
}
