# -*- coding: utf-8 -*-
import numpy as np
import pinocchio as pin
from pathlib import Path


def hat(v):
    x, y, z = v
    return np.array([[0, -z, y],
                     [z,  0,-x],
                     [-y, x, 0]], dtype=float)


def normalize(v, eps=1e-12):
    n = np.linalg.norm(v)
    if n < eps:
        return v * 0.0
    return v / n


def rodrigues(u, phi):
    u = normalize(u)
    K = hat(u)
    return np.eye(3) + np.sin(phi)*K + (1-np.cos(phi))*(K@K)


def minimal_rotation_align(a, b):
    a = normalize(a); b = normalize(b)
    v = np.cross(a, b)
    s = np.linalg.norm(v)
    c = float(np.dot(a, b))
    if s < 1e-12:
        # parallel or anti-parallel
        if c > 0.0:
            return np.eye(3)
        # 180°: choose any axis orthogonal to a
        axis = normalize(np.array([1.0,0,0]) if abs(a[0])<0.9 else np.array([0,1.0,0]))
        axis = normalize(np.cross(a, axis))
        K = hat(axis)
        return np.eye(3) + 2*(K@K)  # R = I + 2[K]^2 for 180°
    K = hat(v/s)
    return np.eye(3) + K*s + (1-c)/(s*s) * (K@K)


def euler_zyx_from_R(R):
    """Return Z-Y-X Euler angles (about z,y,x of the shoulder frame)."""
    sy = -R[2,0]
    cy = np.sqrt(max(0.0, 1.0 - sy*sy))
    if cy > 1e-9:
        z = np.arctan2(R[1,0], R[0,0])
        y = np.arctan2(sy, cy)
        x = np.arctan2(R[2,1], R[2,2])
    else:
        # Gimbal case: cy ~ 0
        z = np.arctan2(-R[0,1], R[1,1])
        y = np.arctan2(sy, cy)
        x = 0.0
    return np.array([z, y, x], dtype=float)

def euler_xzy_from_R(R):
    """
    Decompose R ≈ Rx(a) * Rz(b) * Ry(c)
    Return (a, b, c)
    """
    # from derivation:
    # sb = -R[0,1]; cb = sqrt(R[0,0]^2 + R[0,2]^2)
    sb = -R[0,1]
    cb = np.sqrt(max(0.0, R[0,0]**2 + R[0,2]**2))
    b  = np.arctan2(sb, cb)
    # c = atan2(R[0,2], R[0,0])
    c  = np.arctan2(R[0,2], R[0,0])
    # a from R[1,1] = ca*cb and R[2,1] = sa*cb
    if cb < 1e-9:
        # singular: cb≈0 => b≈±pi/2，退化时把 a=0，c 吸收残差（简化处理）
        a = 0.0
    else:
        ca = np.clip(R[1,1] / cb, -1.0, 1.0)
        sa = np.clip(R[2,1] / cb, -1.0, 1.0)
        a  = np.arctan2(sa, ca)
    return np.array([a, b, c], dtype=float)

def euler_zy_from_R(R):
    """Return Z-Y Euler angles for wrist (about z then y)."""
    # R ≈ Rz(z) * Ry(y)
    # y = asin(R[0,2])? We'll use standard decomposition.
    # From R = Rz*Ry:
    # R[2,0] = -sin(y)
    y = np.arcsin(np.clip(R[0,2], -1.0, 1.0))
    cy = np.cos(y)
    if abs(cy) < 1e-9:
        z = 0.0
    else:
        z = np.arctan2(-R[0,1]/cy, R[0,0]/cy)
    return np.array([z, y], dtype=float)


def rot_error_deg(RA, RB):
    # 旋转误差：以李群对数映射的范数（弧度），再转度
    R = RA.T @ RB
    w = pin.log3(R)
    return np.linalg.norm(w) * 180.0/np.pi

def pose_of_frame(model, data, frame_name):
    fid = model.getFrameId(frame_name)
    oMf = data.oMf[fid]
    return oMf.translation.copy(), oMf.rotation.copy()

def fk_update(model, data, q):
    pin.forwardKinematics(model, data, q)
    pin.updateFramePlacements(model, data)



