# -*- coding: utf-8 -*-
"""
ablation_haptic.py

Quantitative ablation for the haptic rendering module.

Modes:
- baseline   : full method (energy tank + F/V filtering + alpha smoothing + torque LPF)
- no_tank    : energy tank disabled (no passivity enforcement)
- no_filter  : tank enabled but F/V filtering + alpha smoothing + torque LPF disabled

Run:
    python test/ablation_haptic.py
from the project root, with config/config.yaml following the same
structure as demo.py and test_sew.py.
"""

import os
import sys
import time
from typing import Dict

import numpy as np
import pinocchio as pin

import matplotlib.pyplot as plt
from omegaconf import OmegaConf

# Make sure project root is on sys.path
THIS_DIR = os.path.dirname(__file__)
PROJECT_ROOT = os.path.abspath(os.path.join(THIS_DIR, ".."))
if PROJECT_ROOT not in sys.path:
    sys.path.append(PROJECT_ROOT)

from core.sew_mapper import SEWMapper
from core.interaction_estimater import InteractionEstimator
from core.haptic_render import HapticRenderer


# -------------------- Utilities --------------------

def dummy_master_measure(step: int, model: pin.Model) -> np.ndarray:
    """
    Synthetic master joint measurement used for offline ablation.
    Start from neutral and add small sinusoidal motions on the first few joints.
    """
    q = pin.neutral(model)
    n_use = min(model.nq, 6)
    for i in range(n_use):
        q[i] = 0.5 * np.sin(0.01 * step + 0.7 * i)
    return q


def dummy_slave_dynamics(q_cmd: np.ndarray, model: pin.Model) -> np.ndarray:
    """
    Simple slave "execution" model: follow the commanded joint positions
    with a small Gaussian disturbance, to emulate interaction / noise.
    """
    q_cmd = np.asarray(q_cmd, dtype=float).reshape(-1,)
    noise = 0.001 * np.random.randn(model.nq)
    return q_cmd + noise


def load_config():
    """
    Load config/config.yaml in the project root (same style as demo.py / test_sew.py).
    """
    cand = os.path.join(PROJECT_ROOT, "config", "config.yaml")
    if not os.path.exists(cand):
        raise FileNotFoundError(f"config.yaml not found at: {cand}")
    conf = OmegaConf.load(cand)
    print(f"[INFO] Loaded config from: {cand}")
    return conf


def build_modules(conf):
    # Build models from URDF
    master_model = pin.buildModelFromUrdf(str(conf.master_urdf))
    slave_model = pin.buildModelFromUrdf(str(conf.slave_urdf))

    # SEW mapper
    sew = SEWMapper(
        master_model=master_model,
        slave_model=slave_model,
        m_shoulder_frame=conf.m_shoulder_frame,
        m_elbow_frame=conf.m_elbow_frame,
        m_wrist_frame=conf.m_wrist_frame,
        m_ee_frame=conf.m_ee_frame,
        s_shoulder_frame=conf.s_shoulder_frame,
        s_elbow_frame=conf.s_elbow_frame,
        s_wrist_frame=conf.s_wrist_frame,
        s_ee_frame=conf.s_ee_frame,
        slave_joint_names=conf.sew_mapper.slave_joint_names,
        up_dir=np.array(conf.sew_mapper.up_dir, dtype=float),
        eps_clip=float(conf.sew_mapper.eps_clip),
    )

    # Interaction estimator on slave side
    estimator = InteractionEstimator(
        model=slave_model,
        chest_frame_name=conf.s_base_frame,
        ee_frame_name=conf.s_ee_frame,
        lambda_damp=float(conf.interaction_est.lambda_damp),
    )

    return master_model, slave_model, sew, estimator



# -------------------- Haptic renderer variants --------------------

class HapticRendererNoTank(HapticRenderer):
    """
    Variant that completely disables the energy tank:
    - directly maps interaction wrench to master joint torques through CJ_m^T
    - no passivity enforcement
    """

    def _compute_feedback_tau(self,
                              q_m: np.ndarray,
                              qd_m: np.ndarray,
                              CF_int_slave_C: np.ndarray,
                              V_slave_C: np.ndarray | None,
                              dt: float):
        CF = np.asarray(CF_int_slave_C, dtype=float).reshape(6,)
        # Chest Jacobian on master side
        CJ_m = self.CJ_master.chest_jacobian(q_m, qd_m)
        tau_fb_m_raw = CJ_m.T @ CF
        alpha = 1.0
        return tau_fb_m_raw, alpha


