"""Raycast sensor demo.

Run with:
  uv run mjpython scripts/demos/raycast_sensor.py [--viewer native|viser]  # macOS
  uv run python scripts/demos/raycast_sensor.py [--viewer native|viser]    # Linux

Examples:
  # Grid pattern (default)
  uv run python scripts/demos/raycast_sensor.py --pattern grid

  # Pinhole camera pattern
  uv run python scripts/demos/raycast_sensor.py --pattern pinhole

  # With yaw alignment (ignores pitch/roll)
  uv run python scripts/demos/raycast_sensor.py --alignment yaw

If using the native viewer, you can launch in interactive mode with:
  uv run mjpython scripts/demos/raycast_sensor.py --viewer native --interactive
"""

from __future__ import annotations

import os
from typing import Literal

import mujoco
import numpy as np
import torch
import tyro

import mjlab
import mjlab.terrains as terrain_gen
from mjlab.entity import EntityCfg
from mjlab.envs import ManagerBasedRlEnv, ManagerBasedRlEnvCfg
from mjlab.rl import RslRlVecEnvWrapper
from mjlab.scene import SceneCfg
from mjlab.sensor import (
  GridPatternCfg,
  ObjRef,
  PinholeCameraPatternCfg,
  RayCastSensorCfg,
)
from mjlab.terrains.terrain_entity import TerrainEntityCfg
from mjlab.terrains.terrain_generator import TerrainGeneratorCfg
from mjlab.utils.torch import configure_torch_backends
from mjlab.viewer import NativeMujocoViewer, ViserPlayViewer


def create_scanner_spec() -> mujoco.MjSpec:
  spec = mujoco.MjSpec()
  spec.modelname = "scanner"

  mat = spec.add_material()
  mat.name = "scanner_mat"
  mat.rgba[:] = (1.0, 0.5, 0.0, 0.9)

  scanner = spec.worldbody.add_body(mocap=True)
  scanner.name = "scanner"
  scanner.pos[:] = (0, 0, 2.0)

  geom = scanner.add_geom()
  geom.name = "scanner_geom"
  geom.type = mujoco.mjtGeom.mjGEOM_BOX
  geom.size[:] = (0.15, 0.15, 0.05)
  geom.mass = 1.0
  geom.material = "scanner_mat"

  scanner.add_camera(name="scanner", fovy=58.0, resolution=(16, 12))

  record_cam = scanner.add_camera(name="record_cam")
  record_cam.pos[:] = (2, 0, 2)
  record_cam.fovy = 40.0
  record_cam.mode = mujoco.mjtCamLight.mjCAMLIGHT_TARGETBODY
  record_cam.targetbody = "scanner"

  return spec


def create_env_cfg(
  pattern: Literal["grid", "pinhole"],
  alignment: Literal["base", "yaw", "world"],
) -> ManagerBasedRlEnvCfg:
  custom_terrain_cfg = TerrainGeneratorCfg(
    size=(4.0, 4.0),
    border_width=0.5,
    num_rows=1,
    num_cols=4,
    curriculum=True,
    sub_terrains={
      "pyramid_stairs_inv": terrain_gen.BoxInvertedPyramidStairsTerrainCfg(
        proportion=0.25,
        step_height_range=(0.1, 0.25),
        step_width=0.3,
        platform_width=1.5,
        border_width=0.25,
      ),
      "hf_pyramid_slope_inv": terrain_gen.HfPyramidSlopedTerrainCfg(
        proportion=0.25,
        slope_range=(0.6, 1.5),
        platform_width=1.5,
        border_width=0.25,
        inverted=True,
      ),
      "random_rough": terrain_gen.HfRandomUniformTerrainCfg(
        proportion=0.25,
        noise_range=(0.05, 0.15),
        noise_step=0.02,
        border_width=0.25,
      ),
      "wave_terrain": terrain_gen.HfWaveTerrainCfg(
        proportion=0.25,
        amplitude_range=(0.15, 0.25),
        num_waves=3,
        border_width=0.25,
      ),
    },
    add_lights=True,
  )

  terrain_cfg = TerrainEntityCfg(
    terrain_type="generator",
    terrain_generator=custom_terrain_cfg,
    num_envs=1,
  )

  scanner_entity_cfg = EntityCfg(
    spec_fn=create_scanner_spec,
    init_state=EntityCfg.InitialStateCfg(pos=(0.65, -0.4, 0.5)),
  )

  if pattern == "grid":
    pattern_cfg = GridPatternCfg(
      size=(0.6, 0.6),
      resolution=0.1,
      direction=(0.0, 0.0, -1.0),
    )
  else:
    assert pattern == "pinhole"
    pattern_cfg = PinholeCameraPatternCfg.from_mujoco_camera("scanner/scanner")

  raycast_cfg = RayCastSensorCfg(
    name="terrain_scan",
    frame=ObjRef(type="body", name="scanner", entity="scanner"),
    pattern=pattern_cfg,
    ray_alignment=alignment,
    max_distance=5.0,
    exclude_parent_body=True,
    debug_vis=True,
    viz=RayCastSensorCfg.VizCfg(
      hit_color=(0.0, 1.0, 0.0, 0.9),
      miss_color=(1.0, 0.0, 0.0, 0.5),
      show_rays=False,
      show_normals=True,
    ),
  )

  cfg = ManagerBasedRlEnvCfg(
    decimation=10,
    scene=SceneCfg(
      num_envs=1,
      env_spacing=0.0,
      extent=2.0,
      terrain=terrain_cfg,
      entities={"scanner": scanner_entity_cfg},
      sensors=(raycast_cfg,),
    ),
  )

  cfg.viewer.body_name = "scanner"
  cfg.viewer.distance = 12.0
  cfg.viewer.elevation = -25.0
  cfg.viewer.azimuth = 135.0

  return cfg


def main(
  viewer: str = "auto",
  interactive: bool = False,
  pattern: Literal["grid", "pinhole"] = "grid",
  alignment: Literal["base", "yaw", "world"] = "base",
) -> None:
  configure_torch_backends()

  device = "cuda:0" if torch.cuda.is_available() else "cpu"

  print("=" * 60)
  print("Raycast Sensor Demo - 4 Terrain Types")
  print(f"  Pattern: {pattern}")
  print(f"  Alignment: {alignment}")
  print("=" * 60)
  print()

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

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

  use_auto_scan = (resolved_viewer == "viser") or (not interactive)

  if use_auto_scan:

    class AutoScanPolicy:
      def __init__(self):
        self.step_count = 0

      def __call__(self, obs) -> torch.Tensor:
        del obs
        t = self.step_count * 0.005
        y_period = 1000
        y_normalized = (self.step_count % y_period) / y_period
        y = -8.0 + 16.0 * y_normalized
        x = 1.5 * np.sin(2 * np.pi * t * 0.3)
        z = 1.0
        env.unwrapped.sim.data.mocap_pos[0, 0, :] = torch.tensor(
          [x, y, z], device=device, dtype=torch.float32
        )
        env.unwrapped.sim.data.mocap_quat[0, 0, :] = torch.tensor(
          [1, 0, 0, 0], device=device, dtype=torch.float32
        )
        self.step_count += 1
        return torch.zeros(env.unwrapped.action_space.shape, device=device)

    policy = AutoScanPolicy()
  else:

    class PolicyZero:
      def __call__(self, obs) -> torch.Tensor:
        del obs
        return torch.zeros(env.unwrapped.action_space.shape, device=device)

    policy = PolicyZero()

  if resolved_viewer == "native":
    print("Launching native viewer...")
    NativeMujocoViewer(env, policy).run()
  elif resolved_viewer == "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)
