# Copyright (c) 2022 NVIDIA CORPORATION.  All rights reserved.
# NVIDIA CORPORATION and its licensors retain all intellectual property
# and proprietary rights in and to this software, related documentation
# and any modifications thereto.  Any use, reproduction, disclosure or
# distribution of this software and related documentation without an express
# license agreement from NVIDIA CORPORATION is strictly prohibited.

import warp as wp
import warp.sim
import warp.render

from collections import defaultdict

import numpy as np

from warp.render.utils import solidify_mesh, tab10_color_map

# TODO allow NaNs in Warp kernels
NAN = wp.constant(-1.0e8)


@wp.kernel
def compute_contact_points(
    body_q: wp.array(dtype=wp.transform),
    shape_body: wp.array(dtype=int),
    contact_shape0: wp.array(dtype=int),
    contact_shape1: wp.array(dtype=int),
    contact_point0: wp.array(dtype=wp.vec3),
    contact_point1: wp.array(dtype=wp.vec3),
    # outputs
    contact_pos0: wp.array(dtype=wp.vec3),
    contact_pos1: wp.array(dtype=wp.vec3),
):
    tid = wp.tid()
    shape_a = contact_shape0[tid]
    shape_b = contact_shape1[tid]
    if shape_a == shape_b:
        contact_pos0[tid] = wp.vec3(NAN, NAN, NAN)
        contact_pos1[tid] = wp.vec3(NAN, NAN, NAN)
        return

    body_a = shape_body[shape_a]
    body_b = shape_body[shape_b]
    X_wb_a = wp.transform_identity()
    X_wb_b = wp.transform_identity()
    if body_a >= 0:
        X_wb_a = body_q[body_a]
    if body_b >= 0:
        X_wb_b = body_q[body_b]

    contact_pos0[tid] = wp.transform_point(X_wb_a, contact_point0[tid])
    contact_pos1[tid] = wp.transform_point(X_wb_b, contact_point1[tid])