def make_renderer(conf, master_model: pin.Model, mode: str) -> HapticRenderer:
    """
    Construct different haptic renderers for ablation.
    """
    common_kwargs = dict(
        master_model=master_model,
        chest_frame_name=conf.m_base_frame,
        ee_frame_name=conf.m_ee_frame,
        feedback_strength=float(conf.haptic_render.feedback_strength),
        E_init=float(conf.haptic_render.E_init),
        E_max=float(conf.haptic_render.E_max),
        alpha_floor=float(conf.haptic_render.alpha_floor),
        alpha_ceil=float(conf.haptic_render.alpha_ceil),
        E0=float(conf.haptic_render.E0),
    )

    if mode == "baseline":
        renderer = HapticRenderer(**common_kwargs)

        # baseline：能量罐 + 强滤波 + 平滑 + 扭矩低通（“管得很严”）
        renderer.tank.tp.force_alpha = 0.05
        renderer.tank.tp.vel_alpha = 0.05
        renderer.tank.tp.alpha_smooth = 0.02
        renderer.tank.tp.power_deadzone = 0.0
        renderer.tau_alpha = 0.05

    elif mode == "no_tank":
        renderer = HapticRendererNoTank(**common_kwargs)

        # no_tank：完全不做能量控制，也不做扭矩低通，暴露最差情况
        renderer.tau_alpha = 1.0

    elif mode == "no_filter":
        renderer = HapticRenderer(**common_kwargs)

        # no_filter：保留能量罐，但不给它任何滤波/平滑能力
        renderer.tank.tp.force_alpha = 1.0
        renderer.tank.tp.vel_alpha = 1.0
        renderer.tank.tp.alpha_smooth = 1.0
        renderer.tank.tp.power_deadzone = 0.0
        renderer.tau_alpha = 1.0

    else:
        raise ValueError(f"Unknown ablation mode: {mode}")

    return renderer


# -------------------- Teleoperation simulation (offline) --------------------

def simulate_teleop(conf,
                    master_model: pin.Model,
                    slave_model: pin.Model,
                    sew: SEWMapper,
                    estimator: InteractionEstimator,
                    renderer: HapticRenderer,
                    Ts: float = 0.002,
                    steps: int = 800) -> Dict[str, np.ndarray]:
    """
    Offline simulation similar to demo.py, but without real-time delays.
    Returns logs for computing quantitative metrics.
    """

    # States
    q_slave = pin.neutral(slave_model)
    dq_slave = np.zeros(slave_model.nv)
    q_slave_prev = q_slave.copy()

    q_m_init = dummy_master_measure(0, master_model)
    pre_q_m = q_m_init.copy()
    pre_qd_m = np.zeros_like(q_m_init)

    # Logs
    log_tau_fb = []
    log_alpha = []
    log_E = []
    log_tau_int_norm = []
    log_qd_m = []
    ee_traj_world = []

    # Harder virtual wall to highlight differences
    d_wall = 0.35
    k_wall = 20000.0

    for k in range(steps):
        # --- master state ---
        q_m = dummy_master_measure(k, master_model)
        if k == 0:
            qd_m = np.zeros_like(q_m)
            qdd_m = np.zeros_like(q_m)
        else:
            qd_m = (q_m - pre_q_m) / Ts
            qdd_m = (qd_m - pre_qd_m) / Ts

        pre_q_m = q_m
        pre_qd_m = qd_m
        log_qd_m.append(qd_m.copy())

        # --- SEW retargetting: master -> desired slave ---
        q_des_slave, _ = sew.retargetting(q_m, q_slave)

        # --- slave motion (with small noise) ---
        q_slave = dummy_slave_dynamics(q_des_slave, slave_model)
        dq_slave = (q_slave - q_slave_prev) / Ts
        q_slave_prev = q_slave.copy()
        qdd_slave = np.zeros(slave_model.nv)

        # --- virtual wall on slave side ---
        CJ_env = estimator._chest_jacobian(q_slave, dq_slave)

        oTC = estimator.data.oMf[estimator.fid_C]   # ^wT_C
        oTEE = estimator.data.oMf[estimator.fid_EE] # ^wT_EE

        R_wc = oTC.rotation
        p_wc = oTC.translation
        R_cw = R_wc.T
        p_cw = -R_cw @ p_wc

        p_we = oTEE.translation
        ee_traj_world.append(p_we.copy())
        p_ce = R_cw @ p_we + p_cw
        x_ce = float(p_ce[0])

        if x_ce > d_wall:
            delta = x_ce - d_wall
            Fx = -k_wall * delta
            CF_env = np.array([Fx, 0, 0, 0, 0, 0], dtype=float)
        else:
            CF_env = np.zeros(6, dtype=float)

        tau_env = CJ_env.T @ CF_env

        # --- interaction estimation on slave ---
        tau_model = estimator._tau_model(q_slave, dq_slave, qdd_slave)
        tau_meas = tau_model + tau_env
        tau_int, CF_int, CJ = estimator.estimate(q_slave, dq_slave, qdd_slave, tau_meas)

        log_tau_int_norm.append(np.linalg.norm(tau_int))

        V_slave_C = CJ @ dq_slave  # 6 x 1

        # --- haptic rendering on master side ---
        tau_cmd_m, tau_fb_m, alpha = renderer.render_tau(
            q_m=q_m,
            qd_m=qd_m,
            qdd_m=qdd_m,
            CF_int_slave_C=CF_int,
            V_slave_C=V_slave_C,
            tau_ff_fric=None,
            dt=Ts,
        )

        log_tau_fb.append(tau_fb_m.copy())
        log_alpha.append(alpha)
        # not all variants really use the tank, but querying E is safe
        log_E.append(getattr(renderer.tank, "E", 0.0))

    # stack
    log_tau_fb = np.vstack(log_tau_fb)
    log_alpha = np.asarray(log_alpha)
    log_E = np.asarray(log_E)
    log_tau_int_norm = np.asarray(log_tau_int_norm)
    log_qd_m = np.vstack(log_qd_m)
    ee_traj_world = np.vstack(ee_traj_world)

    return dict(
        tau_fb=log_tau_fb,
        alpha=log_alpha,
        E=log_E,
        tau_int_norm=log_tau_int_norm,
        qd_m=log_qd_m,
        ee_traj_world=ee_traj_world,
    )


