# json_to_urdf.py
import json
import math
import xml.etree.ElementTree as ET
from pathlib import Path
from xml.dom import minidom

# ============================================================
# CONFIG
# ============================================================

ROBOT_JSON = "robot.json"
OUTPUT_URDF = "robot.urdf"

# ============================================================
# HELPERS
# ============================================================


def mm_to_m(v):
    return float(v) / 1000.0


def vec_mm_to_m(v):
    return [mm_to_m(x) for x in v]


def deg_to_rad(v):
    return math.radians(float(v))


def vec_deg_to_rad(v):
    return [deg_to_rad(x) for x in v]


def xyz_str(v):
    return f"{v[0]:.6f} {v[1]:.6f} {v[2]:.6f}"


# ============================================================
# LOAD JSON
# ============================================================

with open(ROBOT_JSON, "r", encoding="utf-8") as f:
    robot_json = json.load(f)

links = robot_json.get("links", {})

# ============================================================
# ROOT
# ============================================================

robot_name = Path(ROBOT_JSON).stem
robot = ET.Element("robot")
robot.set("name", robot_name)

# ============================================================
# CREATE LINKS
# ============================================================

for link_name, link_info in links.items():

    urdf_link = ET.SubElement(robot, "link")
    urdf_link.set("name", link_name)

    # --------------------------------------------------------
    # VISUALS
    # --------------------------------------------------------

    models = link_info.get("model", [])

    for model in models:

        stl_file = model.get("stlFile")
        if not stl_file:
            continue

        visual = ET.SubElement(urdf_link, "visual")

        origin = ET.SubElement(visual, "origin")

        xyz = vec_mm_to_m(model.get("originOfModel", [0, 0, 0]))
        rpy = vec_deg_to_rad(model.get("rotationOfModelDegree", [0, 0, 0]))

        origin.set("xyz", xyz_str(xyz))
        origin.set("rpy", xyz_str(rpy))

        geometry = ET.SubElement(visual, "geometry")

        mesh = ET.SubElement(geometry, "mesh")
        mesh.set("filename", stl_file)
        mesh.set("scale", "0.001 0.001 0.001")

    # --------------------------------------------------------
    # SIMPLE COLLISION FROM SKELETON
    # --------------------------------------------------------

    skeleton = link_info.get("skeleton")

    if isinstance(skeleton, dict):

        p1 = skeleton.get("from", [0, 0, 0])
        p2 = skeleton.get("to", [0, 0, 0])

        dx = p2[0] - p1[0]
        dy = p2[1] - p1[1]
        dz = p2[2] - p1[2]

        length_mm = math.sqrt(dx * dx + dy * dy + dz * dz)
        length_m = mm_to_m(length_mm)

        radius_mm = skeleton.get("radius", 4)
        radius_m = mm_to_m(radius_mm)

        cx = (p1[0] + p2[0]) * 0.5
        cy = (p1[1] + p2[1]) * 0.5
        cz = (p1[2] + p2[2]) * 0.5

        collision = ET.SubElement(urdf_link, "collision")

        collision_origin = ET.SubElement(collision, "origin")
        collision_origin.set(
            "xyz",
            xyz_str(vec_mm_to_m([cx, cy, cz]))
        )

        # ----------------------------------------------------
        # CYLINDER ORIENTATION
        # URDF cylinder points along Z
        # ----------------------------------------------------

        roll = 0.0
        pitch = 0.0
        yaw = 0.0

        if abs(dx) >= abs(dy) and abs(dx) >= abs(dz):
            pitch = math.radians(90)
            yaw = 0.0

        elif abs(dy) >= abs(dx) and abs(dy) >= abs(dz):
            pitch = math.radians(90)
            yaw = math.radians(90)

        collision_origin.set(
            "rpy",
            xyz_str([roll, pitch, yaw])
        )

        geometry = ET.SubElement(collision, "geometry")

        cylinder = ET.SubElement(geometry, "cylinder")
        cylinder.set("radius", f"{radius_m:.6f}")
        cylinder.set("length", f"{length_m:.6f}")

# ============================================================
# CREATE JOINTS
# ============================================================

for link_name, link_info in links.items():

    parent_name = link_info.get("parent")

    if parent_name is None:
        continue

    joint_info = link_info.get("jointToParent", {})

    joint_name = joint_info.get("name", f"joint_{parent_name}_{link_name}")
    joint_type = joint_info.get("type", "fixed")

    if joint_type == "linear":
        urdf_joint_type = "prismatic"
    elif joint_type == "revolute":
        urdf_joint_type = "revolute"
    else:
        urdf_joint_type = "fixed"

    joint = ET.SubElement(robot, "joint")
    joint.set("name", joint_name)
    joint.set("type", urdf_joint_type)

    # --------------------------------------------------------
    # PARENT / CHILD
    # --------------------------------------------------------

    parent = ET.SubElement(joint, "parent")
    parent.set("link", parent_name)

    child = ET.SubElement(joint, "child")
    child.set("link", link_name)

    # --------------------------------------------------------
    # ORIGIN
    # --------------------------------------------------------

    origin_xyz = joint_info.get("origin", [0, 0, 0])
    origin_rpy = joint_info.get("rotation", [0, 0, 0])

    origin = ET.SubElement(joint, "origin")
    origin.set(
        "xyz",
        xyz_str(vec_mm_to_m(origin_xyz))
    )
    origin.set(
        "rpy",
        xyz_str(vec_deg_to_rad(origin_rpy))
    )

    # --------------------------------------------------------
    # AXIS
    # --------------------------------------------------------

    axis = joint_info.get("axis", [1, 0, 0])

    axis_tag = ET.SubElement(joint, "axis")
    axis_tag.set("xyz", xyz_str(axis))

    # --------------------------------------------------------
    # DEFAULT LIMITS
    # --------------------------------------------------------

    if urdf_joint_type in ["revolute", "prismatic"]:

        limit = ET.SubElement(joint, "limit")

        if urdf_joint_type == "revolute":
            limit.set("lower", f"{-math.pi:.6f}")
            limit.set("upper", f"{math.pi:.6f}")
        else:
            limit.set("lower", "-1.0")
            limit.set("upper", "1.0")

        limit.set("effort", "100")
        limit.set("velocity", "10")

# ============================================================
# PRETTY XML
# ============================================================

xml_string = ET.tostring(robot, encoding="utf-8")
pretty = minidom.parseString(xml_string).toprettyxml(indent="  ")

with open(OUTPUT_URDF, "w", encoding="utf-8") as f:
    f.write(pretty)

print("URDF written:", OUTPUT_URDF)
