#!/usr/bin/env python3
"""
Hand-ONLY record + replay for the Inspire hand. Ways to record:

  --mode press   (default) : TOUCH a finger and it gets recorded — even a SLIGHT touch.
                             Detection is on Δbase (force change from rest, threshold
                             ~12g vs a ~5g noise floor); any detected touch closes the
                             finger at least --min-close (35%) and HOLDS it --hold (0.8s)
                             so a quick tap is clearly visible on replay. Touch harder =
                             closes more (fully closed at --span, 45g). The fingers stay
                             OPEN while recording and only close on REPLAY.
  --mode levels            : each press steps OPEN → SLIGHT → HALF → FULL → OPEN.
  --mode release           : fingers RELEASED (loose) — pose by hand, records positions.
                             NOTE: the encoder does not see hand-posing on this hand, so
                             this usually records nothing — prefer press mode.
  --mode capture           : pose by hand and tap SPACE to snapshot each pose.

  conda activate g1_env
  cd ~/Robotics_workspace/yslootahtech/G1/G1_Lootah/Manual_Recorder
  python3 hand_record.py enp3s0 --output grip --seconds 20    # touch fingers -> recorded
  python3 hand_record.py enp3s0 --replay grip                 # replay a saved file
  # more sensitive still:  --thresh 8 --span 30 --min-close 0.5 --hold 1.2

Keys during recording: o = re-open all · Ctrl+C = end early.
Needs inspire_g1 running (Docker container). Files go in DataHand/.
q: 1.00 = open, 0.00 = fully closed.  Idx 0-5 = right hand, 6-11 = left.
"""
import sys, time, json, argparse
from pathlib import Path
import termios, tty, select
from unitree_sdk2py.core.channel import ChannelPublisher, ChannelSubscriber, ChannelFactoryInitialize
from unitree_sdk2py.idl.unitree_go.msg.dds_ import MotorCmds_, MotorStates_
from unitree_sdk2py.idl.default import unitree_go_msg_dds__MotorCmd_

HAND_MOTORS = 12
REC_HZ = 30.0
GRIP_FORCE = 500.0
DATA_DIR = Path("DataHand")
NAMES = ["R.pinky", "R.ring", "R.mid", "R.index", "R.thumbB", "R.thumbR",
         "L.pinky", "L.ring", "L.mid", "L.index", "L.thumbB", "L.thumbR"]
CLR = "\x1b[K"
# Discrete close levels: each press of a finger steps to the next one.
# (name, commanded position) — 1.00 = open, 0.00 = fully closed.
LEVELS = [("OPEN", 1.00), ("SLIGHT", 0.75), ("HALF", 0.50), ("FULL", 0.05)]


class KeyPoller:
    def __enter__(self):
        self.fd = sys.stdin.fileno()
        self.old = termios.tcgetattr(self.fd)
        tty.setcbreak(self.fd)
        return self
    def __exit__(self, *a):
        termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old)
    def poll(self):
        if select.select([sys.stdin], [], [], 0)[0]:
            return sys.stdin.read(1)
        return None


def bar(v, w=16):
    v = 0.0 if v < 0 else (1.0 if v > 1 else v)
    n = int(round(v * w))
    return "█" * n + "·" * (w - n)