# -------------------- Metric computation --------------------

def compute_metrics(tau_fb: np.ndarray,
                    qd_m: np.ndarray,
                    E: np.ndarray,
                    Ts: float) -> Dict[str, float]:
    """
    Compute quantitative metrics for ablation:
    - rms / peak torque
    - torque spike count / rate
    - positive power injection ratio
    - (optional) energy range if tank is used
    """
    # torque norms
    tau_norm = np.linalg.norm(tau_fb, axis=1)
    rms_tau = float(np.sqrt(np.mean(tau_norm ** 2)))
    peak_tau = float(np.max(tau_norm))

    # torque spikes: ||Δτ||_∞ > threshold
    dtau = np.diff(tau_fb, axis=0)
    dtau_inf = np.max(np.abs(dtau), axis=1)
    spike_th = 5.0  # Nm, can be adjusted to your system scale
    n_spikes = int(np.sum(dtau_inf > spike_th))
    spike_rate = n_spikes / (len(dtau_inf) * Ts)

    # positive power injection ratio: tau_fb^T * qd_m > 0
    P = np.sum(tau_fb * qd_m, axis=1)
    P_inject_ratio = float(np.mean(P > 0.0))

    metrics = dict(
        rms_tau=rms_tau,
        peak_tau=peak_tau,
        n_spikes=n_spikes,
        spike_rate=spike_rate,
        P_inject_ratio=P_inject_ratio,
    )

    if np.any(E != 0.0):
        metrics["E_min"] = float(np.min(E))
        metrics["E_max"] = float(np.max(E))

    return metrics


