package com.wytehat.btlogger;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class TrackerDatabase extends SQLiteOpenHelper {
    private static final String DB_NAME = "bluetooth_tracker.db";
    private static final int DB_VERSION = 15;

    public TrackerDatabase(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

	public synchronized List<LocationPoint> history(String address, long historyFrom, long historyTo)
	{
		ArrayList<LocationPoint> result = new ArrayList<LocationPoint>();
		if (address == null) return result;
		Cursor cursor = getReadableDatabase().query("location_history", null,
			"address=? AND logged_at>=? AND logged_at<=?",
			new String[] { address, String.valueOf(historyFrom), String.valueOf(historyTo) },
			null, null, "logged_at ASC");
		try {
			while (cursor.moveToNext()) {
				LocationPoint point = new LocationPoint();
				point.latitude = cursor.getDouble(cursor.getColumnIndex("latitude"));
				point.longitude = cursor.getDouble(cursor.getColumnIndex("longitude"));
				point.accuracy = cursor.getFloat(cursor.getColumnIndex("accuracy"));
				point.timestamp = cursor.getLong(cursor.getColumnIndex("logged_at"));
				result.add(point);
			}
		} finally { cursor.close(); }
		return result;
	}

    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE devices (address TEXT PRIMARY KEY,name TEXT," +
            "connected INTEGER NOT NULL DEFAULT 0,connected_at INTEGER DEFAULT 0," +
            "disconnected_at INTEGER DEFAULT 0,latitude REAL,longitude REAL,accuracy REAL," +
            "has_location INTEGER DEFAULT 0,battery INTEGER DEFAULT -1,rssi INTEGER DEFAULT 0," +
            "reason TEXT,updated_at INTEGER DEFAULT 0,photo_uri TEXT,icon_preset TEXT DEFAULT 'bag'," +
            "gps_polling_rate INTEGER DEFAULT 900000,pin_color TEXT DEFAULT '#E53935'," +
            "tracking_priority INTEGER DEFAULT 15,photo_path TEXT,device_gps_interval INTEGER DEFAULT 15," +
            "is_tracked INTEGER DEFAULT 0,custom_name TEXT,broadcast_name TEXT,vendor_name TEXT,tx_power_1m INTEGER DEFAULT -59,path_loss_exponent REAL DEFAULT 2.7,last_rssi_at INTEGER DEFAULT 0,device_category TEXT,category_manual INTEGER DEFAULT 0,vendor_manual INTEGER DEFAULT 0,map_enabled INTEGER DEFAULT 0,live_enabled INTEGER DEFAULT 0,location_locked INTEGER DEFAULT 0,show_trail INTEGER DEFAULT 1,track_broadcast INTEGER DEFAULT 0)");
        createSupportingTables(db);
        seedVendors(db);
    }

    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion < 2) {
            addColumn(db, "ALTER TABLE devices ADD COLUMN photo_uri TEXT");
            addColumn(db, "ALTER TABLE devices ADD COLUMN icon_preset TEXT DEFAULT 'bag'");
            addColumn(db, "ALTER TABLE devices ADD COLUMN gps_polling_rate INTEGER DEFAULT 900000");
        }
        if (oldVersion < 3) {
            addColumn(db, "ALTER TABLE devices ADD COLUMN pin_color TEXT DEFAULT '#E53935'");
            addColumn(db, "ALTER TABLE devices ADD COLUMN tracking_priority INTEGER DEFAULT 15");
            addColumn(db, "ALTER TABLE devices ADD COLUMN photo_path TEXT");
            addColumn(db, "ALTER TABLE devices ADD COLUMN device_gps_interval INTEGER DEFAULT 15");
            addColumn(db, "ALTER TABLE devices ADD COLUMN is_tracked INTEGER DEFAULT 0");
            addColumn(db, "ALTER TABLE devices ADD COLUMN custom_name TEXT");
            addColumn(db, "ALTER TABLE devices ADD COLUMN broadcast_name TEXT");
            addColumn(db, "ALTER TABLE devices ADD COLUMN vendor_name TEXT");
            db.execSQL("UPDATE devices SET broadcast_name=name,photo_path=photo_uri," +
                "device_gps_interval=CASE WHEN gps_polling_rate=0 THEN 0 ELSE gps_polling_rate/60000 END," +
                "tracking_priority=CASE WHEN gps_polling_rate=0 THEN 0 ELSE gps_polling_rate/60000 END," +
                "is_tracked=CASE WHEN connected=1 OR connected_at>0 OR photo_uri IS NOT NULL THEN 1 ELSE 0 END");
            createSupportingTables(db);
            seedVendors(db);
        }

        if (oldVersion < 4) {
            addColumn(db, "ALTER TABLE devices ADD COLUMN tx_power_1m INTEGER DEFAULT -59");
            addColumn(db, "ALTER TABLE devices ADD COLUMN path_loss_exponent REAL DEFAULT 2.7");
            createSupportingTables(db);
        }
        if (oldVersion < 5) {
            addColumn(db, "ALTER TABLE devices ADD COLUMN last_rssi_at INTEGER DEFAULT 0");
        }
        if (oldVersion < 6) addColumn(db, "ALTER TABLE devices ADD COLUMN device_category TEXT");
        if (oldVersion < 7) addColumn(db, "ALTER TABLE devices ADD COLUMN category_manual INTEGER DEFAULT 0");
        if (oldVersion < 8) addColumn(db, "ALTER TABLE devices ADD COLUMN vendor_manual INTEGER DEFAULT 0");
        if (oldVersion < 9) db.execSQL("UPDATE devices SET updated_at=0 WHERE last_rssi_at=0 AND rssi=0 AND connected_at=0 AND disconnected_at=0 AND has_location=0 AND battery<0");
        if (oldVersion < 10) {
            addColumn(db, "ALTER TABLE devices ADD COLUMN map_enabled INTEGER DEFAULT 0");
            addColumn(db, "ALTER TABLE devices ADD COLUMN live_enabled INTEGER DEFAULT 0");
            db.execSQL("UPDATE devices SET map_enabled=1 WHERE has_location=1 AND is_tracked=1");
        }
        if (oldVersion < 11) addColumn(db, "ALTER TABLE devices ADD COLUMN location_locked INTEGER DEFAULT 0");
        if (oldVersion < 12) {
            addColumn(db, "ALTER TABLE devices ADD COLUMN show_trail INTEGER DEFAULT 1");
            db.execSQL("UPDATE devices SET show_trail=0 WHERE location_locked=1");
        }
        if (oldVersion < 13)
            addColumn(db, "ALTER TABLE devices ADD COLUMN track_broadcast INTEGER DEFAULT 0");
        if (oldVersion < 14) createSupportingTables(db);
        if (oldVersion < 15) seedVendors(db);
    }

    private void addColumn(SQLiteDatabase db, String sql) {
        try { db.execSQL(sql); } catch (Exception ignored) { }
    }

    private void createSupportingTables(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE IF NOT EXISTS location_history (id INTEGER PRIMARY KEY AUTOINCREMENT," +
            "address TEXT,latitude REAL,longitude REAL,accuracy REAL,logged_at INTEGER)");
        db.execSQL("CREATE INDEX IF NOT EXISTS history_address_time ON location_history(address,logged_at)");
        db.execSQL("CREATE TABLE IF NOT EXISTS range_samples (id INTEGER PRIMARY KEY AUTOINCREMENT," +
            "address TEXT,session_id TEXT,sampled_at INTEGER,latitude REAL,longitude REAL," +
            "accuracy REAL,raw_rssi INTEGER,filtered_rssi REAL,heading REAL,range_state TEXT)");
        db.execSQL("CREATE INDEX IF NOT EXISTS range_sample_session ON range_samples(session_id,sampled_at)");
        db.execSQL("CREATE TABLE IF NOT EXISTS vendor_lookup (lookup_type TEXT,hex_code TEXT," +
            "vendor_name TEXT,PRIMARY KEY(lookup_type,hex_code))");
        db.execSQL("CREATE TABLE IF NOT EXISTS device_observations (id INTEGER PRIMARY KEY AUTOINCREMENT," +
            "address TEXT,first_seen_at INTEGER,last_seen_at INTEGER,rssi INTEGER,strongest_rssi INTEGER," +
            "sighting_count INTEGER DEFAULT 1,broadcast_name TEXT,vendor_name TEXT,device_category TEXT," +
            "broadcast_type TEXT,appearance INTEGER DEFAULT -1,company_id INTEGER DEFAULT -1," +
            "advertisement_hex TEXT,latitude REAL,longitude REAL,accuracy REAL,has_location INTEGER DEFAULT 0," +
            "uploaded INTEGER DEFAULT 0)");
        db.execSQL("CREATE INDEX IF NOT EXISTS observation_address_time ON device_observations(address,last_seen_at)");
        db.execSQL("CREATE INDEX IF NOT EXISTS observation_upload ON device_observations(uploaded,last_seen_at)");
    }

	private void seedVendors(SQLiteDatabase db) {
        addVendor(db, "OUI", "A4C3F0", "Intel Corporate");
        addVendor(db, "OUI", "0000F0", "Samsung Electronics");
        addVendor(db, "OUI", "0452C7", "Bose Corporation");
        addVendor(db, "OUI", "C4E510", "Tile, Inc.");
        addVendor(db, "OUI", "001A80", "Sony Corporation");
        addVendor(db, "OUI", "5882A8", "Xiaomi Communications");
        addVendor(db, "OUI", "F4F5D8", "Google LLC");
        addVendor(db, "COMPANY", "0006", "Microsoft Corporation");
        addVendor(db, "COMPANY", "0059", "Nordic Semiconductor ASA");
        addVendor(db, "COMPANY", "0075", "Samsung Electronics");
        addVendor(db, "COMPANY", "009E", "Bose Corporation");
        addVendor(db, "COMPANY", "00E0", "Google LLC");
        addVendor(db, "COMPANY", "018E", "Fitbit, Inc.");
        addVendor(db, "COMPANY", "067C", "Tile, Inc.");
        addVendor(db, "COMPANY", "0171", "Amazon.com Services LLC");
        addVendor(db, "COMPANY", "00D0", "Dexcom, Inc.");
        addVendor(db, "COMPANY", "0131", "Cypress Semiconductor");
    }
	
    private void addVendor(SQLiteDatabase db, String type, String code, String vendor) {
        ContentValues values = new ContentValues();
        values.put("lookup_type", type);
        values.put("hex_code", code);
        values.put("vendor_name", vendor);
        db.insertWithOnConflict("vendor_lookup", null, values, SQLiteDatabase.CONFLICT_REPLACE);
    }

    public synchronized void connected(String address, String broadcast, long time, Fix fix) {
        DeviceRecord old = get(address);
        ContentValues values = base(address, broadcast, time, fix);
        values.put("connected", 1);
        values.put("connected_at", time);
        values.put("disconnected_at", 0);
        values.put("reason", "Connected");
        values.put("is_tracked", 1);
        if (old == null) values.put("pin_color", colorFor(address));
        if (old != null) preserveProfile(values, old, fix);
        getWritableDatabase().insertWithOnConflict("devices", null, values,
            SQLiteDatabase.CONFLICT_REPLACE);
        if (fix != null) insertHistory(address, fix, time);
    }

    public synchronized void disconnected(String address, String broadcast, long time,
                                            Fix fix, String reason) {
        ContentValues values = base(address, broadcast, time, fix);
        values.put("connected", 0);
        values.put("disconnected_at", time);
        values.put("reason", reason);
        values.put("is_tracked", 1);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
        if (fix != null) insertHistory(address, fix, time);
    }

    public synchronized void telemetry(String address, String broadcast, Integer battery,
                                       Integer rssi, long time, Fix fix) {
        DeviceRecord old = get(address);
        ContentValues values = base(address, broadcast, time, fix);
        if (battery != null) values.put("battery", battery.intValue());
        if (rssi != null) { values.put("rssi", rssi.intValue()); values.put("last_rssi_at", time); }
        if (old == null) {
            values.put("connected", 0);
            getWritableDatabase().insert("devices", null, values);
        } else {
            getWritableDatabase().update("devices", values, "address=?", new String[] { address });
        }
    }
    public synchronized void recordObservation(String address, long time, int rssi,
                                               String broadcastName, String vendorName,
                                               String category, String broadcastType,
                                               int appearance, int companyId, String advertisementHex,
                                               Fix fix) {
        if (address == null || address.length() == 0 || rssi == 0 || rssi == 127) return;
        SQLiteDatabase db = getWritableDatabase();
        Cursor cursor = db.query("device_observations",
            new String[] { "id", "last_seen_at", "strongest_rssi", "sighting_count" },
            "address=?", new String[] { address }, null, null, "last_seen_at DESC", "1");
        long id = -1L; long last = 0L; int strongest = rssi; int count = 0;
        try {
            if (cursor.moveToFirst()) {
                id = cursor.getLong(0); last = cursor.getLong(1);
                strongest = Math.max(rssi, cursor.getInt(2)); count = cursor.getInt(3);
            }
        } finally { cursor.close(); }
        ContentValues values = new ContentValues();
        values.put("address", address); values.put("last_seen_at", time); values.put("rssi", rssi);
        values.put("strongest_rssi", strongest);
        if (broadcastName != null) values.put("broadcast_name", broadcastName);
        if (vendorName != null) values.put("vendor_name", vendorName);
        if (category != null) values.put("device_category", category);
        if (broadcastType != null) values.put("broadcast_type", broadcastType);
        values.put("appearance", appearance); values.put("company_id", companyId);
        if (advertisementHex != null) values.put("advertisement_hex", advertisementHex);
        if (fix != null) {
            values.put("latitude", fix.latitude); values.put("longitude", fix.longitude);
            values.put("accuracy", fix.accuracy); values.put("has_location", 1);
        }
        if (id >= 0L && time - last <= 15000L) {
            values.put("sighting_count", count + 1);
            db.update("device_observations", values, "id=?", new String[] { String.valueOf(id) });
        } else {
            values.put("first_seen_at", time); values.put("sighting_count", 1); values.put("uploaded", 0);
            db.insert("device_observations", null, values);
        }
    }


    public synchronized void updateLocation(String address, Fix fix, long time) {
        if (fix == null) return;
        ContentValues values = new ContentValues();
        values.put("latitude", fix.latitude);
        values.put("longitude", fix.longitude);
        values.put("accuracy", fix.accuracy);
        values.put("has_location", 1);
        values.put("updated_at", time);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
        insertHistory(address, fix, time);
    }

    private void insertHistory(String address, Fix fix, long time) {
        ContentValues values = new ContentValues();
        values.put("address", address);
        values.put("latitude", fix.latitude);
        values.put("longitude", fix.longitude);
        values.put("accuracy", fix.accuracy);
        values.put("logged_at", time);
        getWritableDatabase().insert("location_history", null, values);
    }

    public synchronized void updateProfile(String address, String title, String photo,
                                           String icon) {
        ContentValues values = new ContentValues();
        if (title != null && title.trim().length() > 0) values.put("custom_name", title.trim());
        values.put("photo_uri", photo);
        values.put("photo_path", photo);
        values.put("icon_preset", icon);
        values.put("is_tracked", 1);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void updateCalibration(String address, int txPower, double exponent) {
        ContentValues values = new ContentValues();
        values.put("tx_power_1m", txPower);
        values.put("path_loss_exponent", exponent);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void logRangeSample(String address, String sessionId, long time,
                                             double latitude, double longitude, float accuracy,
                                             int rawRssi, double filteredRssi, float heading,
                                             String state) {
        ContentValues values = new ContentValues();
        values.put("address", address); values.put("session_id", sessionId);
        values.put("sampled_at", time); values.put("latitude", latitude);
        values.put("longitude", longitude); values.put("accuracy", accuracy);
        values.put("raw_rssi", rawRssi); values.put("filtered_rssi", filteredRssi);
        values.put("heading", heading); values.put("range_state", state);
        getWritableDatabase().insert("range_samples", null, values);
    }

    public synchronized void updateTracking(String address, int minutes, String color) {
        ContentValues values = new ContentValues();
        values.put("tracking_priority", minutes);
        values.put("device_gps_interval", minutes);
        values.put("gps_polling_rate", minutes == 0 ? 0 : minutes * 60000);
        values.put("pin_color", color);
        values.put("is_tracked", 1);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void markTracked(String address, String broadcast) {
        DeviceRecord old = get(address);
        if (old == null) {
            ContentValues created = base(address, broadcast, 0L, null);
            created.put("connected", 0);
            created.put("is_tracked", 1);
            created.put("pin_color", colorFor(address));
            getWritableDatabase().insert("devices", null, created);
            return;
        }
        ContentValues values = new ContentValues();
        values.put("is_tracked", 1);
        if (old.tracked == 0) values.put("pin_color", colorFor(address));
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void removeFromTracking(String address) {
        ContentValues values = new ContentValues();
        values.put("is_tracked", 0);
        values.put("map_enabled", 0);
        values.put("live_enabled", 0);
        values.put("track_broadcast", 0);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void setMapEnabled(String address, boolean enabled) {
        ContentValues values = new ContentValues(); values.put("map_enabled", enabled ? 1 : 0);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void setLiveEnabled(String address, boolean enabled) {
        ContentValues values = new ContentValues(); values.put("live_enabled", enabled ? 1 : 0);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void setTrackBroadcast(String address, boolean enabled) {
        ContentValues values = new ContentValues(); values.put("track_broadcast", enabled ? 1 : 0);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void setShowTrail(String address, boolean shown) {
        ContentValues values = new ContentValues(); values.put("show_trail", shown ? 1 : 0);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void setLocationLocked(String address, boolean locked) {
        ContentValues values = new ContentValues(); values.put("location_locked", locked ? 1 : 0);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void setCustomName(String address, String name) {
        ContentValues values = new ContentValues();
        values.put("custom_name", name == null ? null : name.trim());
        values.put("is_tracked", 1);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void setDetectedCategory(String address, String category) {
        DeviceRecord record = get(address);
        if (record != null && record.categoryManual == 1) return;
        ContentValues values = new ContentValues();
        values.put("device_category", category);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void setCategory(String address, String category) {
        if (category == null) return;
        ContentValues values = new ContentValues();
        values.put("device_category", category);
        values.put("category_manual", 1);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void setVendor(String address, String vendor) {
        DeviceRecord old = get(address);
        if (old != null && old.vendorManual == 1) return;
        if (vendor == null) return;
        ContentValues values = new ContentValues();
        values.put("vendor_name", vendor);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized void setManualVendor(String address, String vendor) {
        ContentValues values = new ContentValues();
        values.put("vendor_name", vendor == null ? null : vendor.trim());
        values.put("vendor_manual", 1);
        getWritableDatabase().update("devices", values, "address=?", new String[] { address });
    }

    public synchronized String lookupOui(String address) {
        if (address == null) return null;
        String code = address.replace(":", "").replace("-", "").toUpperCase(Locale.US);
        if (code.length() < 6) return null;
        return lookupVendor("OUI", code.substring(0, 6));
    }

    public synchronized String lookupCompany(int companyId) {
        return lookupVendor("COMPANY", String.format(Locale.US, "%04X", companyId & 0xffff));
    }

    private String lookupVendor(String type, String code) {
        Cursor cursor = getReadableDatabase().query("vendor_lookup",
            new String[] { "vendor_name" }, "lookup_type=? AND hex_code=?",
            new String[] { type, code }, null, null, null);
        try { return cursor.moveToFirst() ? cursor.getString(0) : null; }
        finally { cursor.close(); }
    }

    public synchronized DeviceRecord get(String address) {
        Cursor cursor = getReadableDatabase().query("devices", null, "address=?",
            new String[] { address }, null, null, null);
        try { return cursor.moveToFirst() ? read(cursor) : null; }
        finally { cursor.close(); }
    }

    public synchronized List<DeviceRecord> all() { return queryDevices(null, null); }
    public synchronized List<DeviceRecord> tracked() {
        return queryDevices("is_tracked=1", null);
    }

    private List<DeviceRecord> queryDevices(String where, String[] args) {
        ArrayList<DeviceRecord> result = new ArrayList<DeviceRecord>();
        Cursor cursor = getReadableDatabase().query("devices", null, where, args,
            null, null, "updated_at DESC");
        try { while (cursor.moveToNext()) result.add(read(cursor)); }
        finally { cursor.close(); }
        return result;
    }

    public synchronized List<LocationPoint> history(String address, long since) {
        ArrayList<LocationPoint> result = new ArrayList<LocationPoint>();
        Cursor cursor = getReadableDatabase().query("location_history", null,
            "address=? AND logged_at>=?", new String[] { address, String.valueOf(since) },
            null, null, "logged_at ASC");
        try {
            while (cursor.moveToNext()) {
                LocationPoint point = new LocationPoint();
                point.latitude = cursor.getDouble(cursor.getColumnIndex("latitude"));
                point.longitude = cursor.getDouble(cursor.getColumnIndex("longitude"));
                point.accuracy = cursor.getFloat(cursor.getColumnIndex("accuracy"));
                point.timestamp = cursor.getLong(cursor.getColumnIndex("logged_at"));
                result.add(point);
            }
        } finally { cursor.close(); }
        return result;
    }

    private ContentValues base(String address, String broadcast, long time, Fix fix) {
        ContentValues values = new ContentValues();
        values.put("address", address);
        if (meaningfulBroadcast(broadcast)) {
            values.put("name", broadcast.trim());
            values.put("broadcast_name", broadcast.trim());
        }
        values.put("updated_at", time);
        if (fix != null) {
            values.put("latitude", fix.latitude);
            values.put("longitude", fix.longitude);
            values.put("accuracy", fix.accuracy);
            values.put("has_location", 1);
        }
        return values;
    }
    private boolean meaningfulBroadcast(String value) {
        return value != null && value.trim().length() > 0
            && !"Unknown Device".equalsIgnoreCase(value.trim())
            && !"Unknown item".equalsIgnoreCase(value.trim());
    }


    private void preserveProfile(ContentValues values, DeviceRecord old, Fix fix) {
        values.put("battery", old.battery);
        values.put("rssi", old.rssi);
        values.put("last_rssi_at", old.lastRssiAt);
        values.put("photo_uri", old.photoUri);
        values.put("photo_path", old.photoUri);
        values.put("icon_preset", old.iconPreset);
        values.put("pin_color", old.pinColor);
        values.put("tracking_priority", old.trackingPriority);
        values.put("device_gps_interval", old.trackingPriority);
        values.put("gps_polling_rate", old.gpsPollingRate);
        values.put("track_broadcast", old.trackBroadcast);
        values.put("custom_name", old.customName);
        values.put("vendor_name", old.vendorName);
        values.put("tx_power_1m", old.txPowerAtOneMeter);
        values.put("path_loss_exponent", old.pathLossExponent);
        if (fix == null && old.hasLocation) {
            values.put("latitude", old.latitude);
            values.put("longitude", old.longitude);
            values.put("accuracy", old.accuracy);
            values.put("has_location", 1);
        }
    }

    private DeviceRecord read(Cursor c) {
        DeviceRecord r = new DeviceRecord();
        r.address = string(c, "address");
        r.name = string(c, "name");
        r.customName = string(c, "custom_name");
        r.broadcastName = string(c, "broadcast_name");
        r.vendorName = string(c, "vendor_name");
        r.category = string(c, "device_category");
        if (r.category == null) r.category = "Other";
        r.categoryManual = integer(c, "category_manual");
        r.vendorManual = integer(c, "vendor_manual");
        r.mapEnabled = integer(c, "map_enabled");
        r.liveEnabled = integer(c, "live_enabled");
        r.locationLocked = integer(c, "location_locked");
        r.showTrail = integer(c, "show_trail");
        r.trackBroadcast = integer(c, "track_broadcast");
        r.connected = integer(c, "connected");
        r.connectedAt = number(c, "connected_at");
        r.disconnectedAt = number(c, "disconnected_at");
        r.latitude = decimal(c, "latitude");
        r.longitude = decimal(c, "longitude");
        r.accuracy = (float) decimal(c, "accuracy");
        r.hasLocation = integer(c, "has_location") == 1;
        r.battery = integer(c, "battery");
        r.rssi = integer(c, "rssi");
        r.lastRssiAt = number(c, "last_rssi_at");
        r.reason = string(c, "reason");
        r.updatedAt = number(c, "updated_at");
        r.photoUri = string(c, "photo_path");
        if (r.photoUri == null) r.photoUri = string(c, "photo_uri");
        r.iconPreset = string(c, "icon_preset");
        r.pinColor = string(c, "pin_color");
        if (r.pinColor == null) r.pinColor = "#E53935";
        r.trackingPriority = integer(c, "device_gps_interval");
        r.gpsPollingRate = r.trackingPriority == 0 ? 0 : r.trackingPriority * 60000;
        r.tracked = integer(c, "is_tracked");
        r.txPowerAtOneMeter = integer(c, "tx_power_1m");
        if (r.txPowerAtOneMeter == 0) r.txPowerAtOneMeter = -59;
        r.pathLossExponent = decimal(c, "path_loss_exponent");
        if (r.pathLossExponent <= 0) r.pathLossExponent = 2.7;
        return r;
    }

    private int column(Cursor c, String name) { return c.getColumnIndex(name); }
    private String string(Cursor c, String name) {
        int index = column(c, name);
        return index < 0 || c.isNull(index) ? null : c.getString(index);
    }
    private int integer(Cursor c, String name) {
        int index = column(c, name);
        return index < 0 || c.isNull(index) ? 0 : c.getInt(index);
    }
    private long number(Cursor c, String name) {
        int index = column(c, name);
        return index < 0 || c.isNull(index) ? 0 : c.getLong(index);
    }
    private double decimal(Cursor c, String name) {
        int index = column(c, name);
        return index < 0 || c.isNull(index) ? 0 : c.getDouble(index);
    }
    private String colorFor(String address) {
        String[] colors = { "#E53935", "#1E88E5", "#43A047", "#FB8C00", "#8E24AA", "#00897B" };
        int hash = address == null ? 0 : address.hashCode();
        if (hash == Integer.MIN_VALUE) hash = 0;
        return colors[Math.abs(hash) % colors.length];
    }

    private String safe(String value) {
        return value == null || value.length() == 0 ? "Unknown item" : value;
    }

    public static class Fix {
        public double latitude;
        public double longitude;
        public float accuracy;
        public Fix(double lat, double lon, float acc) {
            latitude = lat; longitude = lon; accuracy = acc;
        }
    }
}