class SEWMapper:
    """
    SEW Mapper (closed-form, no numeric IK):
    - Loads master & slave URDFs
    - Computes heteromorphic retargeting from master EE pose to slave q_s using SEW geometry
    Assumptions:
      * Slave arm is 7-DoF in the order: 3 shoulder + 1 elbow + 3 wrist (axes orthogonal at shoulder/wrist frames).
      * Provide correct link/frame names and joint name order for your model.
    """

    def __init__(
        self,
        master_model: pin.Model,
        slave_model: pin.Model,
        # Frame/link names
        m_shoulder_frame: str,
        m_elbow_frame: str,
        m_wrist_frame: str,
        m_ee_frame: str,
        s_shoulder_frame: str,
        s_elbow_frame: str,
        s_wrist_frame: str,
        s_ee_frame: str,
        # Joint name order for slave 7-DoF: [S1,S2,S3, EL, W1, W2, W3]
        slave_joint_names: list,
        # world/up direction and safety margins
        up_dir=np.array([0,0,1.0]),
        eps_clip=1e-3,
    ):
        # Load master
        self.m_model = master_model
        self.m_data  = self.m_model.createData()

        # Load slave
        self.s_model = slave_model
        self.s_data  = self.s_model.createData()

        # Frame IDs
        self.fid_mS = self._get_frame_id(self.m_model, m_shoulder_frame)
        self.fid_mE = self._get_frame_id(self.m_model, m_elbow_frame)
        self.fid_mW = self._get_frame_id(self.m_model, m_wrist_frame)
        self.fid_mEE= self._get_frame_id(self.m_model, m_ee_frame)

        self.fid_sS = self._get_frame_id(self.s_model, s_shoulder_frame)
        self.fid_sE = self._get_frame_id(self.s_model, s_elbow_frame)
        self.fid_sW = self._get_frame_id(self.s_model, s_wrist_frame)
        self.fid_sEE= self._get_frame_id(self.s_model, s_ee_frame)

        # Up direction and epsilon
        self.up = normalize(up_dir)
        self.eps_clip = float(eps_clip)

        # Query slave joint indices (order critical)
        self.s_joint_ids = [self.s_model.getJointId(n) for n in slave_joint_names]
        self.s_qidx      = [self.s_model.joints[jid].idx_q for jid in self.s_joint_ids]
        assert len(self.s_qidx) == 7, "Provide 7 slave joints in order [S1,S2,S3, EL, W1, W2, W3]"

        # Pre-compute slave segment lengths L1, L2 in reference (zero) config
        self.qs0 = pin.neutral(self.s_model)
        self._update_fk_slave(self.qs0)
        pS = self._frame_pos(self.s_data, self.fid_sS)
        pE = self._frame_pos(self.s_data, self.fid_sE)
        pW = self._frame_pos(self.s_data, self.fid_sW)
        self.L1 = float(np.linalg.norm(pE - pS))
        self.L2 = float(np.linalg.norm(pW - pE))

        # Fixed slave shoulder location in world
        self.pS_s_fixed = pS.copy()

    # ------------------ Utilities ------------------ #
    def _get_frame_id(self, model, name):
        try:
            return model.getFrameId(name)
        except:
            # fallback: also try link->frame mapping via joint name
            return model.getFrameId(name)

    def _update_fk_master(self, q_m):
        pin.forwardKinematics(self.m_model, self.m_data, q_m)
        pin.updateFramePlacements(self.m_model, self.m_data)

    def _update_fk_slave(self, q_s):
        pin.forwardKinematics(self.s_model, self.s_data, q_s)
        pin.updateFramePlacements(self.s_model, self.s_data)

    def _frame_pos(self, data, fid):
        return data.oMf[fid].translation.copy()

    def _frame_rot(self, data, fid):
        return data.oMf[fid].rotation.copy()

    # ------------------ Core Retarget ------------------ #
    def retargetting(self, q_m, q_s_init=None):
        """
        Input:
          q_m     : master joint configuration (np.ndarray, size = master nq)
          q_s_init: optional slave initial seed (ignored for geometry, used only to keep continuity of angle unwrap if desired)
        Output:
          q_s     : slave joint angles (np.ndarray, size = slave nq)
          debug   : dict with intermediate targets (pE_s, pW_s_ref, phi_m, theta)
        """
        # 1) Master FK and SEW quantities
        self._update_fk_master(q_m)
        pS_m = self._frame_pos(self.m_data, self.fid_mS)
        pE_m = self._frame_pos(self.m_data, self.fid_mE)
        pW_m = self._frame_pos(self.m_data, self.fid_mW)
        # RW_m = self._frame_rot(self.m_data, self.fid_mW)
        RE_m = self._frame_rot(self.m_data, self.fid_mEE) 

        r_m = pW_m - pS_m
        d_m = float(np.linalg.norm(r_m))
        xhat_m = normalize(r_m)

        # Swivel (master)
        nm_raw = np.cross(pE_m - pS_m, pW_m - pS_m)
        nm = normalize(nm_raw)
        nref_tilde = self.up - np.dot(self.up, xhat_m) * xhat_m
        if np.linalg.norm(nref_tilde) < 1e-6:
            ey = np.array([0,1.0,0])
            nref_tilde = ey - np.dot(ey, xhat_m) * xhat_m
        nref = normalize(nref_tilde)
        num = np.dot(xhat_m, np.cross(nref, nm))
        den = float(np.dot(nref, nm))
        phi_m = np.arctan2(num, den)

        # 2) Wrist pose and reach clipping on slave
        d_min = abs(self.L1 - self.L2) + self.eps_clip
        d_max = (self.L1 + self.L2) - self.eps_clip
        d_s = np.clip(d_m, d_min, d_max)
        xhat_s = xhat_m.copy()
        pS_s = self.pS_s_fixed
        pW_s_ref = pS_s + d_s * xhat_s
        R_ee_ref = RE_m.copy()   # preserve orientation

        # 3) Two-sphere elbow construction on slave
        d = float(np.linalg.norm(pW_s_ref - pS_s))
        e3 = rodrigues(xhat_s, phi_m) @ nref
        e3 = normalize(e3)
        e2 = normalize(np.cross(xhat_s, e3))
        cos_th = (self.L1**2 + d**2 - self.L2**2) / (2*self.L1*d)
        cos_th = float(np.clip(cos_th, -1.0, 1.0))
        th = np.arccos(cos_th)
        sin_th = np.sqrt(max(0.0, 1.0 - cos_th*cos_th))
        pE_s = pS_s + self.L1*(cos_th * xhat_s + sin_th * e2)

        # 4) Joint reconstruction (assumes 3-1-3 structure with Z-Y-X at shoulder and Z-Y at wrist)
        q_s = pin.neutral(self.s_model) if q_s_init is None else q_s_init.copy()

        # (a) Set shoulder (first 3 joints): align upper-arm vector
        u = normalize(pE_s - pS_s)
        y_axis = e3               # 让“肘轴=局部Y”严格对齐 SEW 的平面法向
        x_axis = u
        z_axis = normalize(np.cross(x_axis, y_axis))
        RS_des = np.column_stack([x_axis, y_axis, z_axis])


        dz, dy, dx = euler_zyx_from_R(RS_des)               # R ≈ Rz(dz)*Ry(dy)*Rx(dx)
        q_s[self.s_qidx[0]] = dz
        q_s[self.s_qidx[1]] = dy
        q_s[self.s_qidx[2]] = dx
        # (b) Elbow flex = pi - theta
        fhat = normalize(pW_s_ref - pE_s)
        u = normalize(pE_s - pS_s)# 上臂方向（从肩指向肘）
        q_elbow = np.arctan2(np.dot(e3, np.cross(u, fhat)), np.dot(u, fhat))# 计算把 u 绕 e3 旋到 fhat 的有符号角：atan2( 轴·(u×f),  u·f )
        q_s[self.s_qidx[3]] = q_elbow
        self._update_fk_slave(q_s)
        # (c) Wrist orientation match with X–Z–Y on wrist frame
        RW_cur = self._frame_rot(self.s_data, self.fid_sW)
        R_needed = RW_cur.T @ R_ee_ref         
        a_b_c = euler_xzy_from_R(R_needed)      # (roll-X, yaw-Z, pitch-Y)
        q_s[self.s_qidx[4]] = a_b_c[0]
        q_s[self.s_qidx[5]] = a_b_c[1]
        q_s[self.s_qidx[6]] = a_b_c[2]

        debug = dict(
            phi_m=phi_m,
            d_s=d_s,
            pW_s_ref=pW_s_ref,
            pE_s=pE_s,
            theta=th
        )
        return q_s, debug
