# bme680_ui.py (VERSION: BME680-GRAPH+BAR-1)
print("=== BME680 UI VERSION: BME680-GRAPH+BAR-1 ===")

import asyncio
import threading
import time
import math
import sys

from bleak import BleakClient
from PyQt5 import QtWidgets, QtCore
import pyqtgraph as pg

# ====== HARD-CODED DEVICE + UUIDs ======
ADDRESS = "98:88:E0:76:30:56"  # change if your BME680 device uses a different MAC
CHAR_UUID = "7e2a0002-1111-2222-3333-444455556666"  # change if different from ADXL project

# ====== UI SETTINGS ======
HISTORY_SECONDS = 120.0   # 2 minutes of history (BME changes slower than accel)
UI_UPDATE_MS = 250        # 4 Hz UI refresh is plenty for env data

# “Air change” percent meter tuning (based on gas resistance change)
# We measure delta from a slowly-updated baseline.
BASELINE_ALPHA = 0.01     # slower = steadier baseline
DELTA_FULL_SCALE = 20000  # ohms delta => 100% (tune this)

# Thresholds for indicator states (percent)
WARN_PCT  = 30
ALERT_PCT = 60

latest = {
    "t": None,   # °C
    "h": None,   # %
    "gas": None, # ohms
    "p": None,   # hPa
    "alt": None, # m
    "ts": 0.0,
    "count": 0,
    "raw": "",
}

# Buffers
t_buf = []
temp_buf = []
hum_buf  = []
press_buf = []
gas_buf  = []
alt_buf  = []

gas_baseline = None  # running baseline for “change” meter


def parse_csv_5(s: str):
    s = s.strip().strip('"')
    parts = s.split(",")
    if len(parts) != 5:
        return None
    try:
        # temp, hum, gas_ohms, pressure_hpa, altitude_m
        return float(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])
    except ValueError:
        return None


def notify_handler(_sender: int, data: bytearray):
    s = data.decode(errors="ignore").strip()
    latest["raw"] = s
    v = parse_csv_5(s)
    if v is None:
        return

    latest["t"], latest["h"], latest["gas"], latest["p"], latest["alt"] = v
    latest["ts"] = time.time()
    latest["count"] += 1


async def ble_loop():
    while True:
        try:
            print(f"Connecting to {ADDRESS} ...")
            async with BleakClient(ADDRESS, timeout=15.0) as client:
                if not client.is_connected:
                    raise RuntimeError("Connect failed.")

                print("Connected. Enabling notifications...")
                await client.start_notify(CHAR_UUID, notify_handler)
                print("Notify enabled. Streaming...\n")

                while True:
                    await asyncio.sleep(1.0)

        except Exception as e:
            print(f"[BLE] Error/disconnect: {e}")
            print("Reconnecting in 2 seconds...\n")
            await asyncio.sleep(2.0)


