#!/usr/bin/env python3
"""
G1 REPLAY TRIGGER (R2 + X) - SINGLE FILE
---------------------------------------
- Hold R2 + X to replay DataG1/photo_G3.jsonl (default).
- While playing: ignore any additional R2+X until replay finishes and returns home.
- Cancel combo: R2 + L1 cancels replay immediately and returns arms to home.

Usage:
  python3 g1_replay_trigger_r2x.py enp3s0
  python3 g1_replay_trigger_r2x.py enp3s0 --input photo_G3.jsonl --home arm_home.jsonl --speed 1.0
"""

import time
import sys
import json
import argparse
import struct
from pathlib import Path

from unitree_sdk2py.core.channel import ChannelPublisher, ChannelSubscriber, ChannelFactoryInitialize
from unitree_sdk2py.idl.default import unitree_hg_msg_dds__LowCmd_
from unitree_sdk2py.idl.default import unitree_hg_msg_dds__LowState_
from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowCmd_
from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowState_
from unitree_sdk2py.utils.crc import CRC

from Logger import Logs

# ---------------- Logger (exactly as requested) ----------------
controller_logs = Logs()
controller_logs.LogEngine("G1_Logs", "g1_replay_trigger_r2x.log")

# ---------------- Constants ----------------
G1_NUM_MOTOR = 29
ENABLE_ARM_SDK_INDEX = 29

DATA_DIR = Path("DataG1")
REPLAY_HZ = 60.0

# --- GAINS (same as your replay) ---
KP_HIGH = 300.0  # Core/Legs
KD_HIGH = 3.0
KP_LOW  = 80.0   # Arms/Ankles
KD_LOW  = 3.0
KP_WRIST = 40.0
KD_WRIST = 1.5

WEAK_MOTORS  = [4, 10, 15, 16, 17, 18, 22, 23, 24, 25]
WRIST_MOTORS = [19, 20, 21, 26, 27, 28]

# ---------------- Helpers ----------------
def resolve_input_path(in_path: str) -> str:
    p = Path(in_path)
    if len(p.parts) == 1:
        return str(DATA_DIR / p.name)
    return str(p)

def load_home_pose(home_path: str):
    """Reads the last frame of arm_home.jsonl to get the target pose."""
    path = resolve_input_path(home_path)
    try:
        last_valid_q = None
        with open(path, 'r') as f:
            for line in f:
                d = json.loads(line)
                if 'q' in d and len(d['q']) == G1_NUM_MOTOR:
                    last_valid_q = d['q']
        if last_valid_q:
            controller_logs.print_and_log(f"✅ Loaded Home Pose from {path}", "info")
            return last_valid_q
        else:
            controller_logs.print_and_log(f"⚠️  Warning: {path} found but contained no valid 'q' data.", "warning")
    except FileNotFoundError:
        controller_logs.print_and_log(f"⚠️  Warning: Home file {path} not found.", "warning")

    controller_logs.print_and_log("⚠️  Using Default Home (Arms at 0.0)", "warning")
    return [0.0] * G1_NUM_MOTOR

# ---------------- Wireless Controller Parser (embedded) ----------------
class unitreeRemoteController:
    def __init__(self):
        self.Lx = 0; self.Rx = 0; self.Ry = 0; self.Ly = 0
        self.L1 = 0; self.L2 = 0; self.R1 = 0; self.R2 = 0
        self.A = 0; self.B = 0; self.X = 0; self.Y = 0
        self.Up = 0; self.Down = 0; self.Left = 0; self.Right = 0
        self.Select = 0; self.F1 = 0; self.F3 = 0; self.Start = 0

    def parse_botton(self, data1, data2):
        self.R1 = (data1 >> 0) & 1; self.L1 = (data1 >> 1) & 1
        self.Start = (data1 >> 2) & 1; self.Select = (data1 >> 3) & 1
        self.R2 = (data1 >> 4) & 1; self.L2 = (data1 >> 5) & 1
        self.F1 = (data1 >> 6) & 1; self.F3 = (data1 >> 7) & 1
        self.A = (data2 >> 0) & 1; self.B = (data2 >> 1) & 1
        self.X = (data2 >> 2) & 1; self.Y = (data2 >> 3) & 1
        self.Up = (data2 >> 4) & 1; self.Right = (data2 >> 5) & 1
        self.Down = (data2 >> 6) & 1; self.Left = (data2 >> 7) & 1

    def parse_key(self, data):
        offsets = [4, 8, 12, 20]  # Lx, Rx, Ry, Ly
        self.Lx, self.Rx, self.Ry, self.Ly = [struct.unpack('<f', data[o:o+4])[0] for o in offsets]

    def parse(self, remoteData):
        self.parse_key(remoteData)
        self.parse_botton(remoteData[2], remoteData[3])

    def get_state(self):
        return self.__dict__.copy()

