import math
import torch
import torch.nn as nn
import numpy as np
from typing import Optional, List, Tuple

from .transformers import Attention
from utils.custom_op import FakeLinear

class Mtp(torch.nn.Module):
    def __init__(self, mtp, base):
        super().__init__()
        self.model_type = base.model_type
        self.mtp = mtp
        self.embed_ = base.embed
        self.lm_ = base.lm

        self.config_ = base.config
        if not hasattr(base.config, 'head_dim'):
            self.config_.head_dim = base.head_dim
        self.config_.rotary = base.rotary
        self.config_.model_type = base.model_type
        self.config_.model_map = base.model_map
        self.hidden_size = base.hidden_size
        self.past_kv_shape = base.past_kv_shape
        self.num_attention_heads = base.num_attention_heads
        self.llm_config = base.llm_config
        self.load()
        self.unloaded_ops = {}


    @staticmethod
    def get_mtp(model_type):
        mtps = {
            'mimo': MimoMtp,
            'poi_qwen2_mtp' : PoiQwenMtp,
        }
        if model_type in mtps:
            return mtps[model_type]
        return None

    def export(self, onnx_path):
        onnx_model = f'{onnx_path}/mtp.onnx'

        # unload linear weight to save export memory
        self.unload_param()

        self.seq_len = 3
        input_ids = torch.arange(3, dtype=torch.long)
        attention_mask =  (1 - torch.tril(torch.ones([1, 1, self.seq_len, self.seq_len]))) * torch.finfo(torch.float32).min
        position_ids = torch.arange(self.seq_len, dtype=torch.int).unsqueeze(0)
        hidden_states = torch.ones([self.seq_len, 1, self.hidden_size], dtype=torch.float)

        # For export onnx, don't need image or audio's embedding
        input_embed = self.embed_(input_ids)
        past_key_values = torch.zeros(self.past_kv_shape[1:])
        logits_index = torch.tensor([-1], dtype=torch.int32)
        # export to onnx
        with torch.no_grad():
            torch.onnx.export(
                self, (input_embed, hidden_states, attention_mask, position_ids, past_key_values, logits_index),
                onnx_model,
                input_names=[
                    'input_embed', 'hidden_states',
                    'attention_mask', 'position_ids', 
                    'past_key_values', 'logits_index'
                ],
                output_names=['logits', 'presents'],
                dynamic_axes={
                    "input_embed" : { 0: "seq_len" },
                    "hidden_states" : { 0: "seq_len" },
                    "attention_mask" : { 2: "seq_len", 3: "seq_len" },
                    "position_ids" : { 1: "seq_len" },
                    "past_key_values" : { 2: "history_len" }
                    },
                do_constant_folding=True,
                verbose=False,
                opset_version=15)
        return onnx_model

    def load(self):
        raise NotImplementedError

    def forward(self, images):
        raise NotImplementedError


class MimoMtp(Mtp):
    def __init__(self, mtp, base):
        super().__init__(mtp, base)

    def load(self):
        self.mtp.eval()
        self.token_layernorm = getattr(self.mtp[0], 'token_layernorm')
        self.hidden_layernorm = getattr(self.mtp[0], 'hidden_layernorm')
        self.input_proj = getattr(self.mtp[0], 'input_proj')
        self.input_layernorm = getattr(self.mtp[0], 'input_layernorm')
        self.self_attn = getattr(self.mtp[0], 'self_attn')
        self.post_attention_layernorm = getattr(self.mtp[0], 'post_attention_layernorm')
        self.mlp = getattr(self.mtp[0], 'mlp')
        self.final_layernorm = getattr(self.mtp[0], 'final_layernorm')
        self.self_attn = Attention(self.self_attn, 0, self.config_)

    def unload_param(self):
        def build_faker(real, name):
            faker = FakeLinear(real.in_features, real.out_features, real.bias is not None, name)
            self.unloaded_ops[name] = real
            return faker
        # replace linear with fakelinear to save export memory and time
        with torch.no_grad():
            # different kv cache shape in different layers
            if isinstance(self.num_attention_heads, list):
                self.self_attn.export_fused_attn = True
            for name, child in self.self_attn.named_children():
                if isinstance(child, torch.nn.Linear):
                    setattr(self.self_attn, name, build_faker(child, f'/mtp_layers.0/self_attn/{name}/Linear'))
            for name, child in self.mlp.named_children():
                if isinstance(child, torch.nn.Linear):
                    setattr(self.mlp, name, build_faker(child, f'/mtp_layers.0/mlp/{name}/Linear'))
            self.input_proj = build_faker(self.input_proj, f'/mtp/input_proj/Linear')

    def forward(self,
                input_embeds: torch.Tensor,
                hidden_states: torch.Tensor,
                attention_mask: torch.Tensor,
                position_ids: torch.Tensor,
                past_key_values: Optional[Tuple[torch.Tensor]] = None,
                logits_index: int = -1
                ):
        input_embeds = input_embeds.view(1, -1, self.hidden_size)
        hidden_states = hidden_states.view(1, -1, self.hidden_size)
        hidden_states = hidden_states[:, 0 : input_embeds.size(1), :]

        input_embeds = self.token_layernorm(input_embeds)
        previous_hidden_states = self.hidden_layernorm(hidden_states)
        hidden_states = self.input_proj(torch.cat([previous_hidden_states, input_embeds], dim=-1))
        residual = hidden_states
        hidden_states = self.input_layernorm(hidden_states)

        rotary_pos_emb = self.config_.rotary(position_ids)

        # Self Attention
        hidden_states, present_key_value = self.self_attn(
            hidden_states=hidden_states,
            rotary_pos_emb=rotary_pos_emb,
            attention_mask=attention_mask,
            past_key_value=past_key_values,
            cross_attention_states=None,
        )

        hidden_states = residual + hidden_states
        residual = hidden_states
        hidden_states = self.post_attention_layernorm(hidden_states)
        hidden_states = self.mlp(hidden_states)
        hidden_states = residual + hidden_states

        hidden_states = hidden_states[:, logits_index:, :]
        hidden_states = self.final_layernorm(hidden_states)
        
        logits = self.lm_(hidden_states)
        return logits, present_key_value
    