def start_ble_thread():
    t = threading.Thread(target=lambda: asyncio.run(ble_loop()), daemon=True)
    t.start()


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("BME680 – Live Graph + Air-Change Meter (BLE)")

        central = QtWidgets.QWidget()
        self.setCentralWidget(central)

        outer = QtWidgets.QVBoxLayout(central)
        outer.setContentsMargins(8, 8, 8, 8)
        outer.setSpacing(8)

        self.status = QtWidgets.QLabel("Waiting for data...")
        self.status.setStyleSheet("color:white; background:rgba(0,0,0,140); padding:8px;")
        outer.addWidget(self.status)

        # Bottom area: plots + meter on right
        row = QtWidgets.QHBoxLayout()
        row.setSpacing(10)
        outer.addLayout(row)

        # One plot with multiple y-scales is messy; we’ll use 2 plots stacked.
        plots_col = QtWidgets.QVBoxLayout()
        plots_col.setSpacing(8)
        row.addLayout(plots_col, stretch=5)

        # Plot 1: Temp + Humidity
        self.plot1 = pg.PlotWidget()
        self.plot1.showGrid(x=True, y=True, alpha=0.25)
        self.plot1.setLabel("left", "Temp (°C) / Humidity (%)")
        self.plot1.setLabel("bottom", "Time (s)")
        self.plot1.addLegend(offset=(10, 10))
        plots_col.addWidget(self.plot1)

        self.curve_temp = self.plot1.plot([], [], name="Temp °C", pen=pg.mkPen('r', width=2))
        self.curve_hum  = self.plot1.plot([], [], name="Humidity %", pen=pg.mkPen('g', width=2))

        # Plot 2: Pressure + Gas (gas is big so it’ll dominate visually; that’s ok)
        self.plot2 = pg.PlotWidget()
        self.plot2.showGrid(x=True, y=True, alpha=0.25)
        self.plot2.setLabel("left", "Pressure (hPa) / Gas (Ω)")
        self.plot2.setLabel("bottom", "Time (s)")
        self.plot2.addLegend(offset=(10, 10))
        plots_col.addWidget(self.plot2)

        self.curve_press = self.plot2.plot([], [], name="Pressure hPa", pen=pg.mkPen('c', width=2))
        self.curve_gas   = self.plot2.plot([], [], name="Gas Ω", pen=pg.mkPen('y', width=2))

        # Meter panel on right
        panel = QtWidgets.QVBoxLayout()
        panel.setSpacing(8)
        row.addLayout(panel, stretch=1)

        title = QtWidgets.QLabel("Air change (gas Δ)")
        title.setStyleSheet("font-weight: 700;")
        panel.addWidget(title)

        self.bar = QtWidgets.QProgressBar()
        self.bar.setRange(0, 100)
        self.bar.setValue(0)
        self.bar.setMinimumWidth(240)
        self.bar.setMinimumHeight(28)
        self.bar.setFormat("%p%")
        panel.addWidget(self.bar)

        self.readout = QtWidgets.QLabel("gas: --- Ω\nbaseline: --- Ω\nΔ: --- Ω")
        panel.addWidget(self.readout)

        self.ind = QtWidgets.QLabel("CALM")
        self.ind.setAlignment(QtCore.Qt.AlignCenter)
        self.ind.setStyleSheet(
            "font-weight: 800; font-size: 20px; padding: 10px;"
            "border: 2px solid #555; border-radius: 10px;"
            "background: rgba(80,80,80,120); color: white;"
        )
        panel.addWidget(self.ind)
        panel.addStretch(1)

        self.t0 = time.time()

        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.update_ui)
        self.timer.start(UI_UPDATE_MS)

    def set_state(self, state: str):
        if state == "CALM":
            self.ind.setText("CALM")
            self.ind.setStyleSheet(
                "font-weight: 800; font-size: 20px; padding: 10px;"
                "border: 2px solid #555; border-radius: 10px;"
                "background: rgba(80,80,80,120); color: white;"
            )
        elif state == "WARN":
            self.ind.setText("CHANGE")
            self.ind.setStyleSheet(
                "font-weight: 800; font-size: 20px; padding: 10px;"
                "border: 2px solid #a86a00; border-radius: 10px;"
                "background: rgba(255,165,0,140); color: black;"
            )
        else:
            self.ind.setText("BIG CHANGE")
            self.ind.setStyleSheet(
                "font-weight: 900; font-size: 20px; padding: 10px;"
                "border: 2px solid #b00000; border-radius: 10px;"
                "background: rgba(255,60,60,180); color: white;"
            )

    def update_ui(self):
        global gas_baseline

        if latest["t"] is None:
            return

        now = time.time()
        t = now - self.t0

        temp = latest["t"]
        hum  = latest["h"]
        gas  = latest["gas"]
        pres = latest["p"]
        alt  = latest["alt"]

        # Update baseline slowly
        if gas_baseline is None:
            gas_baseline = gas
        else:
            gas_baseline = (1.0 - BASELINE_ALPHA) * gas_baseline + BASELINE_ALPHA * gas

        delta = abs(gas - gas_baseline)
        pct = int(max(0.0, min(1.0, delta / max(1e-6, DELTA_FULL_SCALE))) * 100.0)
        self.bar.setValue(pct)

        if pct >= ALERT_PCT:
            self.set_state("ALERT")
        elif pct >= WARN_PCT:
            self.set_state("WARN")
        else:
            self.set_state("CALM")

        self.readout.setText(
            f"gas: {gas:,.0f} Ω\nbaseline: {gas_baseline:,.0f} Ω\nΔ: {delta:,.0f} Ω"
        )

        # Buffers
        t_buf.append(t)
        temp_buf.append(temp)
        hum_buf.append(hum)
        press_buf.append(pres)
        gas_buf.append(gas)

        # Trim
        while t_buf and (t - t_buf[0]) > HISTORY_SECONDS:
            t_buf.pop(0)
            temp_buf.pop(0)
            hum_buf.pop(0)
            press_buf.pop(0)
            gas_buf.pop(0)

        # Update plots
        self.curve_temp.setData(t_buf, temp_buf)
        self.curve_hum.setData(t_buf, hum_buf)
        self.curve_press.setData(t_buf, press_buf)
        self.curve_gas.setData(t_buf, gas_buf)

        if t_buf:
            xmin = max(0, t_buf[-1] - HISTORY_SECONDS)
            xmax = t_buf[-1]
            self.plot1.setXRange(xmin, xmax)
            self.plot2.setXRange(xmin, xmax)

        age_ms = (now - latest["ts"]) * 1000.0 if latest["ts"] else 0.0
        self.status.setText(
            f"Packets: {latest['count']}   Age: {age_ms:.0f} ms\n"
            f"T={temp:.2f} °C   RH={hum:.1f}%   P={pres:.1f} hPa   Gas={gas:,.0f} Ω   Alt={alt:.1f} m"
        )


def main():
    print("IMPORTANT: Disconnect nRF Connect (or any other BLE client) first.")
    start_ble_thread()

    app = QtWidgets.QApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)

    win = MainWindow()
    win.resize(1200, 720)
    win.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