# ---------------- Replay Engine ----------------
class ReplayWithHome:
    def __init__(self, watchdog_timeout=0.25, watchdog_disable_after=1.0):
        self.low_state = None
        self.low_cmd = unitree_hg_msg_dds__LowCmd_()
        self.crc = CRC()

        self.arm_pub = ChannelPublisher("rt/arm_sdk", LowCmd_)
        self.arm_pub.Init()

        self.state_sub = None
        self.first_state = False

        # watchdog
        self.last_state_time = 0.0
        self.watchdog_timeout = float(watchdog_timeout)
        self.watchdog_disable_after = float(watchdog_disable_after)

        # controller
        self.remote = unitreeRemoteController()
        self.controller_state = self.remote.get_state()

        self.is_playing = False

    def InitStateSubscriber(self, topic: str):
        self.state_sub = ChannelSubscriber(topic, LowState_)
        self.state_sub.Init(self.LowStateHandler, 10)

    def LowStateHandler(self, msg: LowState_):
        self.low_state = msg
        self.first_state = True
        self.last_state_time = time.time()
        try:
            self.remote.parse(msg.wireless_remote)
            self.controller_state = self.remote.get_state()
        except Exception:
            pass

    def StateFresh(self) -> bool:
        return (time.time() - self.last_state_time) < self.watchdog_timeout

    def CancelRequested(self) -> bool:
        """Cancel combo: R2 + L1"""
        s = self.controller_state
        return bool(s.get("R2", 0) and s.get("L1", 0))

    def SendFrame(self, arm_target_q, body_lock_q):
        self.low_cmd.motor_cmd[ENABLE_ARM_SDK_INDEX].q = 1.0

        for i in range(G1_NUM_MOTOR):
            self.low_cmd.motor_cmd[i].mode = 1
            self.low_cmd.motor_cmd[i].dq = 0
            self.low_cmd.motor_cmd[i].tau = 0

            if i >= 15:
                self.low_cmd.motor_cmd[i].q = arm_target_q[i]
            else:
                self.low_cmd.motor_cmd[i].q = body_lock_q[i]

            if i in WEAK_MOTORS:
                self.low_cmd.motor_cmd[i].kp = KP_LOW
                self.low_cmd.motor_cmd[i].kd = KD_LOW
            elif i in WRIST_MOTORS:
                self.low_cmd.motor_cmd[i].kp = KP_WRIST
                self.low_cmd.motor_cmd[i].kd = KD_WRIST
            else:
                self.low_cmd.motor_cmd[i].kp = KP_HIGH
                self.low_cmd.motor_cmd[i].kd = KD_HIGH

        self.low_cmd.crc = self.crc.Crc(self.low_cmd)
        self.arm_pub.Write(self.low_cmd)

    def DisableSDK(self):
        controller_logs.print_and_log("🔌 Disabling SDK...", "info")
        self.low_cmd.motor_cmd[ENABLE_ARM_SDK_INDEX].q = 0.0
        self.low_cmd.crc = self.crc.Crc(self.low_cmd)
        for _ in range(10):
            self.arm_pub.Write(self.low_cmd)
            time.sleep(0.02)

    def ReturnArmsHome(self, last_arm_q, body_lock_q, home_q, home_steps=180):
        """Always go home smoothly (arms only), body remains locked."""
        controller_logs.print_and_log("🏡 Returning arms to HOME...", "info")
        for k in range(home_steps):
            stale_for = (time.time() - self.last_state_time)
            if stale_for > self.watchdog_disable_after:
                controller_logs.print_and_log("🛑 WATCHDOG: connection lost during home. Disabling SDK.", "error")
                self.DisableSDK()
                return

            if not self.StateFresh():
                controller_logs.print_and_log("⚠️ WATCHDOG: state stale during home. Holding last pose...", "warning")
                self.SendFrame(last_arm_q, body_lock_q)
                time.sleep(1.0 / REPLAY_HZ)
                continue

            alpha = k / home_steps
            interp_q = list(last_arm_q)
            for j in range(15, 29):
                interp_q[j] = (1-alpha)*last_arm_q[j] + alpha*home_q[j]
            self.SendFrame(interp_q, body_lock_q)
            time.sleep(1.0 / REPLAY_HZ)

        controller_logs.print_and_log("✅ Home Reached.", "info")

    def RunReplay(self, filename: str, home_filename: str, speed: float):
        self.is_playing = True
        cancel_requested = False

        try:
            controller_logs.print_and_log(f"🎬 Triggered replay: {filename}", "info")

            controller_logs.print_and_log("Waiting for robot...", "info")
            while not self.first_state:
                time.sleep(0.05)
            controller_logs.print_and_log("✅ Robot Connected!", "info")

            home_q = load_home_pose(home_filename)
            full_body_lock_q = [self.low_state.motor_state[i].q for i in range(G1_NUM_MOTOR)]

            frames = []
            with open(filename, 'r') as f:
                for line in f:
                    d = json.loads(line)
                    if 'q' in d:
                        frames.append(d)

            if not frames:
                controller_logs.print_and_log("❌ No frames found in input file.", "error")
                return

            controller_logs.print_and_log(f"🟢 Ready to play {len(frames)} frames.", "info")
            controller_logs.print_and_log("🔒 Body LOCKED. Arms replay then return HOME.", "info")
            controller_logs.print_and_log("🛑 Cancel combo: Hold R2 + L1", "info")

            # Move to start
            controller_logs.print_and_log("Moving to start...", "info")
            file_start_q = frames[0]['q']
            last_played_q = file_start_q  # will update

            steps = 60
            for k in range(steps):
                if self.CancelRequested():
                    controller_logs.print_and_log("🛑 CANCEL: R2+L1 detected أثناء الانتقال للبداية.", "warning")
                    cancel_requested = True
                    break

                if not self.StateFresh():
                    controller_logs.print_and_log("⚠️ WATCHDOG: stale while moving to start. Holding...", "warning")
                    self.SendFrame(last_played_q, full_body_lock_q)
                    time.sleep(1.0 / REPLAY_HZ)
                    continue

                alpha = k / steps
                interp_q = list(full_body_lock_q)
                for j in range(15, 29):
                    interp_q[j] = (1-alpha)*full_body_lock_q[j] + alpha*file_start_q[j]
                self.SendFrame(interp_q, full_body_lock_q)
                last_played_q = interp_q
                time.sleep(1.0 / REPLAY_HZ)

            # Play replay (unless canceled)
            if not cancel_requested:
                controller_logs.print_and_log("▶️ Playing...", "info")
                play_elapsed = 0.0
                last_real = time.time()

                while True:
                    if self.CancelRequested():
                        controller_logs.print_and_log("🛑 CANCEL: R2+L1 detected أثناء التشغيل.", "warning")
                        cancel_requested = True
                        break

                    stale_for = (time.time() - self.last_state_time)
                    if stale_for > self.watchdog_disable_after:
                        controller_logs.print_and_log("🛑 WATCHDOG: connection lost. Disabling SDK.", "error")
                        self.DisableSDK()
                        return

                    if not self.StateFresh():
                        controller_logs.print_and_log("⚠️ WATCHDOG: timeout! Holding last pose...", "warning")
                        self.SendFrame(last_played_q, full_body_lock_q)
                        time.sleep(1.0 / REPLAY_HZ)
                        continue

                    now_real = time.time()
                    dt_real = now_real - last_real
                    last_real = now_real
                    play_elapsed += dt_real * speed

                    target_frame = None
                    for fr in frames:
                        if fr['t'] - frames[0]['t'] >= play_elapsed:
                            target_frame = fr
                            break
                    if target_frame is None:
                        break

                    self.SendFrame(target_frame['q'], full_body_lock_q)
                    last_played_q = target_frame['q']
                    time.sleep(1.0 / REPLAY_HZ)

            # Always go home (normal finish OR cancel)
            self.ReturnArmsHome(last_played_q, full_body_lock_q, home_q, home_steps=180)
            self.DisableSDK()

        except Exception as e:
            controller_logs.print_and_log(f"❌ Exception in replay: {e}", "error")
            try:
                self.DisableSDK()
            except Exception:
                pass
        finally:
            self.is_playing = False

