#!/usr/bin/env python3
"""
Minimal serial plotter for the HeaveOnly sketch.

Expects CSV lines at 2_000_000 baud:  Front,Back,Avg,ActiveRef,PWM,outputOn
Key commands (focus the plot window):
  0 → output off
  1 → output on, PID
  2 → output on, full attract
  q → quit

Text boxes at the bottom of the window send:
  Ref (mm)           → R<value>
  PID (kp,ki,kd)     → P<kp>,<ki>,<kd>
"""

import argparse
import sys
import time
from collections import deque

import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.widgets import TextBox, CheckButtons
import serial
import serial.tools.list_ports

BAUD_RATE  = 2_000_000
MAX_POINTS = 400        # rolling window length
TIMEOUT_S  = 0.02


def pick_port():
    ports = list(serial.tools.list_ports.comports())
    if not ports:
        sys.exit('No serial ports found.')
    for p in ports:
        d = (p.description or '').lower()
        if 'usbmodem' in p.device or 'arduino' in d or 'usbserial' in p.device.lower():
            return p.device
    return ports[0].device


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--port', default=None)
    ap.add_argument('--window', type=int, default=MAX_POINTS)
    args = ap.parse_args()

    port = args.port or pick_port()
    print(f'Opening {port} @ {BAUD_RATE} baud')
    ser = serial.Serial(port, BAUD_RATE, timeout=TIMEOUT_S)
    time.sleep(2.0)          # let the Arduino reset
    ser.reset_input_buffer()

    t0 = time.time()
    N  = args.window
    t_buf    = deque(maxlen=N)
    front    = deque(maxlen=N)
    back     = deque(maxlen=N)
    avg      = deque(maxlen=N)
    aref     = deque(maxlen=N)
    pwm      = deque(maxlen=N)
    on_buf   = deque(maxlen=N)

    fig, (ax_mm, ax_pwm) = plt.subplots(2, 1, sharex=True, figsize=(10, 6))
    fig.subplots_adjust(bottom=0.18)
    l_front, = ax_mm.plot([], [], label='Front', color='tab:blue')
    l_back,  = ax_mm.plot([], [], label='Back',  color='tab:orange')
    l_avg,   = ax_mm.plot([], [], label='Avg',   color='k', lw=2)
    l_aref,  = ax_mm.plot([], [], label='ActiveRef', color='tab:green',
                          lw=1.2, ls='--')
    ax_mm.set_ylabel('Gap (mm)')
    ax_mm.grid(True, alpha=0.3)
    ax_mm.legend(loc='upper right')

    l_pwm, = ax_pwm.plot([], [], label='PWM', color='tab:red')
    ax_pwm.axhline(0, color='gray', lw=0.5)
    ax_pwm.set_ylabel('PWM')
    ax_pwm.set_xlabel('Time (s)')
    ax_pwm.set_ylim(-260, 260)
    ax_pwm.grid(True, alpha=0.3)

    mode_txt = fig.text(0.01, 0.97, 'mode: ?', fontsize=10,
                        family='monospace', va='top')
    fig.suptitle('HeaveOnly — keys: 0=off  1=PID  2=attract  q=quit')

    def send_mode(cmd_char):
        ser.write((cmd_char + '\n').encode())
        mode_txt.set_text({'0': 'mode: OFF',
                           '1': 'mode: PID',
                           '2': 'mode: ATTRACT'}[cmd_char])

    def on_key(event):
        # Suppress mode keys while either TextBox is actively editing,
        # so typing digits into Ref/PID fields doesn't ping-pong the coils.
        if getattr(tb_ref, 'capturekeystrokes', False) or \
           getattr(tb_pid, 'capturekeystrokes', False):
            return
        if event.key in ('0', '1', '2'):
            send_mode(event.key)
        elif event.key == 'q':
            plt.close(fig)
    fig.canvas.mpl_connect('key_press_event', on_key)

    ax_ref = fig.add_axes([0.10, 0.04, 0.15, 0.05])
    tb_ref = TextBox(ax_ref, 'Ref (mm) ', initial='12')
    ax_pid = fig.add_axes([0.50, 0.04, 0.30, 0.05])
    tb_pid = TextBox(ax_pid, 'PID (kp,ki,kd) ', initial='400,0,300')
    ax_ff = fig.add_axes([0.88, 0.03, 0.08, 0.07])
    cb_ff = CheckButtons(ax_ff, ['FF'], [False])

    def on_ref(text):
        try:
            float(text)
        except ValueError:
            return
        ser.write(f'R{text}\n'.encode())

    def on_pid(text):
        parts = [p.strip() for p in text.split(',')]
        if len(parts) != 3:
            return
        try:
            [float(p) for p in parts]
        except ValueError:
            return
        ser.write(f'P{",".join(parts)}\n'.encode())

    def on_ff(_label):
        ser.write(b'F1\n' if cb_ff.get_status()[0] else b'F0\n')

    tb_ref.on_submit(on_ref)
    tb_pid.on_submit(on_pid)
    cb_ff.on_clicked(on_ff)

    def poll_serial():
        # Drain everything in the OS buffer each frame
        while ser.in_waiting:
            raw = ser.readline()
            try:
                parts = raw.decode('ascii', 'ignore').strip().split(',')
                if len(parts) != 6:
                    continue
                f, b, a, r = (float(parts[0]), float(parts[1]),
                              float(parts[2]), float(parts[3]))
                p = int(parts[4])
                on = int(parts[5])
            except ValueError:
                continue
            t_buf.append(time.time() - t0)
            front.append(f); back.append(b); avg.append(a); aref.append(r)
            pwm.append(p);   on_buf.append(on)

    def update(_frame):
        poll_serial()
        if not t_buf:
            return l_front, l_back, l_avg, l_aref, l_pwm, mode_txt
        xs = list(t_buf)
        l_front.set_data(xs, list(front))
        l_back .set_data(xs, list(back))
        l_avg  .set_data(xs, list(avg))
        l_aref .set_data(xs, list(aref))
        l_pwm  .set_data(xs, list(pwm))
        ax_mm.relim();  ax_mm.autoscale_view(scalex=True, scaley=True)
        ax_pwm.set_xlim(xs[0], max(xs[-1], xs[0] + 1e-3))
        return l_front, l_back, l_avg, l_aref, l_pwm, mode_txt

    ani = FuncAnimation(fig, update, interval=50, blit=False,
                        cache_frame_data=False)
    try:
        plt.show()
    finally:
        try:
            ser.write(b'0\n')   # safety: turn output off on exit
            ser.close()
        except Exception:
            pass


if __name__ == '__main__':
    main()
