# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

import copy
import os
import torch
from torch import nn
from typing import Optional


def export_policy_as_jit(policy: object, path: str, normalizer: Optional[object] = None, filename="policy.pt"):
    """Export policy into a Torch JIT file.

    Args:
        policy: The policy torch module.
        normalizer: The empirical normalizer module. If None, Identity is used.
        path: The path to the saving directory.
        filename: The name of exported JIT file. Defaults to "policy.pt".
    """
    policy_exporter = _TorchPolicyExporter(policy, normalizer)
    policy_exporter.export(path, filename)


def export_policy_as_onnx(
    policy: object, path: str, normalizer: Optional[object] = None, filename="policy.onnx", verbose=False
):
    """Export policy into a Torch ONNX file.

    Args:
        policy: The policy torch module.
        normalizer: The empirical normalizer module. If None, Identity is used.
        path: The path to the saving directory.
        filename: The name of exported ONNX file. Defaults to "policy.onnx".
        verbose: Whether to print the model summary. Defaults to False.
    """
    if not os.path.exists(path):
        os.makedirs(path, exist_ok=True)
    policy_exporter = _OnnxPolicyExporter(policy, normalizer, verbose)
    policy_exporter.export(path, filename)



def export_policy_as_pkl(
    policy: nn.Module, path: str, filename="policy.pkl"
):
    """Export policy into a Torch pkl file.

    Args:
        policy: The policy torch module.
        normalizer: The empirical normalizer module. If None, Identity is used.
        path: The path to the saving directory.
        filename: The name of exported pkl file. Defaults to "policy.pkl".
    """
    if not os.path.exists(path):
        os.makedirs(path, exist_ok=True)
    model_dict = policy.state_dict()
    torch.save(model_dict, os.path.join(path, filename))


"""
Helper Classes - Private.
"""