class HandRecorder:
    def __init__(self, iface):
        ChannelFactoryInitialize(0, iface)
        self.st = {"q": [1.0] * 12, "f": [0.0] * 12, "n": 0}
        ChannelSubscriber("rt/inspire/state", MotorStates_).Init(self._on_state, 10)
        self.pub = ChannelPublisher("rt/inspire/cmd", MotorCmds_)
        self.pub.Init()
        self.msg = MotorCmds_()
        self.msg.cmds = [unitree_go_msg_dds__MotorCmd_() for _ in range(12)]
        time.sleep(0.5)

    def _on_state(self, m):
        try:
            self.st["q"] = [float(m.states[i].q) for i in range(12)]
            self.st["f"] = [float(m.states[i].tau_est) for i in range(12)]
            self.st["n"] += 1
        except Exception:
            pass

    def send(self, q, force=GRIP_FORCE):
        for i in range(12):
            self.msg.cmds[i].q = float(q[i])
            self.msg.cmds[i].kp = float(force)
        self.pub.Write(self.msg)

    def release(self):
        self.send([-1.0] * 12, force=0.0)     # motor OFF -> fingers loose

    def open_hand(self, n=25):
        for _ in range(n):
            self.send([1.0] * 12)
            time.sleep(0.02)

    # ---------- mode: release (pose by hand) ----------
    def record_release(self, seconds):
        print("🖐️  Opening hand, then releasing so you can pose it by hand...")
        self.open_hand()
        for _ in range(10):
            self.release(); time.sleep(0.02)
        print("\x1b[2J", end="")
        frames, dt, t0 = [], 1.0 / REC_HZ, time.time()
        try:
            with KeyPoller() as kp:
                while time.time() - t0 < seconds:
                    loop = time.time()
                    k = kp.poll()
                    if k and k.lower() == "o":
                        self.open_hand(10)
                    self.release()
                    frames.append({"t": round(time.time() - t0, 4), "h": list(self.st["q"])})
                    self._draw("REC (pose by hand)", self.st["q"])
                    time.sleep(max(0, dt - (time.time() - loop)))
        except KeyboardInterrupt:
            pass
        print(f"\n\n✅ recorded {len(frames)} frames ({frames[-1]['t'] if frames else 0:.1f}s).")
        return frames

    # ---------- mode: capture (snapshot poses by hand) ----------
    def record_capture(self, hold_sec):
        print("🖐️  Opening + releasing. Pose the fingers by hand, tap SPACE to CAPTURE each")
        print("    pose you like, Ctrl+C when done. Replay walks through the captured poses.")
        self.open_hand()
        for _ in range(10):
            self.release(); time.sleep(0.02)
        keyframes, flash = [], 0
        print("\x1b[2J", end="")
        try:
            with KeyPoller() as kp:
                while True:
                    loop = time.time()
                    k = kp.poll()
                    if k == " ":
                        keyframes.append(list(self.st["q"]))
                        flash = 8
                    elif k and k.lower() == "o":
                        self.open_hand(10)
                    self.release()
                    tag = f"CAPTURE — {len(keyframes)} pose(s) saved" + ("   ✓ CAPTURED!" if flash else "")
                    self._draw(tag + "   (SPACE=capture · o=open · Ctrl+C=done)", self.st["q"])
                    flash = max(0, flash - 1)
                    time.sleep(max(0, 1.0 / REC_HZ - (time.time() - loop)))
        except KeyboardInterrupt:
            pass
        print(f"\n\n✅ captured {len(keyframes)} pose(s).")
        # Build a time-series: hold each captured pose for hold_sec (motor ramps between).
        frames, t, dt, n = [], 0.0, 1.0 / REC_HZ, int(hold_sec * REC_HZ)
        for kf in keyframes:
            for _ in range(n):
                frames.append({"t": round(t, 4), "h": list(kf)})
                t += dt
        return frames

    # ---------- mode: press (proportional to Δbase, recorded + replayed) ----------
    def record_press(self, seconds, thresh, span, signed, min_close=0.35, hold=0.8, auto_scale=True):
        """Continuously monitor each finger's Δbase (force change from rest) and map it
        to how far that finger closes. Records BOTH the raw Δbase track and the mapped
        positions; replay drives the fingers through the mapped positions."""
        print("🤛 Opening hand + calibrating Δbase — DON'T touch (2s)...")
        self.open_hand()
        acc = [0.0] * 12; fmin = [1e9] * 12; fmax = [-1e9] * 12; nc = 0
        t0 = time.time()
        while time.time() - t0 < 2.0:
            self.send([1.0] * 12)
            if any(abs(v) > 1e-6 for v in self.st["f"]):
                for i in range(12):
                    acc[i] += self.st["f"][i]
                    fmin[i] = min(fmin[i], self.st["f"][i]); fmax[i] = max(fmax[i], self.st["f"][i])
                nc += 1
            time.sleep(0.05)
        base = [acc[i] / nc if nc else 0.0 for i in range(12)]
        thr = [max(thresh, (fmax[i] - fmin[i]) * 1.5 + 5) if nc else thresh for i in range(12)]
        if nc:
            print("   thresholds R:", [round(thr[i]) for i in range(6)],
                  " L:", [round(thr[i]) for i in range(6, 12)])
        else:
            print("   ⚠️  no force data — inspire_g1 up? adapter connected?")

        peak = [0.0] * 12
        latch = [0.0] * 12          # held closeness per finger
        latch_until = [0.0] * 12    # hold the latch until this time
        touches = [0] * 12          # how many touches were captured per finger
        touching = [False] * 12
        print("\x1b[2J", end="")
        frames, dt, t0 = [], 1.0 / REC_HZ, time.time()
        try:
            with KeyPoller() as kp:
                while time.time() - t0 < seconds:
                    loop = time.time()
                    kp.poll()
                    now = time.time()
                    devs, vals = [0.0] * 12, [1.0] * 12
                    for i in range(12):
                        d = self.st["f"][i] - base[i]
                        devs[i] = d
                        peak[i] = max(peak[i], abs(d))
                        # sign: negative = pushed toward closing. --signed uses that
                        # direction; default uses magnitude (either sign = a press).
                        amt = (-d if signed else abs(d))
                        closeness = (amt - thr[i]) / max(1.0, span - thr[i])
                        closeness = min(1.0, max(0.0, closeness))
                        if amt > thr[i]:
                            # ANY detected touch closes at least min_close, so even the
                            # slightest tap is clearly visible on replay.
                            closeness = max(closeness, min_close)
                            if not touching[i]:
                                touches[i] += 1
                                touching[i] = True
                            latch[i] = max(latch[i], closeness)
                            latch_until[i] = now + hold      # keep holding while touched
                        elif amt < thr[i] * 0.5:
                            touching[i] = False
                        # latch: hold the closure briefly after the touch ends, then release
                        if now < latch_until[i]:
                            closeness = max(closeness, latch[i])
                        else:
                            latch[i] = 0.0
                        vals[i] = 1.0 - closeness          # 1.0 open .. 0.0 closed
                    self.send([1.0] * 12)                  # finger stays OPEN while recording
                    frames.append({"t": round(time.time() - t0, 4),
                                   "h": list(vals), "d": [round(x, 1) for x in devs]})
                    self._draw_press(f"REC (Δbase → close) {seconds-(time.time()-t0):4.1f}s left",
                                     devs, vals, thr, peak)
                    time.sleep(max(0, dt - (time.time() - loop)))
        except KeyboardInterrupt:
            pass
        print(f"\n\n✅ recorded {len(frames)} frames ({frames[-1]['t'] if frames else 0:.1f}s).")
        # Re-derive positions from the Δbase track so the DEGREE of each touch survives.
        frames, pk = self._remap(frames, thr, min_close, hold, auto_scale, span)
        tot = sum(touches)
        print(f"   {tot} touch(es) captured"
              + (" — each finger scaled to its own hardest press:" if auto_scale else ":")
              if tot else
              "   ⚠️  no touches captured — lower --thresh (try 8) and touch the fingertip pad.")
        for i in range(12):
            if touches[i] or peak[i] > thr[i] * 0.5:
                closes = [1.0 - f["h"][i] for f in frames]
                mx = max(closes) if closes else 0.0
                if touches[i]:
                    print(f"     {NAMES[i]:9s} peakΔ {pk[i]:5.0f}g  {touches[i]} touch(es)"
                          f"  → closes up to {mx*100:3.0f}% on replay")
                else:
                    print(f"     {NAMES[i]:9s} peakΔ {pk[i]:5.0f}g  (below thr {thr[i]:.0f}, not captured)")
        return frames

    def _remap(self, frames, thr, min_close, hold, auto_scale, span):
        """Re-derive the replay positions from the recorded Δbase track so the DEGREE of
        each touch is preserved. With auto_scale, every finger is normalised to its OWN
        hardest press (that press = fully closed, lighter ones proportionally less), which
        matters because a thumb reaches ~190g while an index may only reach ~50g."""
        if not frames:
            return frames, [0.0] * 12
        pk = [0.0] * 12
        for f in frames:
            for i in range(12):
                pk[i] = max(pk[i], abs(f["d"][i]))
        for f in frames:
            for i in range(12):
                amt = abs(f["d"][i])
                if amt <= thr[i]:
                    c = 0.0
                else:
                    top = pk[i] if (auto_scale and pk[i] > thr[i] + 5) else span
                    top = max(top, thr[i] + 5)
                    frac = min(1.0, (amt - thr[i]) / (top - thr[i]))
                    # threshold touch -> min_close, hardest touch -> fully closed
                    c = min_close + (1.0 - min_close) * frac
                f["h"][i] = 1.0 - c
        # latch: hold each touch's closure briefly so a quick tap stays visible
        w = int(hold * REC_HZ)
        for i in range(12):
            cl = [1.0 - f["h"][i] for f in frames]
            run, cnt = 0.0, 0
            for k in range(len(frames)):
                if cl[k] > 0:
                    run, cnt = max(run, cl[k]), w
                elif cnt > 0:
                    cnt -= 1
                else:
                    run = 0.0
                frames[k]["h"][i] = 1.0 - max(cl[k], run if cnt > 0 else 0.0)
        return frames, pk

    def _draw_press(self, tag, devs, vals, thr, peak):
        out = ["\x1b[H", f"HAND RECORD — {tag}   msgs={self.st['n']:>6d}{CLR}",
               "  press a finger: the harder you press, the more it will close on replay" + CLR,
               f"  {'joint':9s} {'Δbase':>7s} {'peakΔ':>6s} {'thr':>4s}  close-on-replay{CLR}", CLR]
        for i, nm in enumerate(NAMES):
            if i == 6:
                out.append(CLR)
            hit = "◄" if abs(devs[i]) > thr[i] else " "
            out.append(f"  {nm:9s} {devs[i]:+7.0f} {peak[i]:6.0f} {thr[i]:4.0f} {hit} "
                       f"[{bar(1.0 - vals[i])}] {(1.0-vals[i])*100:3.0f}%{CLR}")
        print("\n".join(out), end="", flush=True)

    # ---------- mode: force (press joints, based on Δbase) ----------
    def record_force(self, seconds, thresh, close_rate):
        print("🤛 Opening hand + calibrating Δbase — DON'T touch (2s)...")
        self.open_hand()
        acc = [0.0] * 12; fmin = [1e9] * 12; fmax = [-1e9] * 12; nc = 0
        t0 = time.time()
        while time.time() - t0 < 2.0:
            self.send([1.0] * 12)
            if any(abs(v) > 1e-6 for v in self.st["f"]):
                for i in range(12):
                    acc[i] += self.st["f"][i]
                    fmin[i] = min(fmin[i], self.st["f"][i]); fmax[i] = max(fmax[i], self.st["f"][i])
                nc += 1
            time.sleep(0.05)
        base = [acc[i] / nc if nc else 0.0 for i in range(12)]
        thr = [max(thresh, (fmax[i] - fmin[i]) * 1.5 + 10) if nc else thresh for i in range(12)]
        lvl = [0] * 12                       # level index per finger
        pushed = [False] * 12
        flash = [0] * 12
        print("\x1b[2J", end="")
        frames, dt, t0 = [], 1.0 / REC_HZ, time.time()
        try:
            with KeyPoller() as kp:
                while time.time() - t0 < seconds:
                    loop = time.time()
                    k = kp.poll()
                    if k:
                        kl = k.lower()
                        if kl == "o": lvl = [0] * 12
                        elif kl == "c": lvl = [3] * 12
                        elif kl == "[":
                            for i in range(6): lvl[i] = 0
                        elif kl == "]":
                            for i in range(6): lvl[i] = 3
                        elif kl == ";":
                            for i in range(6, 12): lvl[i] = 0
                        elif kl == "'":
                            for i in range(6, 12): lvl[i] = 3
                    for i in range(12):
                        dev = self.st["f"][i] - base[i]
                        if abs(dev) > thr[i]:                     # press detected via Δbase
                            if not pushed[i]:                     # rising edge -> step one level
                                lvl[i] = (lvl[i] + 1) % len(LEVELS)
                                pushed[i] = True
                                flash[i] = 12
                        elif abs(dev) < thr[i] * 0.5:             # released (hysteresis)
                            pushed[i] = False
                    self.send([1.0] * 12)                         # finger stays OPEN while recording
                    vals = [LEVELS[lvl[i]][1] for i in range(12)]
                    frames.append({"t": round(time.time() - t0, 4), "h": list(vals)})
                    self._draw_levels(f"REC (press a finger = next level) {seconds-(time.time()-t0):4.1f}s",
                                      lvl, flash)
                    for i in range(12):
                        flash[i] = max(0, flash[i] - 1)
                    time.sleep(max(0, dt - (time.time() - loop)))
        except KeyboardInterrupt:
            pass
        print(f"\n\n✅ recorded {len(frames)} frames ({frames[-1]['t'] if frames else 0:.1f}s). "
              f"Fingers stayed open; they close to these levels on replay:")
        for i in range(12):
            if lvl[i]:
                print(f"    {NAMES[i]:9s} {LEVELS[lvl[i]][0]}")
        return frames

    def _draw_levels(self, tag, lvl, flash):
        out = ["\x1b[H", f"HAND RECORD — {tag}   msgs={self.st['n']:>6d}{CLR}",
               "  press a finger → OPEN → SLIGHT → HALF → FULL → OPEN   (no need to close it!)" + CLR,
               "  keys: o/c all open/full · [ ] right · ; ' left · Ctrl+C=end" + CLR, CLR]
        for i, nm in enumerate(NAMES):
            if i == 6:
                out.append(CLR)
            name, val = LEVELS[lvl[i]]
            mark = " ← set" if flash[i] else ""
            out.append(f"  {nm:9s} [{bar(val)}] {name:6s}{mark}{CLR}")
        print("\n".join(out), end="", flush=True)

    def _draw(self, tag, vals, label="position"):
        out = ["\x1b[H", f"HAND RECORD — {tag}   msgs={self.st['n']:>6d}   (o=open all · Ctrl+C=end){CLR}",
               f"  {label}: 1.00 open ░ 0.00 closed" + CLR, CLR]
        for i, nm in enumerate(NAMES):
            if i == 6:
                out.append(CLR)
            out.append(f"  {nm:9s} [{bar(vals[i])}] {vals[i]:4.2f}{CLR}")
        print("\n".join(out), end="", flush=True)

    def replay(self, frames, speed=1.0):
        if not frames:
            print("   (nothing to replay)")
            return
        print("▶️  replaying — the motor drives the fingers through the recording...")
        first = frames[0]["h"]
        cur = list(self.st["q"])
        for k in range(20):
            a = k / 20.0
            self.send([(1 - a) * cur[i] + a * first[i] for i in range(12)])
            time.sleep(1.0 / REC_HZ)
        print("\x1b[2J", end="")
        t0, i = time.time(), 0
        try:
            while True:
                tgt = (time.time() - t0) * speed
                while i < len(frames) - 1 and frames[i]["t"] < tgt:
                    i += 1
                self.send(frames[i]["h"])
                self._draw("REPLAY", frames[i]["h"])
                if i >= len(frames) - 1 and frames[-1]["t"] < tgt:
                    break
                time.sleep(1.0 / REC_HZ)
            print("\n\n✅ replay done.")
        except KeyboardInterrupt:
            print("\n\n⛔ replay stopped.")


