"""Body impulse demo with force visualization.

A cylinder hangs from a ball joint like a punching bag. Random impulses
swing it around while magenta arrows show the applied forces. Both native
and Viser viewers render the arrows via ``apply_body_impulse``'s built-in
debug visualization.

Run with:
  uv run mjpython scripts/demos/body_impulse.py                      # macOS
  uv run python scripts/demos/body_impulse.py                        # Linux
  uv run python scripts/demos/body_impulse.py --viewer viser         # Viser
"""

from __future__ import annotations

import math
import os

import mujoco
import torch
import tyro

import mjlab
from mjlab.entity import EntityCfg
from mjlab.envs import ManagerBasedRlEnv, ManagerBasedRlEnvCfg
from mjlab.envs.mdp.events import apply_body_impulse, reset_scene_to_default
from mjlab.managers.event_manager import EventTermCfg
from mjlab.managers.scene_entity_config import SceneEntityCfg
from mjlab.rl import RslRlVecEnvWrapper
from mjlab.scene import SceneCfg
from mjlab.utils.torch import configure_torch_backends
from mjlab.viewer import NativeMujocoViewer, ViserPlayViewer

BAG_RADIUS = 0.12  # punching bag radius
BAG_HALF_HEIGHT = 0.25  # punching bag half-height
BAG_DENSITY = 500.0  # ~5.7 kg
ROPE_LEN = 0.3  # rope length in meters
CEILING_Z = 1.0  # pivot height
ROPE_RADIUS = 0.008  # visual rope thickness


def create_pendulum_spec() -> mujoco.MjSpec:
  spec = mujoco.MjSpec()
  spec.modelname = "impulse_punching_bag"

  # Ground plane.
  ground = spec.worldbody.add_geom()
  ground.type = mujoco.mjtGeom.mjGEOM_PLANE
  ground.size[:] = (5.0, 5.0, 0.1)
  ground.rgba[:] = (0.4, 0.5, 0.6, 1.0)

  # Light.
  light = spec.worldbody.add_light()
  light.pos[:] = (0, 0, 4)
  light.dir[:] = (0, 0, -1)
  light.diffuse[:] = (0.8, 0.8, 0.8)

  # Ceiling anchor (visual only).
  anchor = spec.worldbody.add_geom()
  anchor.type = mujoco.mjtGeom.mjGEOM_BOX
  anchor.size[:] = (0.04, 0.04, 0.02)
  anchor.pos[:] = (0, 0, CEILING_Z)
  anchor.rgba[:] = (0.3, 0.3, 0.3, 1.0)
  anchor.contype = 0
  anchor.conaffinity = 0

  # Pendulum body. Ball joint pivot is at (0, 0, CEILING_Z).
  bag_body = spec.worldbody.add_body()
  bag_body.name = "bag"
  bag_body.pos[:] = (0, 0, CEILING_Z)

  joint = bag_body.add_joint()
  joint.name = "bag_joint"
  joint.type = mujoco.mjtJoint.mjJNT_BALL
  joint.damping[:] = 3.0
  joint.frictionloss = 0.5

  # Rope (visual only capsule from pivot to bag center).
  rope = bag_body.add_geom()
  rope.type = mujoco.mjtGeom.mjGEOM_CAPSULE
  rope.size[:2] = (ROPE_RADIUS, ROPE_LEN / 2)
  rope.pos[:] = (0, 0, -ROPE_LEN / 2)
  rope.rgba[:] = (0.5, 0.4, 0.3, 1.0)
  rope.contype = 0
  rope.conaffinity = 0
  rope.mass = 0.001  # negligible mass

  # Punching bag cylinder hanging at the end of the rope.
  geom = bag_body.add_geom()
  geom.name = "bag_geom"
  geom.type = mujoco.mjtGeom.mjGEOM_CYLINDER
  geom.size[:2] = (BAG_RADIUS, BAG_HALF_HEIGHT)
  geom.pos[:] = (0, 0, -ROPE_LEN - BAG_HALF_HEIGHT)
  geom.density = BAG_DENSITY
  geom.rgba[:] = (0.55, 0.15, 0.1, 0.35)

  return spec


def create_env_cfg() -> ManagerBasedRlEnvCfg:
  bag_cfg = EntityCfg(
    spec_fn=create_pendulum_spec,
    init_state=EntityCfg.InitialStateCfg(
      pos=(0.0, 0.0, 0.0),
    ),
  )

  bag_mass = BAG_DENSITY * math.pi * BAG_RADIUS**2 * (2 * BAG_HALF_HEIGHT)
  weight = bag_mass * 9.81
  force_mag = weight * 0.8  # 0.8x body weight

  cfg = ManagerBasedRlEnvCfg(
    decimation=10,
    scene=SceneCfg(
      num_envs=1,
      env_spacing=0.0,
      extent=2.0,
      entities={"bag": bag_cfg},
    ),
    events={
      "reset_scene_to_default": EventTermCfg(
        func=reset_scene_to_default,
        mode="reset",
      ),
      "impulse": EventTermCfg(
        func=apply_body_impulse,
        mode="step",
        params={
          "force_range": (-force_mag, force_mag),
          "torque_range": (-force_mag * 0.3, force_mag * 0.3),
          "duration_s": (0.05, 0.1),
          "cooldown_s": (1.0, 2.5),
          "asset_cfg": SceneEntityCfg("bag", body_names=("bag",)),
        },
      ),
    },
  )

  cfg.viewer.distance = 1.8
  cfg.viewer.elevation = -10.0

  return cfg


class ZeroPolicy:
  def __call__(self, obs: object) -> torch.Tensor:
    del obs
    return torch.zeros(1, 0)


def main(device: str = "cpu", viewer: str = "auto") -> None:
  configure_torch_backends()

  env_cfg = create_env_cfg()
  env = ManagerBasedRlEnv(cfg=env_cfg, device=device)
  env = RslRlVecEnvWrapper(env)

  # Print force scaling info.
  mjm = env.unwrapped.sim.mj_model
  bag_id = mujoco.mj_name2id(mjm, mujoco.mjtObj.mjOBJ_BODY, "bag")
  subtree_mass = mjm.body_subtreemass[bag_id]
  weight = subtree_mass * 9.81
  bag_mass = BAG_DENSITY * math.pi * BAG_RADIUS**2 * (2 * BAG_HALF_HEIGHT)
  force_mag = bag_mass * 9.81 * 0.8
  print("=" * 50)
  print("Body Impulse Demo (punching bag)")
  print(f"  Bag mass         : {bag_mass:.2f} kg")
  print(f"  Bag weight       : {weight:.2f} N")
  print(f"  Rope length      : {ROPE_LEN} m")
  print(f"  Force range      : +/-{force_mag:.0f} N")
  print(f"  Force/weight     : {force_mag / weight:.1f}x")
  print("=" * 50)

  if viewer == "auto":
    has_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
    resolved = "native" if has_display else "viser"
  else:
    resolved = viewer

  policy = ZeroPolicy()
  if resolved == "native":
    print("Launching native viewer...")
    NativeMujocoViewer(env, policy).run()
  elif resolved == "viser":
    print("Launching Viser viewer...")
    ViserPlayViewer(env, policy).run()
  else:
    raise ValueError(f"Unknown viewer: {viewer}")

  env.close()


if __name__ == "__main__":
  tyro.cli(main, config=mjlab.TYRO_FLAGS)