class _TorchPolicyExporter(torch.nn.Module):
    """Exporter of actor-critic into JIT file."""

    def __init__(self, policy, normalizer=None):
        super().__init__()
        self.is_recurrent = policy.is_recurrent
        # copy policy parameters
        if hasattr(policy, "student_encoder"):
            self.student_encoder = copy.deepcopy(policy.student_encoder).cpu()
            self.history = torch.zeros([1, policy.history.shape[1], policy.history.shape[2]], device='cpu')
            self.forward = self.forward_cts
        if hasattr(policy, "student_moe_encoder"):
            self.student_moe_encoder = copy.deepcopy(policy.student_moe_encoder).cpu()
            if hasattr(policy, "obs_no_goal_mask"):
                self.obs_no_goal_mask = copy.deepcopy(policy.obs_no_goal_mask).cpu()
            self.history_length = policy.history.shape[1]
            self.history = torch.zeros([1, policy.history.shape[1], policy.history.shape[2]], device='cpu')
            self.forward = self.forward_moe_no_goal_cts
            if not hasattr(policy, "obs_no_goal_mask"):
                self.forward = self.forward_moe_cts
        if hasattr(policy, "actor_mcp"):
            self.actor = copy.deepcopy(policy.actor_mcp)
            self.obs_no_goal_mask = copy.deepcopy(policy.obs_no_goal_mask).cpu()
            self.forward = self.forward_mcp_cts
        elif hasattr(policy, "actor_moe"):
            self.actor = copy.deepcopy(policy.actor_moe)
            self.forward = self.forward_ac_moe
        elif hasattr(policy, "actor"):
            self.actor = copy.deepcopy(policy.actor)
            if self.is_recurrent:
                self.rnn = copy.deepcopy(policy.memory_a.rnn)
        elif hasattr(policy, "student"):
            self.actor = copy.deepcopy(policy.student)
            if self.is_recurrent:
                self.rnn = copy.deepcopy(policy.memory_s.rnn)
        else:
            raise ValueError("Policy does not have an actor/student module.")
        if hasattr(policy, "student_moe_encoder") and hasattr(policy, "actor_moe"):
            self.forward = self.forward_dual_moe_cts
        # set up recurrent network
        if self.is_recurrent:
            self.rnn.cpu()
            self.register_buffer("hidden_state", torch.zeros(self.rnn.num_layers, 1, self.rnn.hidden_size))
            self.register_buffer("cell_state", torch.zeros(self.rnn.num_layers, 1, self.rnn.hidden_size))
            self.forward = self.forward_lstm
            self.reset = self.reset_memory
        # copy normalizer if exists
        if normalizer:
            self.normalizer = copy.deepcopy(normalizer)
        else:
            self.normalizer = torch.nn.Identity()

    def forward_lstm(self, x):
        x = self.normalizer(x)
        x, (h, c) = self.rnn(x.unsqueeze(0), (self.hidden_state, self.cell_state))
        self.hidden_state[:] = h
        self.cell_state[:] = c
        x = x.squeeze(0)
        return self.actor(x)

    def forward(self, x):
        return self.actor(self.normalizer(x))
    
    def forward_cts(self, x):  # x is single observations
        x = self.normalizer(x)
        self.history = torch.cat([self.history[:, 1:], x.unsqueeze(1)], dim=1)
        latent = self.student_encoder(self.history.flatten(1))
        x = torch.cat([latent, x], dim=1)
        return self.actor(x), (None, latent)
    
    def forward_moe_no_goal_cts(self, x):  # x is single observations
        x = self.normalizer(x)
        self.history = torch.cat([self.history[:, 1:], x.unsqueeze(1)], dim=1)
        history_no_goal = self.history.reshape(1, self.history_length, -1)[:, :, self.obs_no_goal_mask].reshape(1, -1)
        latent, weights = self.student_moe_encoder(self.history.flatten(1), history_no_goal)
        x = torch.cat([latent, x], dim=1)
        return self.actor(x), (weights, latent)

    def forward_moe_cts(self, x):  # x is single observations
        x = self.normalizer(x)
        self.history = torch.cat([self.history[:, 1:], x.unsqueeze(1)], dim=1)
        latent, weights = self.student_moe_encoder(self.history.flatten(1))
        x = torch.cat([latent, x], dim=1)
        return self.actor(x), (weights, latent)
    
    def forward_mcp_cts(self, x):  # x is single observations
        x = self.normalizer(x)
        self.history = torch.cat([self.history[:, 1:], x.unsqueeze(1)], dim=1)
        x_no_goal = x[:, self.obs_no_goal_mask]
        latent = self.student_encoder(self.history.flatten(1))
        x = torch.cat([latent, x], dim=1)
        x_no_goal = torch.cat([latent, x_no_goal], dim=1)
        mean_action, _, weights = self.actor(x, x_no_goal)
        return mean_action, (weights, latent)

    def forward_ac_moe(self, x):  # x is single observations
        x = self.normalizer(x)
        self.history = torch.cat([self.history[:, 1:], x.unsqueeze(1)], dim=1)
        latent = self.student_encoder(self.history.flatten(1))
        x = torch.cat([latent, x], dim=1)
        mean, weights = self.actor(x)
        return mean, (weights, latent)

    def forward_dual_moe_cts(self, x):  # x is single observations
        x = self.normalizer(x)
        self.history = torch.cat([self.history[:, 1:], x.unsqueeze(1)], dim=1)
        latent, student_weights = self.student_moe_encoder(self.history.flatten(1))
        x = torch.cat([latent, x], dim=1)
        mean, actor_weights = self.actor(x)
        return mean, (student_weights, actor_weights, latent)

    @torch.jit.export
    def reset(self):
        if hasattr(self, 'history'):
            self.history = torch.zeros_like(self.history)

    def reset_memory(self):
        self.hidden_state[:] = 0.0
        self.cell_state[:] = 0.0

    def export(self, path, filename):
        os.makedirs(path, exist_ok=True)
        path = os.path.join(path, filename)
        self.to("cpu")
        traced_script_module = torch.jit.script(self)
        traced_script_module.save(path)