class PoiQwenMtp(Mtp):
    def __init__(self, mtp, base):
        self.num_mtp_layers = 2
        super().__init__(mtp, base)

    def load(self):
        self.mtp[0].eval()
        self.mtp[1].eval()
        self.decode_layers = nn.ModuleList([])
        self.hidden_norm = nn.ModuleList([])
        self.last_norm = nn.ModuleList([])

        with torch.no_grad():
            for i in range(self.num_mtp_layers):
                self.decode_layers.append(getattr(self.mtp[i], 'layers'))
                self.hidden_norm.append(getattr(self.mtp[i], 'RMSorm_MTP_1'))
                self.last_norm.append(getattr(self.mtp[i], 'norm'))

        self.input_layernorm = nn.ModuleList([])
        self.post_attention_layernorm = nn.ModuleList([])
        self.mlp = nn.ModuleList([])
        self.self_attn = nn.ModuleList([])

        with torch.no_grad():
            for i in range(self.num_mtp_layers):
                self.input_layernorm.append(getattr(self.decode_layers[i], 'input_layernorm'))
                self.ori_attn = getattr(self.decode_layers[i], 'self_attn')
                self.post_attention_layernorm.append(getattr(self.decode_layers[i], 'post_attention_layernorm'))
                self.mlp.append(getattr(self.decode_layers[i], 'mlp'))
                self.self_attn.append(Attention(self.ori_attn, i, self.config_))

    def unload_param(self):
        def build_faker(real, name):
            faker = FakeLinear(real.in_features, real.out_features, real.bias is not None, name)
            self.unloaded_ops[name] = real
            return faker
        # replace linear with fakelinear to save export memory and time
        with torch.no_grad():
            for i in range(self.num_mtp_layers):
                # different kv cache shape in different layers
                if isinstance(self.num_attention_heads, list):
                    self.self_attn[i].export_fused_attn = True
                for name, child in self.self_attn[i].named_children():
                    if isinstance(child, torch.nn.Linear):
                        setattr(self.self_attn[i], name, build_faker(child, f'/mtp_layers.{i}/self_attn/{name}/Linear'))
                for name, child in self.mlp[i].named_children():
                    if isinstance(child, torch.nn.Linear):
                        setattr(self.mlp[i], name, build_faker(child, f'/mtp_layers.{i}/mlp/{name}/Linear'))

    def forward(self,
                input_embeds: torch.Tensor,
                hidden_states: torch.Tensor,
                attention_mask: torch.Tensor,
                position_ids: torch.Tensor,
                past_key_values: Optional[Tuple[torch.Tensor]] = None,
                logits_index: int = -1
                ):
        present_key_value = []
        # [1, -1, self.hidden_size]
        mtp_hidden_states = []

        rotary_pos_emb = self.config_.rotary(position_ids)
        hidden_states = hidden_states.view(1, -1, self.hidden_size)
        hidden_states = hidden_states[:, 0 : input_embeds.size(0), :]

        for i in range(self.num_mtp_layers):
            # first norm
            hidden_states = self.hidden_norm[i](hidden_states)

            # Decoder Layer
            residual = hidden_states
            hidden_states = self.input_layernorm[i](hidden_states)

            # Self Attention
            hidden_states, kv = self.self_attn[i](
                hidden_states=hidden_states,
                rotary_pos_emb=rotary_pos_emb,
                attention_mask=attention_mask,
                past_key_value=past_key_values,
                cross_attention_states=None,
            )
            present_key_value.append(kv)

            hidden_states = residual + hidden_states
            residual = hidden_states
            hidden_states = self.post_attention_layernorm[i](hidden_states)
            hidden_states = self.mlp[i](hidden_states)
            hidden_states = residual + hidden_states

            # last norm
            hidden_states = self.last_norm[i](hidden_states)

            mtp_hidden_states.append(hidden_states)
            hidden_states = mtp_hidden_states[i]

        for i in range(self.num_mtp_layers):
            mtp_hidden_states[i] = mtp_hidden_states[i][:, logits_index:, :]

        mtp_logits = self.lm_(mtp_hidden_states[0])
        for i in range(self.num_mtp_layers-1):
            logits = self.lm_(mtp_hidden_states[i+1])
            mtp_logits = torch.cat([mtp_logits, logits], dim=0)
        return mtp_logits, present_key_value