package com.wytehat.btlogger;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.net.Uri;
import android.graphics.Color;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.view.View;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.ArrayAdapter;
import android.widget.AdapterView;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.List;

public class MapActivity extends Activity implements LocationListener, SensorEventListener {
    private TrackerDatabase db;
    private WebView web;
    private CheckBox showAll;
    private String focus;
    private Spinner categorySpinner;
    private ArrayAdapter<String> categoryAdapter;
    private final ArrayList<String> categoryValues = new ArrayList<String>();
    private final ArrayList<String> categoryLabels = new ArrayList<String>();
    private String categoryFilter = "All";
    private boolean rebuildingCategories;
    private LocationManager locationManager;
    private SensorManager sensorManager;
    private Sensor rotationSensor;
    private Location phoneLocation;
    private float phoneHeading;
    private boolean mapReady;
    private static final String[] CATEGORY_ORDER = { "Trackers / Tags", "Audio", "Wearables", "Phones", "Computers", "Vehicles", "Other" };

    protected void onCreate(Bundle state) {
        super.onCreate(state);
        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);
        root.setBackgroundColor(Color.WHITE);
        root.addView(NavigationHelper.createBar(this, 2, "Tracked Item Map"));

        showAll = new CheckBox(this);
        showAll.setText("Troubleshooting: show all scanned devices");
        showAll.setPadding(dp(12), dp(5), dp(12), dp(5));
        showAll.setChecked(getSharedPreferences("map_settings", 0)
            .getBoolean("show_all_scanned", false));
        root.addView(showAll, new LinearLayout.LayoutParams(
            LinearLayout.LayoutParams.MATCH_PARENT,
            LinearLayout.LayoutParams.WRAP_CONTENT));

        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));

        web = new WebView(this);
        web.getSettings().setJavaScriptEnabled(true);
        web.getSettings().setAllowFileAccess(true);
        web.setWebViewClient(new WebViewClient() {
            public void onPageFinished(WebView view, String url) {
                mapReady = true; updatePhoneMarker();
            }
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                if (url != null && url.startsWith("geo:")) { openMap(url); return true; }
                return false;
            }
        });
        root.addView(web, new LinearLayout.LayoutParams(
            LinearLayout.LayoutParams.MATCH_PARENT, 0, 1));
        setContentView(root);

        db = new TrackerDatabase(this);
        locationManager = (LocationManager)getSystemService(LOCATION_SERVICE);
        sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
        if (sensorManager != null) rotationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
        focus = getIntent().getStringExtra("focus");
        categorySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                if (!rebuildingCategories && position >= 0 && position < categoryValues.size()) {
                    categoryFilter = categoryValues.get(position); reloadMap();
                }
            }
            public void onNothingSelected(AdapterView<?> parent) { }
        });
        reloadMap();
        showAll.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                getSharedPreferences("map_settings", 0).edit()
                    .putBoolean("show_all_scanned", showAll.isChecked()).apply();
                reloadMap();
            }
        });
    }

    private void reloadMap() {
        mapReady = false;
        List<DeviceRecord> available = showAll.isChecked() ? db.all() : defaultMapItems();
        refreshMapCategories(available);
        ArrayList<DeviceRecord> rows = new ArrayList<DeviceRecord>();
        for (int i = 0; i < available.size(); i++) {
            DeviceRecord record = available.get(i);
            if (!record.hasLocation) continue;
            if ("All".equals(categoryFilter) || categoryFilter.equals(record.category)) rows.add(record);
        }
        web.loadDataWithBaseURL("https://www.openstreetmap.org/",
            buildHtml(rows, focus), "text/html", "UTF-8", null);
    }

    private void refreshMapCategories(List<DeviceRecord> rows) {
        int[] counts = new int[CATEGORY_ORDER.length];
        int located = 0;
        for (int r = 0; r < rows.size(); r++) {
            DeviceRecord record = rows.get(r);
            if (!record.hasLocation) continue;
            located++;
            for (int i = 0; i < CATEGORY_ORDER.length; i++) {
                if (CATEGORY_ORDER[i].equals(record.category)) { counts[i]++; break; }
            }
        }
        ArrayList<String> nextValues = new ArrayList<String>();
        ArrayList<String> nextLabels = new ArrayList<String>();
        nextValues.add("All"); nextLabels.add("All (" + located + ")");
        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; }
        rebuildingCategories = true;
        if (!nextLabels.equals(categoryLabels)) {
            categoryValues.clear(); categoryValues.addAll(nextValues);
            categoryLabels.clear(); categoryLabels.addAll(nextLabels);
            categoryAdapter.notifyDataSetChanged();
        }
        categorySpinner.setSelection(selected);
        rebuildingCategories = false;
    }

    private List<DeviceRecord> defaultMapItems() {
        List<DeviceRecord> all = db.all();
        ArrayList<DeviceRecord> result = new ArrayList<DeviceRecord>();
        HashSet<String> paired = new HashSet<String>();
        try {
            BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
            if (adapter != null) {
                for (BluetoothDevice device : adapter.getBondedDevices())
                    paired.add(device.getAddress());
            }
        } catch (Exception ignored) { }
        for (int i = 0; i < all.size(); i++) {
            DeviceRecord record = all.get(i);
            if (record.mapEnabled == 1)
                result.add(record);
        }
        return result;
    }

    private String buildHtml(List<DeviceRecord> rows, String selectedAddress) {
        StringBuilder script = new StringBuilder();
        long today = startOfToday();
        for (int i = 0; i < rows.size(); i++) {
            DeviceRecord record = rows.get(i);
            if (!record.hasLocation) continue;
            String color = validColor(record.pinColor);
            List<LocationPoint> history = db.history(record.address, today);
            if (record.showTrail == 1 && history.size() > 1) {
                script.append("var trail").append(i).append("=[");
                for (int p = 0; p < history.size(); p++) {
                    if (p > 0) script.append(',');
                    script.append('[').append(history.get(p).latitude).append(',')
                        .append(history.get(p).longitude).append(']');
                }
                script.append("];L.polyline(trail").append(i)
                    .append(",{color:'").append(color)
                    .append("',weight:4,opacity:.65}).addTo(map);")
                    .append("for(var k=0;k<trail").append(i)
                    .append(".length;k++){L.circleMarker(trail").append(i)
                    .append("[k],{radius:3,color:'").append(color)
                    .append("',fillOpacity:.8}).addTo(map);} ");
            }

            String photo = "";
            if (record.photoUri != null && new java.io.File(record.photoUri).exists()) {
                photo = "<img src='file://" + htmlText(record.photoUri) +
                    "' style='width:72px;height:72px;object-fit:cover;border-radius:10px'><br>";
            }
            StringBuilder detailsBuilder = new StringBuilder(photo);
            detailsBuilder.append("<b>").append(htmlText(record.displayName())).append("</b><br>");
            detailsBuilder.append("<a href='geo:").append(record.latitude).append(",")
                .append(record.longitude).append("?q=").append(record.latitude).append(",")
                .append(record.longitude).append("'>GPS: ").append(record.latitude).append(", ")
                .append(record.longitude).append("</a>");
            if (record.accuracy > 0) detailsBuilder.append(" (±").append(Math.round(record.accuracy)).append(" m)");
            if (hasValue(record.category) && !"Other".equalsIgnoreCase(record.category))
                detailsBuilder.append("<br>Category: ").append(htmlText(record.category));
            if (hasValue(record.vendorName) && !"Unknown".equalsIgnoreCase(record.vendorName) &&
                !"Manufacturer unknown".equalsIgnoreCase(record.vendorName))
                detailsBuilder.append("<br>Manufacturer: ").append(htmlText(record.vendorName));
            if (record.updatedAt > 0) detailsBuilder.append("<br>Last seen: ").append(htmlText(date(record.updatedAt)));
            if (record.battery >= 0) detailsBuilder.append("<br>Battery: ").append(record.battery).append("%");
            if (record.rssi != 0 && record.lastRssiAt > 0)
                detailsBuilder.append("<br>RSSI: ").append(record.rssi).append(" dBm");
            String status = reason(record.reason);
            if (hasValue(status) && !"Status unknown".equalsIgnoreCase(status))
                detailsBuilder.append("<br>Status: ").append(htmlText(status));
            String details = detailsBuilder.toString();
            boolean locked = record.locationLocked == 1;
            int markerSize = locked ? 10 : 22;
            int iconSize = markerSize + 6;
            int anchor = iconSize / 2;
            String markerColor = locked ? "#00A86B" : color;
            script.append("var icon").append(i).append("=L.divIcon({className:'',html:\"")
                .append("<div style='width:").append(markerSize).append("px;height:")
                .append(markerSize).append("px;border-radius:50%;background:")
                .append(markerColor).append(";border:2px solid white;box-shadow:0 1px 4px #333'></div>\"")
                .append(",iconSize:[").append(iconSize).append(",").append(iconSize)
                .append("],iconAnchor:[").append(anchor).append(",").append(anchor).append("]});");
            script.append("var marker").append(i).append("=L.marker([").append(record.latitude)
                .append(',').append(record.longitude).append("],{icon:icon").append(i)
                .append("}).addTo(map).bindPopup('").append(js(details)).append("');")
                .append("bounds.push([").append(record.latitude).append(',')
                .append(record.longitude).append("]);");
            if (record.address != null && record.address.equals(selectedAddress)) {
                script.append("marker").append(i).append(".openPopup();map.setView([").append(
                    record.latitude).append(',').append(record.longitude).append("],21);");
            }
        }

        return "<!doctype html><html><head><meta name='viewport' content='width=device-width," +
            "initial-scale=1'><link rel='stylesheet' href='https://unpkg.com/leaflet@1.9.4/" +
            "dist/leaflet.css'><style>html,body,#map{height:100%;margin:0}.leaflet-popup-content" +
            "{font:14px sans-serif;line-height:1.5}.phone-bearing{position:relative;width:38px;height:38px;transform-origin:19px 19px}.phone-dot{position:absolute;left:9px;top:9px;width:16px;height:16px;border-radius:50%;background:#4285F4;border:3px solid white;box-shadow:0 1px 4px #555}.phone-tip{position:absolute;left:15px;top:1px;width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:10px solid #4285F4}</style></head><body><div id='map'></div>" +
            "<script src='https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'></script><script>" +
            "var streets=L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxNativeZoom:19,maxZoom:22,attribution:'© OpenStreetMap contributors'})," +
            "sat=L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',{maxNativeZoom:19,maxZoom:22,attribution:'Esri World Imagery'})," +
            "map=L.map('map',{layers:[streets]}).setView([20,0],2);" +
            "L.control.layers({'Streets':streets,'Satellite':sat},null,{collapsed:false}).addTo(map);" +
            "var phone=null,phoneShown=false,phoneIcon=L.divIcon({className:'',html:'<div id=phoneBearing class=phone-bearing><div class=phone-dot></div><div class=phone-tip></div></div>',iconSize:[38,38],iconAnchor:[19,19]});" +
            "function updatePhone(a,b,h){var p=[a,b];if(!phone)phone=L.marker(p,{icon:phoneIcon,zIndexOffset:1100}).addTo(map);else phone.setLatLng(p);if(!phoneShown){phoneShown=true;var visible=bounds.slice(0);visible.push(p);if(visible.length>1)map.fitBounds(visible,{padding:[35,35],maxZoom:20});else map.setView(p,20);}var e=document.getElementById('phoneBearing');if(e)e.style.transform='rotate('+h+'deg)';}var bounds=[];" + script + "if(bounds.length>0&&" +
            "'" +
            js(selectedAddress == null ? "" : selectedAddress) +
            "'=='')map.fitBounds(bounds,{padding:[30,30],maxZoom:20});</script></body></html>";
    }

    private long startOfToday() {
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        return calendar.getTimeInMillis();
    }

    private String validColor(String value) {
        if (value != null && value.matches("#[0-9a-fA-F]{6}")) return value;
        return "#E53935";
    }

    private String date(long time) {
        return time <= 0 ? "Unknown" : DateFormat.getDateTimeInstance().format(new Date(time));
    }

    private String reason(String value) {
        if (value == null) return "Status unknown";
        if (value.indexOf("Range") >= 0) return "Out of range";
        if (value.indexOf("Battery") >= 0 || value.indexOf("Power") >= 0)
            return "Possible dead battery";
        return value;
    }

    private boolean hasValue(String value) {
        return value != null && value.trim().length() > 0;
    }

    private String js(String value) {
        return value == null ? "" : value.replace("\\", "\\\\").replace("'", "\\'")
            .replace("\n", " ").replace("\r", " ");
    }

    private String htmlText(String value) {
        return value == null ? "" : value.replace("&", "&amp;").replace("<", "&lt;")
            .replace(">", "&gt;").replace("'", "&#39;");
    }

    protected void onResume() {
        super.onResume();
        try {
            Location gps = locationManager == null ? null : locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
            Location network = locationManager == null ? null : locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
            phoneLocation = gps == null ? network : gps;
            if (locationManager != null) {
                locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000L, 1f, this);
                locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 1000L, 1f, this);
            }
        } catch (Exception ignored) { }
        if (sensorManager != null && rotationSensor != null)
            sensorManager.registerListener(this, rotationSensor, SensorManager.SENSOR_DELAY_UI);
        updatePhoneMarker();
    }

    protected void onPause() {
        try { if (locationManager != null) locationManager.removeUpdates(this); } catch (Exception ignored) { }
        try { if (sensorManager != null) sensorManager.unregisterListener(this); } catch (Exception ignored) { }
        super.onPause();
    }

    public void onLocationChanged(Location location) {
        if (location == null) return;
        phoneLocation = location;
        if (location.hasBearing() && location.hasSpeed() && location.getSpeed() > 0.8f)
            phoneHeading = location.getBearing();
        updatePhoneMarker();
    }
    public void onProviderDisabled(String provider) { }
    public void onProviderEnabled(String provider) { }
    public void onStatusChanged(String provider, int status, Bundle extras) { }

    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() != Sensor.TYPE_ROTATION_VECTOR) return;
        float[] rotation = new float[9]; float[] orientation = new float[3];
        SensorManager.getRotationMatrixFromVector(rotation, event.values);
        SensorManager.getOrientation(rotation, orientation);
        float compass = (float)Math.toDegrees(orientation[0]);
        if (compass < 0) compass += 360f;
        if (phoneLocation == null || !phoneLocation.hasSpeed() || phoneLocation.getSpeed() <= 0.8f)
            phoneHeading = compass;
        updatePhoneMarker();
    }
    public void onAccuracyChanged(Sensor sensor, int accuracy) { }

    private void updatePhoneMarker() {
        if (!mapReady || web == null || phoneLocation == null) return;
        web.loadUrl(String.format(java.util.Locale.US, "javascript:updatePhone(%.7f,%.7f,%.1f)",
            phoneLocation.getLatitude(), phoneLocation.getLongitude(), phoneHeading));
    }

    private void openMap(String uri) {
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
        intent.setPackage("com.google.android.apps.maps");
        try { startActivity(intent); }
        catch (Exception error) {
            intent.setPackage(null);
            try { startActivity(intent); } catch (Exception ignored) { }
        }
    }

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

    protected void onDestroy() {
        if (web != null) web.destroy();
        db.close();
        super.onDestroy();
    }
}