class _OnnxPolicyExporter(torch.nn.Module):
    """Exporter of actor-critic into ONNX file."""

    def __init__(self, policy, normalizer=None, verbose=False):
        super().__init__()
        self.verbose = verbose
        self.input_dim = None
        self.num_actions = 12
        self.normalizer = torch.nn.Identity()
        
        # copy policy parameters
        if hasattr(policy, 'student_encoder'):
            self.student_encoder = copy.deepcopy(policy.student_encoder)
            self.forward = self.forward_cts
            self.input_dim = self.student_encoder[0].in_features
            
        elif hasattr(policy, "student_moe_encoder"):
            self.student_moe_encoder = copy.deepcopy(policy.student_moe_encoder)
            self.history_length = policy.history.shape[1]
            self.forward = self.forward_moe_no_goal_cts
            self.input_dim = self.history_length * policy.history.shape[2]
            if hasattr(policy, "obs_no_goal_mask"):
                self.obs_no_goal_mask = copy.deepcopy(policy.obs_no_goal_mask).cpu()
            else:
                self.forward = self.forward_moe_cts
        
        else:  # PPO
            self.forward = self.forward_ppo
            
        if hasattr(policy, "actor"):
            self.actor = copy.deepcopy(policy.actor)
            if hasattr(self, 'is_recurrent') and self.is_recurrent:
                self.rnn = copy.deepcopy(policy.memory_a.rnn)
            if self.input_dim is None:
                 self.input_dim = self.actor[0].in_features
        elif hasattr(policy, "actor_mcp"):
            self.actor = copy.deepcopy(policy.actor_mcp)
            self.obs_no_goal_mask = copy.deepcopy(policy.obs_no_goal_mask).cpu()
            self.history_length = policy.history.shape[1]
            self.forward = self.forward_mcp_cts 
        else:
            raise ValueError("Policy does not have an actor/student module.")

    def flatten_obs(self, x):  # flatten stack obs by terms to stack by step frames
        term_dims = [3, 3, 3, self.num_actions, self.num_actions, self.num_actions]
        obs_dim = sum(term_dims)
        if x.shape[1] % obs_dim != 0:
            raise ValueError(f"x.shape[1] ({x.shape[1]}) 不是 obs_dim ({obs_dim}) 的整数倍")
            
        frames = x.shape[1] // obs_dim
        split_sizes = [dim * frames for dim in term_dims]
        # [B, dim0*frames], [B, dim1*frames], ...
        term_chunks = torch.split(x, split_sizes, dim=1)

        # [ [B, frames, dim0], [B, frames, dim1], ... ]
        frame_terms_reshaped = [
            chunk.view(-1, frames, dim) 
            for chunk, dim in zip(term_chunks, term_dims)
        ]

        history_by_frame = []
        for i in range(frames):
            # [ [B, dim0], [B, dim1], ... ]
            terms_for_this_frame = [ftr[:, i, :] for ftr in frame_terms_reshaped]
            history_by_frame.append(torch.cat(terms_for_this_frame, dim=1))
        # [B, (Frame0_AllTerms), (Frame1_AllTerms), ...]
        history = torch.cat(history_by_frame, dim=1)
        return history, obs_dim
    
    def forward_ppo(self, x):  # x is stack observations by terms
        x = self.normalizer(x)
        history, obs_dim = self.flatten_obs(x)
        last_obs = history[:, -obs_dim:]
        return self.actor(last_obs)

    def forward_cts(self, x):  # x is stack observations by terms
        x = self.normalizer(x)
        history, obs_dim = self.flatten_obs(x)

        last_obs = history[:, -obs_dim:]
        latent = self.student_encoder(history)
        x = torch.cat([latent, last_obs], dim=1)
        
        return self.actor(x)

    def forward_moe_no_goal_cts(self, x):
        x = self.normalizer(x)
        history, obs_dim = self.flatten_obs(x)

        last_obs = history[:, -obs_dim:]
        history_3d = history.view(-1, self.history_length, obs_dim)
        history_no_goal = history_3d[:, :, self.obs_no_goal_mask].reshape(x.shape[0], -1)

        latent, weights = self.student_moe_encoder(history, history_no_goal)
        x = torch.cat([latent, last_obs], dim=1)

        return self.actor(x), weights, latent

    def forward_moe_cts(self, x):
        x = self.normalizer(x)
        history, obs_dim = self.flatten_obs(x)

        last_obs = history[:, -obs_dim:]

        latent, weights = self.student_moe_encoder(history)
        x = torch.cat([latent, last_obs], dim=1)

        return self.actor(x), weights, latent
    
    def forward_mcp_cts(self, x):
        x = self.normalizer(x)
        history, obs_dim = self.flatten_obs(x)

        last_obs = history[:, -obs_dim:]
        obs_no_goal = last_obs[:, self.obs_no_goal_mask]
        latent = self.student_encoder(history)
        x_in = torch.cat([latent, last_obs], dim=1)
        x_no_goal_in = torch.cat([latent, obs_no_goal], dim=1)
        
        mean_action, _, weights = self.actor(x_in, x_no_goal_in)
        return mean_action, weights

    def export(self, path, filename):
        self.to("cpu")
        obs = torch.zeros(1, self.input_dim)
        
        output_names = ["actions"]
        if self.forward == self.forward_moe_no_goal_cts:
            output_names.append("weights")
            output_names.append("latent")
        if self.forward == self.forward_mcp_cts:
            output_names.append("weights")

        torch.onnx.export(
            self,
            obs,
            os.path.join(path, filename),
            export_params=True,
            opset_version=11,
            verbose=self.verbose,
            input_names=["obs"],
            output_names=output_names,
            dynamic_axes={},
        )