def plot_ablation_results(all_logs, all_metrics, Ts):

    modes = ["baseline", "no_tank", "no_filter"]
    colors = {
        "baseline": "tab:blue",
        "no_tank": "tab:red",
        "no_filter": "tab:green",
    }

    # -------------------------------------------
    # 1) Torque feedback trajectories (norm vs time)
    # -------------------------------------------
    plt.figure(figsize=(8,4))
    for mode in modes:
        tau_norm = np.linalg.norm(all_logs[mode]["tau_fb"], axis=1)
        t = np.arange(len(tau_norm)) * Ts
        plt.plot(t, tau_norm, label=mode, color=colors[mode])
    plt.xlabel("Time [s]")
    plt.ylabel(r"$\|\tau_{\mathrm{fb}}(t)\|\ \mathrm{[N\cdot m]}$")
    plt.title("Feedback Torque Norm Over Time")
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig("logs_ablation/fig_tau_fb_norm.png", dpi=300)

    # -------------------------------------------
    # 2) Alpha(t) (energy tank scaling)
    # -------------------------------------------
    plt.figure(figsize=(8,4))
    for mode in modes:
        alpha = all_logs[mode]["alpha"]
        t = np.arange(len(alpha)) * Ts
        plt.plot(t, alpha, label=mode, color=colors[mode])
    plt.xlabel("Time [s]")
    plt.ylabel(r"$\alpha(t)$")
    plt.title("Energy Tank Scaling Factor")
    plt.ylim([-0.1, 1.1])
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig("logs_ablation/fig_alpha.png", dpi=300)

    # -------------------------------------------
    # 3) Tank Energy E(t)
    # -------------------------------------------
    plt.figure(figsize=(8,4))
    for mode in modes:
        E = all_logs[mode]["E"]
        t = np.arange(len(E)) * Ts
        plt.plot(t, E, label=mode, color=colors[mode])
    plt.xlabel("Time [s]")
    plt.ylabel(r"$E(t)$")
    plt.title("Energy Tank Level")
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig("logs_ablation/fig_energy.png", dpi=300)

    # -------------------------------------------
    # 4) Bar charts: RMS / Peak / Spikes / P>0
    # -------------------------------------------
    metrics_list = ["rms_tau", "peak_tau", "n_spikes", "P_inject_ratio"]
    metric_names = [
        "RMS Torque",
        "Peak Torque",
        "# Torque Spikes",
        "Positive Power Ratio",
    ]

    plt.figure(figsize=(9,4))
    for i, key in enumerate(metrics_list):
        plt.subplot(1,4,i+1)
        vals = [all_metrics[m][key] for m in modes]
        plt.bar(modes, vals, color=[colors[m] for m in modes])
        plt.title(metric_names[i])
        plt.xticks(rotation=45)
        plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig("logs_ablation/fig_metrics_barchart.png", dpi=300)

    print("[INFO] All plots saved to logs_ablation/")



# -------------------- Main --------------------

def main():
    conf = load_config()
    master_model, slave_model, sew, estimator = build_modules(conf)

    Ts = 0.002
    steps = 800

    modes = ["baseline", "no_tank", "no_filter"]
    all_logs = {}
    all_metrics = {}

    print(f"dt = {Ts:.4f} s, steps = {steps}")

    for mode in modes:
        print(f"\n========== Running haptic ablation: {mode} ==========")
        renderer = make_renderer(conf, master_model, mode)
        renderer.reset_tank(float(conf.haptic_render.E0))

        # fix random seed so that each mode sees the same slave noise
        np.random.seed(0)

        t0 = time.time()
        logs = simulate_teleop(
            conf,
            master_model,
            slave_model,
            sew,
            estimator,
            renderer,
            Ts=Ts,
            steps=steps,
        )
        elapsed = time.time() - t0

        all_logs[mode] = logs

        metrics = compute_metrics(
            tau_fb=logs["tau_fb"],
            qd_m=logs["qd_m"],
            E=logs["E"],
            Ts=Ts,
        )
        all_metrics[mode] = metrics

        print(
            f"[{mode}] done in {elapsed:.3f} s | "
            f"rms_tau={metrics['rms_tau']:.3f}, "
            f"peak_tau={metrics['peak_tau']:.3f}, "
            f"spikes={metrics['n_spikes']}, "
            f"spike_rate={metrics['spike_rate']:.3f} 1/s, "
            f"P>0={metrics['P_inject_ratio']:.3f}, "
            f"E_range="
            f"{'[{:.2f},{:.2f}]'.format(metrics['E_min'], metrics['E_max']) if 'E_min' in metrics else 'N/A'}"
        )

    # Save logs for further plotting if needed
    save_dir = os.path.join(PROJECT_ROOT, "logs_ablation")
    os.makedirs(save_dir, exist_ok=True)
    save_path = os.path.join(save_dir, "haptics_ablation_results.npz")

    # pack as a single dict and rely on pickle when loading
    np.savez(
        save_path,
        all_logs=all_logs,
        all_metrics=all_metrics,
        Ts=Ts,
    )

    print(f"\n[INFO] Ablation logs and metrics saved to:\n  {save_path}")
    plot_ablation_results(all_logs, all_metrics, Ts)


if __name__ == "__main__":
    main()