def save(frames, name):
    DATA_DIR.mkdir(exist_ok=True)
    p = DATA_DIR / (name if name.endswith(".jsonl") else name + ".jsonl")
    with open(p, "w") as f:
        f.write(json.dumps({"meta": {"hz": REC_HZ, "hand_motors": HAND_MOTORS}}) + "\n")
        for fr in frames:
            f.write(json.dumps(fr) + "\n")
    print(f"💾 saved {len(frames)} frames to {p}")


def load(name):
    p = DATA_DIR / (name if name.endswith(".jsonl") else name + ".jsonl")
    frames = []
    with open(p) as f:
        f.readline()
        for line in f:
            d = json.loads(line)
            if "h" in d:
                frames.append(d)
    print(f"loaded {len(frames)} frames from {p}")
    return frames


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("iface", nargs="?", default="enp3s0")
    ap.add_argument("--output", default="hand_rec")
    ap.add_argument("--seconds", type=float, default=20.0)
    ap.add_argument("--mode", choices=["press", "levels", "release", "capture", "force"], default="press",
                    help="press=Δbase proportional — harder press closes the finger more, recorded "
                         "continuously and replayed (default); levels=press steps OPEN/SLIGHT/HALF/FULL; "
                         "release=pose by hand; capture=snapshot poses (SPACE); force=alias of levels")
    ap.add_argument("--span", type=float, default=45.0,
                    help="press mode: Δbase (g) mapping to a FULLY closed finger; lower = lighter touch closes more (default 45)")
    ap.add_argument("--min-close", type=float, default=0.35,
                    help="press mode: ANY detected touch closes at least this much, 0-1 (default 0.35)")
    ap.add_argument("--hold", type=float, default=0.8,
                    help="press mode: seconds to hold a touch's closure so a quick tap is clearly replayed (default 0.8)")
    ap.add_argument("--no-auto-scale", action="store_true",
                    help="press mode: use the fixed --span instead of scaling each finger to its own hardest press")
    ap.add_argument("--signed", action="store_true",
                    help="press mode: use force DIRECTION (negative Δbase = closing) instead of magnitude")
    ap.add_argument("--pose-hold", type=float, default=1.5, help="capture mode: seconds to hold each pose on replay")
    ap.add_argument("--replay", default=None, help="replay a saved hand file (name in DataHand/) and exit")
    ap.add_argument("--speed", type=float, default=1.0)
    ap.add_argument("--thresh", type=float, default=12.0,
                    help="Δbase press threshold in grams. Noise floor is ~5g, so 12 catches a SLIGHT touch (default 12)")
    ap.add_argument("--close-rate", type=float, default=1.2, help="force mode: degree ramp speed (per s)")
    a = ap.parse_args()

    rec = HandRecorder(a.iface)
    if a.replay:
        rec.replay(load(a.replay), a.speed)
        return

    if a.mode == "press":
        frames = rec.record_press(a.seconds, a.thresh, a.span, a.signed, a.min_close, a.hold,
                                  not a.no_auto_scale)
    elif a.mode in ("levels", "force"):
        frames = rec.record_force(a.seconds, a.thresh, a.close_rate)
    elif a.mode == "capture":
        frames = rec.record_capture(a.pose_hold)
    else:
        frames = rec.record_release(a.seconds)
    if input("Replay the recording? [y/n]: ").strip().lower() in ("y", "yes", ""):
        rec.replay(frames, a.speed)
    if input("Save recording? [y/n]: ").strip().lower() in ("y", "yes", ""):
        save(frames, a.output)
    print("done.")


if __name__ == "__main__":
    main()