def CreateSimRenderer(renderer):
    class SimRenderer(renderer):
        use_unique_colors = True

        def __init__(
            self,
            model: warp.sim.Model,
            path,
            scaling=1.0,
            fps=60,
            up_axis="Y",
            show_rigid_contact_points=False,
            contact_points_radius=1e-3,
            show_joints=False,
            **render_kwargs,
        ):
            # create USD stage
            super().__init__(path, scaling=scaling, fps=fps, up_axis=up_axis, **render_kwargs)
            self.scaling = scaling
            self.cam_axis = "XYZ".index(up_axis.upper())
            self.show_rigid_contact_points = show_rigid_contact_points
            self.show_joints = show_joints
            self.contact_points_radius = contact_points_radius
            self.populate(model)

        def populate(self, model: warp.sim.Model):
            self.skip_rendering = False

            self.model = model
            self.num_envs = model.num_envs
            self.body_names = []

            if self.show_rigid_contact_points and model.rigid_contact_max:
                self.contact_points0 = wp.array(
                    np.zeros((model.rigid_contact_max, 3)), dtype=wp.vec3, device=model.device
                )
                self.contact_points1 = wp.array(
                    np.zeros((model.rigid_contact_max, 3)), dtype=wp.vec3, device=model.device
                )

                self.contact_points0_colors = [(1.0, 0.5, 0.0)] * model.rigid_contact_max
                self.contact_points1_colors = [(0.0, 0.5, 1.0)] * model.rigid_contact_max

            self.body_env = []  # mapping from body index to its environment index
            env_id = 0
            self.bodies_per_env = model.body_count // self.num_envs
            # create rigid body nodes
            for b in range(model.body_count):
                body_name = f"body_{b}_{self.model.body_name[b].replace(' ', '_')}"
                self.body_names.append(body_name)
                self.register_body(body_name)
                if b > 0 and b % self.bodies_per_env == 0:
                    env_id += 1
                self.body_env.append(env_id)

            # create rigid shape children
            if self.model.shape_count:
                # mapping from hash of geometry to shape ID
                self.geo_shape = {}

                self.instance_count = 0

                self.body_name = {}  # mapping from body name to its body ID
                self.body_shapes = defaultdict(list)  # mapping from body index to its shape IDs

                shape_body = model.shape_body.numpy()
                shape_geo_src = model.shape_geo_src
                shape_geo_type = model.shape_geo.type.numpy()
                shape_geo_scale = model.shape_geo.scale.numpy()
                shape_geo_thickness = model.shape_geo.thickness.numpy()
                shape_geo_is_solid = model.shape_geo.is_solid.numpy()
                shape_transform = model.shape_transform.numpy()

                p = np.zeros(3, dtype=np.float32)
                q = np.array([0.0, 0.0, 0.0, 1.0], dtype=np.float32)
                scale = np.ones(3)
                color = np.ones(3)
                # loop over shapes excluding the ground plane
                for s in range(model.shape_count - 1):
                    geo_type = shape_geo_type[s]
                    geo_scale = [float(v) for v in shape_geo_scale[s]]
                    geo_thickness = float(shape_geo_thickness[s])
                    geo_is_solid = bool(shape_geo_is_solid[s])
                    geo_src = shape_geo_src[s]
                    if self.use_unique_colors:
                        color = self._get_new_color()
                    name = f"shape_{s}"

                    # shape transform in body frame
                    body = int(shape_body[s])
                    if body >= 0 and body < len(self.body_names):
                        body = self.body_names[body]
                    else:
                        body = None

                    # shape transform in body frame
                    X_bs = wp.transform_expand(shape_transform[s])
                    # check whether we can instance an already created shape with the same geometry
                    geo_hash = hash((int(geo_type), geo_src, *geo_scale, geo_thickness, geo_is_solid))
                    if geo_hash in self.geo_shape:
                        shape = self.geo_shape[geo_hash]
                    else:
                        if geo_type == warp.sim.GEO_PLANE:
                            if s == model.shape_count - 1 and not model.ground:
                                continue  # hide ground plane

                            # plane mesh
                            width = geo_scale[0] if geo_scale[0] > 0.0 else 100.0
                            length = geo_scale[1] if geo_scale[1] > 0.0 else 100.0

                            shape = self.render_plane(
                                name, p, q, width, length, color, parent_body=body, is_template=True
                            )

                        elif geo_type == warp.sim.GEO_SPHERE:
                            shape = self.render_sphere(name, p, q, geo_scale[0], parent_body=body, is_template=True)

                        elif geo_type == warp.sim.GEO_CAPSULE:
                            shape = self.render_capsule(
                                name, p, q, geo_scale[0], geo_scale[1], parent_body=body, is_template=True
                            )

                        elif geo_type == warp.sim.GEO_CYLINDER:
                            shape = self.render_cylinder(
                                name, p, q, geo_scale[0], geo_scale[1], parent_body=body, is_template=True
                            )

                        elif geo_type == warp.sim.GEO_CONE:
                            shape = self.render_cone(
                                name, p, q, geo_scale[0], geo_scale[1], parent_body=body, is_template=True
                            )

                        elif geo_type == warp.sim.GEO_BOX:
                            shape = self.render_box(name, p, q, geo_scale, parent_body=body, is_template=True)

                        elif geo_type == warp.sim.GEO_MESH:
                            if not geo_is_solid:
                                faces, vertices = solidify_mesh(geo_src.indices, geo_src.vertices, geo_thickness)
                            else:
                                faces, vertices = geo_src.indices, geo_src.vertices

                            shape = self.render_mesh(
                                name,
                                vertices,
                                faces,
                                pos=p,
                                rot=q,
                                scale=geo_scale,
                                colors=[color],
                                parent_body=body,
                                is_template=True,
                            )

                        elif geo_type == warp.sim.GEO_SDF:
                            continue

                        self.geo_shape[geo_hash] = shape

                    self.add_shape_instance(name, shape, body, X_bs.p, X_bs.q, scale)
                    self.instance_count += 1

                if self.show_joints and model.joint_count:
                    joint_type = model.joint_type.numpy()
                    joint_axis = model.joint_axis.numpy()
                    joint_axis_start = model.joint_axis_start.numpy()
                    joint_axis_dim = model.joint_axis_dim.numpy()
                    joint_parent = model.joint_parent.numpy()
                    joint_child = model.joint_child.numpy()
                    joint_tf = model.joint_X_p.numpy()
                    shape_collision_radius = model.shape_collision_radius.numpy()
                    y_axis = wp.vec3(0., 1., 0.)
                    color = (1., 0., 1.)

                    shape = self.render_arrow(
                        "joint_arrow", None, None,
                        base_radius=0.01, base_height=0.4,
                        cap_radius=0.02, cap_height=0.1,
                        parent_body=None, is_template=True,
                        color=color
                    )
                    for i, t in enumerate(joint_type):
                        if t not in {
                            warp.sim.JOINT_REVOLUTE,
                            # warp.sim.JOINT_PRISMATIC,
                            warp.sim.JOINT_UNIVERSAL,
                            warp.sim.JOINT_COMPOUND,
                            warp.sim.JOINT_D6,
                        }:
                            continue
                        tf = joint_tf[i]
                        body = int(joint_parent[i])
                        # if body == -1:
                        #     continue
                        num_linear_axes = int(joint_axis_dim[i][0])
                        num_angular_axes = int(joint_axis_dim[i][1])

                        # find a good scale for the arrow based on the average radius
                        # of the shapes attached to the joint child body
                        scale = np.ones(3)
                        child = int(joint_child[i])
                        if child >= 0:
                            radii = []
                            for s in model.body_shapes[child]:
                                radii.append(shape_collision_radius[s])
                            if len(radii) > 0:
                                scale *= np.mean(radii) * 2.0

                        for a in range(num_linear_axes, num_linear_axes + num_angular_axes):
                            index = joint_axis_start[i] + a
                            axis = joint_axis[index]
                            if np.linalg.norm(axis) < 1e-6:
                                continue
                            p = wp.vec3(tf[:3])
                            q = wp.quat(tf[3:])
                            # compute rotation between axis and y
                            axis = axis / np.linalg.norm(axis)
                            q = q * wp.quat_between_vectors(wp.vec3(*axis), y_axis)
                            name = f"joint_{i}_{a}"
                            self.add_shape_instance(name, shape, body, p, q, scale, color1=color, color2=color)
                            self.instance_count += 1

            if model.ground:
                self.render_ground()

            if hasattr(self, "complete_setup"):
                self.complete_setup()

        def _get_new_color(self):
            return tab10_color_map(self.instance_count)

        def render(self, state: warp.sim.State):
            if self.skip_rendering:
                return

            if self.model.particle_count:
                particle_q = state.particle_q.numpy()

                # render particles
                self.render_points("particles", particle_q, radius=self.model.particle_radius.numpy())

                # render tris
                if self.model.tri_count:
                    self.render_mesh("surface", particle_q, self.model.tri_indices.numpy().flatten())

                # render springs
                if self.model.spring_count:
                    self.render_line_list("springs", particle_q, self.model.spring_indices.numpy().flatten(), [], 0.05)

            # render muscles
            if self.model.muscle_count:
                body_q = state.body_q.numpy()

                muscle_start = self.model.muscle_start.numpy()
                muscle_links = self.model.muscle_bodies.numpy()
                muscle_points = self.model.muscle_points.numpy()
                muscle_activation = self.model.muscle_activation.numpy()

                # for s in self.skeletons:

                #     # for mesh, link in s.mesh_map.items():

                #     #     if link != -1:
                #     #         X_sc = wp.transform_expand(self.state.body_X_sc[link].tolist())

                #     #         #self.renderer.add_mesh(mesh, "../assets/snu/OBJ/" + mesh + ".usd", X_sc, 1.0, self.render_time)
                #     #         self.renderer.add_mesh(mesh, "../assets/snu/OBJ/" + mesh + ".usd", X_sc, 1.0, self.render_time)

                for m in range(self.model.muscle_count):
                    start = int(muscle_start[m])
                    end = int(muscle_start[m + 1])

                    points = []

                    for w in range(start, end):
                        link = muscle_links[w]
                        point = muscle_points[w]

                        X_sc = wp.transform_expand(body_q[link][0])

                        points.append(wp.transform_point(X_sc, point).tolist())

                    self.render_line_strip(
                        name=f"muscle_{m}", vertices=points, radius=0.0075, color=(muscle_activation[m], 0.2, 0.5)
                    )

            # update bodies
            if self.model.body_count:
                self.update_body_transforms(state.body_q)

                if self.show_rigid_contact_points and self.model.rigid_contact_max:
                    wp.launch(
                        kernel=compute_contact_points,
                        dim=self.model.rigid_contact_max,
                        inputs=[
                            state.body_q,
                            self.model.shape_body,
                            self.model.rigid_contact_shape0,
                            self.model.rigid_contact_shape1,
                            self.model.rigid_contact_point0,
                            self.model.rigid_contact_point1,
                        ],
                        outputs=[
                            self.contact_points0,
                            self.contact_points1,
                        ],
                        device=self.model.device,
                    )

                    self.render_points(
                        "contact_points0",
                        self.contact_points0.numpy(),
                        radius=self.contact_points_radius * self.scaling,
                        colors=self.contact_points0_colors,
                    )
                    self.render_points(
                        "contact_points1",
                        self.contact_points1.numpy(),
                        radius=self.contact_points_radius * self.scaling,
                        colors=self.contact_points1_colors,
                    )

    return SimRenderer


SimRendererUsd = CreateSimRenderer(wp.render.UsdRenderer)
SimRendererOpenGL = CreateSimRenderer(wp.render.OpenGLRenderer)
SimRenderer = SimRendererUsd