# ---------------- Trigger Loop (R2 + X) ----------------
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("iface", help="Network interface")
    parser.add_argument("--input", default="photo_G3.jsonl", help="Input recording file (default: photo_G3.jsonl)")
    parser.add_argument("--home", default="arm_home.jsonl", help="Home pose file")
    parser.add_argument("--speed", type=float, default=1.0)
    parser.add_argument("--state_topic", default="rt/lowstate", help="Lowstate topic (default: rt/lowstate)")
    parser.add_argument("--watchdog", type=float, default=0.25, help="Watchdog timeout seconds")
    parser.add_argument("--watchdog_disable_after", type=float, default=1.0, help="Disable SDK if stale longer than this")
    args = parser.parse_args()

    ChannelFactoryInitialize(0, args.iface)

    engine = ReplayWithHome(
        watchdog_timeout=args.watchdog,
        watchdog_disable_after=args.watchdog_disable_after
    )
    engine.InitStateSubscriber(args.state_topic)

    target_path = resolve_input_path(args.input)

    controller_logs.print_and_log("✅ G1 R2+X Trigger Ready", "info")
    controller_logs.print_and_log(f"🎯 Trigger file: {target_path}", "info")
    controller_logs.print_and_log("🎮 Hold R2 + X to replay", "info")
    controller_logs.print_and_log("🛑 Cancel while playing: Hold R2 + L1", "info")

    # Rising-edge trigger + require release before next trigger
    combo_prev = False

    while True:
        time.sleep(0.01)

        if not engine.first_state:
            continue

        s = engine.controller_state

        # Trigger combo: R2 + X
        combo_now = bool(s.get("R2", 0) and s.get("X", 0))

        # ✅ IGNORE while playing (and also prevent retrigger until released)
        if engine.is_playing:
            combo_prev = combo_now
            continue

        # Trigger only on rising edge (must release and press again)
        if combo_now and not combo_prev:
            controller_logs.print_and_log("[TRIGGER] R2 + X detected", "info")
            engine.RunReplay(target_path, args.home, args.speed)
            # After RunReplay returns, if user still holding, combo_prev will get updated next loop
            # and won’t retrigger until they release and press again.

        combo_prev = combo_now

if __name__ == "__main__":
    main()
