import os
os.environ["TOKENIZERS_PARALLELISM"] = "false"
import math
from typing import Iterable, Optional, Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import (
    CLIPTextModel,
    CLIPTokenizer,
)
from typing import *

from .util import timestep_embedding
from .full_attn import scaled_dot_product_attention

def init_linear(l, stddev):
    nn.init.normal_(l.weight, std=stddev)
    if l.bias is not None:
        nn.init.constant_(l.bias, 0.0)

class MLP(nn.Module):
    def __init__(self, width, init_scale=0.25):
        super().__init__()
        self.width = width
        self.c_fc = nn.Linear(width, width * 4)
        self.c_proj = nn.Linear(width * 4, width)
        self.gelu = nn.GELU()
        init_linear(self.c_fc, init_scale)
        init_linear(self.c_proj, init_scale)

    def forward(self, x):
        return self.c_proj(self.gelu(self.c_fc(x)))



class AdaLayerNormZero(nn.Module):
    r"""
    Norm layer adaptive layer norm zero (adaLN-Zero).

    Parameters:
        embedding_dim (`int`): The size of each embedding vector.
        num_embeddings (`int`): The size of the embeddings dictionary.
    """

    def __init__(self, embedding_dim, norm_type="layer_norm", bias=True):
        super().__init__()
        
        self.silu = nn.SiLU()
        self.linear = nn.Linear(embedding_dim, 6 * embedding_dim, bias=bias)

        self.norm = nn.LayerNorm(embedding_dim, elementwise_affine=False, eps=1e-6)

    def forward(self, x, emb=None):
        emb = self.linear(self.silu(emb))
        shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = emb.chunk(6, dim=1)
        x = self.norm(x) * (1 + scale_msa[:, None]) + shift_msa[:, None]
        return x, gate_msa, shift_mlp, scale_mlp, gate_mlp

class MultiHeadRMSNorm(nn.Module):
    def __init__(self, dim: int, heads: int):
        super().__init__()
        self.scale = dim ** 0.5
        self.gamma = nn.Parameter(torch.ones(heads, dim))

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return (F.normalize(x.float(), dim = -1) * self.gamma * self.scale).to(x.dtype)

class MultiHeadAttention(nn.Module):
    def __init__(
        self,
        channels: int,
        num_heads: int,
        ctx_channels: Optional[int]=None,
        type: Literal["self", "cross"] = "self",
        attn_mode: Literal["full", "windowed"] = "full",
        window_size: Optional[int] = None,
        shift_window: Optional[Tuple[int, int, int]] = None,
        qkv_bias: bool = True,
        use_rope: bool = False,
        qk_rms_norm: bool = False,
    ):
        super().__init__()
        assert channels % num_heads == 0
        assert type in ["self", "cross"], f"Invalid attention type: {type}"
        assert attn_mode in ["full", "windowed"], f"Invalid attention mode: {attn_mode}"
        assert type == "self" or attn_mode == "full", "Cross-attention only supports full attention"
        
        if attn_mode == "windowed":
            raise NotImplementedError("Windowed attention is not yet implemented")
        
        self.channels = channels
        self.head_dim = channels // num_heads
        self.ctx_channels = ctx_channels if ctx_channels is not None else channels
        self.num_heads = num_heads
        self._type = type
        self.attn_mode = attn_mode
        self.window_size = window_size
        self.shift_window = shift_window
        self.use_rope = use_rope
        self.qk_rms_norm = qk_rms_norm

        if self._type == "self":
            self.to_qkv = nn.Linear(channels, channels * 3, bias=qkv_bias)
        else:
            self.to_q = nn.Linear(channels, channels, bias=qkv_bias)
            self.to_kv = nn.Linear(self.ctx_channels, channels * 2, bias=qkv_bias)
            
        if self.qk_rms_norm:
            self.q_rms_norm = MultiHeadRMSNorm(self.head_dim, num_heads)
            self.k_rms_norm = MultiHeadRMSNorm(self.head_dim, num_heads)
            
        self.to_out = nn.Linear(channels, channels)

        if use_rope:
            self.rope = RotaryPositionEmbedder(channels)
    
    def forward(self, x: torch.Tensor, context: Optional[torch.Tensor] = None, indices: Optional[torch.Tensor] = None) -> torch.Tensor:
        B, L, C = x.shape
        if self._type == "self":
            qkv = self.to_qkv(x)
            qkv = qkv.reshape(B, L, 3, self.num_heads, -1)
            if self.use_rope:
                q, k, v = qkv.unbind(dim=2)
                q, k = self.rope(q, k, indices)
                qkv = torch.stack([q, k, v], dim=2)
            if self.attn_mode == "full":
                if self.qk_rms_norm:
                    q, k, v = qkv.unbind(dim=2)
                    q = self.q_rms_norm(q)
                    k = self.k_rms_norm(k)
                    h = scaled_dot_product_attention(q, k, v)
                else:
                    h = scaled_dot_product_attention(qkv)
            elif self.attn_mode == "windowed":
                raise NotImplementedError("Windowed attention is not yet implemented")
        else:
            Lkv = context.shape[1]
            q = self.to_q(x)
            kv = self.to_kv(context)
            q = q.reshape(B, L, self.num_heads, -1)
            kv = kv.reshape(B, Lkv, 2, self.num_heads, -1)
            if self.qk_rms_norm:
                q = self.q_rms_norm(q)
                k, v = kv.unbind(dim=2)
                k = self.k_rms_norm(k)
                h = scaled_dot_product_attention(q, k, v)
            else:
                h = scaled_dot_product_attention(q, kv)
        h = h.reshape(B, L, -1)
        h = self.to_out(h)
        return h



class CondAdaLayerNormZero(nn.Module):
    r"""
    Norm layer adaptive layer norm zero (adaLN-Zero).

    Parameters:
        embedding_dim (`int`): The size of each embedding vector.
        num_embeddings (`int`): The size of the embeddings dictionary.
    """

    def __init__(self, embedding_dim, norm_type="layer_norm", bias=True):
        super().__init__()
        
        self.silu = nn.SiLU()
        self.linear = nn.Linear(embedding_dim, 2 * embedding_dim, bias=bias)

        self.norm = nn.LayerNorm(embedding_dim, elementwise_affine=False, eps=1e-6)

    def forward(self, x, emb=None):
        emb = self.linear(self.silu(emb))
        shift_msa, scale_msa = emb.chunk(2, dim=1)
        x = self.norm(x) * (1 + scale_msa[:, None]) + shift_msa[:, None]
        return x

class CogXAttentionBlock(nn.Module):
    def __init__(
        self,
        width,
        heads,
        init_scale=1.0,
    ):
        super().__init__()


        self.attn_self = MultiHeadAttention(
            width,
            num_heads=heads,
            type="self",
            attn_mode="full",
            window_size=None,
            shift_window=None,
            qkv_bias=True,
            use_rope=False,
            qk_rms_norm=True,
        )
        
        self.text_cross_attn = MultiHeadAttention(
            width,
            ctx_channels=width,
            num_heads=heads,
            type="cross",
            attn_mode="full",
            qkv_bias=True,
            qk_rms_norm=True,
        )

        
        self.rest_cross_attn = MultiHeadAttention(
            width,
            ctx_channels=width,
            num_heads=heads,
            type="cross",
            attn_mode="full",
            qkv_bias=True,
            qk_rms_norm=True,
        )

        

        self.mlp = MLP(width=width, init_scale=init_scale)
        
        self.adaln_x = AdaLayerNormZero(width)
        self.adaln_rest = CondAdaLayerNormZero(width)
        self.adaln_text = CondAdaLayerNormZero(width)

        self.ln_x1 = nn.LayerNorm(width)
        self.ln_x2 = nn.LayerNorm(width)
        self.ln_x3 = nn.LayerNorm(width)
 

    def forward(self, x,rest_emb, text_emb, t_emb):

        # adaln for x layenorma+adpative
        norm_x, gate_msa_x, shift_mlp_x, scale_mlp_x, gate_mlp_x = self.adaln_x(x, emb=t_emb)
        
        # adaln for text
        norm_text  = self.adaln_text(text_emb, emb=t_emb)
        
        # adaln for rest
        norm_rest  = self.adaln_rest(rest_emb, emb=t_emb)

        # self attention 
        h = self.attn_self(norm_x) 
        h = h * gate_msa_x.unsqueeze(1) 
        x = x + h

        # Text cross attention
        h = self.ln_x1(x)
        h = self.text_cross_attn(h,norm_text)
        x = x + h

        # Rest cross attention 
        h = self.ln_x2(x)
        h = self.rest_cross_attn(h,norm_rest)
        x = x + h

        # feedforward by MLP
        h = self.ln_x3(x)
        h = h * (1 + scale_mlp_x[:, None]) + shift_mlp_x[:, None]
        h = self.mlp(h)
        h = gate_mlp_x[:, None] * h


        x = x + h

        return x


class Transformer_cogx(nn.Module):
    def __init__(
        self,
        width,
        layers,
        heads,
        init_scale=0.25,
    ):
        super().__init__()
        self.width = width
        self.layers = layers
        init_scale = init_scale * math.sqrt(1.0 / width)
        self.resblocks = nn.ModuleList(
            [
                CogXAttentionBlock(
                    width=width,
                    heads=heads,
                    init_scale=init_scale,
                )
                for _ in range(layers)
            ]
        )

    def forward(self, x: torch.Tensor,rest_emb: torch.Tensor, text_emb: torch.Tensor, t_emb: torch.Tensor):
        for block in self.resblocks:
            x = block(x,rest_emb, text_emb, t_emb)
        return x


class BsplineVarianceDiT(nn.Module):
    def __init__(
        self,
        input_channels=64,
        output_channels=64,
        width=512,
        layers=12,
        heads=8,
        init_scale=0.25,
        cond_drop_prob=0.1,
        **kwargs,
    ):
        super().__init__()

        self.cond_drop_prob = cond_drop_prob
        
        self.time_embed = MLP(
            width=width, init_scale=init_scale * math.sqrt(1.0 / width)
        )
        self.backbone = Transformer_cogx(
            width=width,
            layers=layers,
            heads=heads,
            init_scale=init_scale,
        )

        self.ln_pre = nn.LayerNorm(width)
        self.ln_pre_text = nn.LayerNorm(width)
        self.ln_pre_rest = nn.LayerNorm(width)
        self.ln_post = nn.LayerNorm(width)
        
        # Please specify your local path to the stable-diffusion-v1-5 model
        model_path = "stable-diffusion-v1-5"

        self.clip_text_model = CLIPTextModel.from_pretrained(
            f"{model_path}/text_encoder", local_files_only=True,use_safetensors=True
        )
        self.tokenizer = CLIPTokenizer.from_pretrained(
            f"{model_path}/tokenizer", local_files_only=True
        )
        self.clip_token_mlp = nn.Linear(768, self.backbone.width)
        self.rest_shape_mlp = nn.Linear(int(input_channels)//2, self.backbone.width)
        self.input_channels = input_channels
        self.input_proj = nn.Linear(input_channels, width)
        self.output_proj = nn.Linear(width, output_channels)
        with torch.no_grad():
            self.output_proj.weight.zero_()
            self.output_proj.bias.zero_()
        
        for param in self.clip_text_model.parameters():
            param.requires_grad = False

    def _forward_cogx(self, x,rest_embed, text_emb, t_emb):
        
        # conver to 512
        h = self.input_proj(x)    
        h = self.ln_pre(h)
        rest_embed = self.ln_pre_rest(rest_embed)
        text_emb = self.ln_pre_text(text_emb)
        
        # attention 
        h = self.backbone(h,rest_emb = rest_embed ,text_emb=text_emb, t_emb=t_emb)
        
        # project back
        h = self.ln_post(h)
        h = self.output_proj(h)

        return h

    def forward(self, x, t, texts=None,rest_mask=None):
        device = x.device

        # ---- Temporal embedding ----
        t_embed = self.time_embed(timestep_embedding(t, self.backbone.width))

        # ---- Canonical shape embedding ----
        rest_embed = x[:, :, : int(self.input_channels // 2)]
        rest_embed = self.rest_shape_mlp(rest_embed)


        # ---- Text encoding ----
        with torch.no_grad():
            text_inputs = self.tokenizer(
                texts,
                padding="max_length",
                truncation=True,
                return_tensors="pt",
            ).input_ids.to(self.clip_text_model.device)

            text_embed = self.clip_text_model(text_inputs)[0]  # [B, 77, 768]

        text_embed = self.clip_token_mlp(text_embed).to(device)
        
        #---- Determine which samples have text ----
        has_text = torch.tensor([text != '' for text in texts],
                                dtype=torch.bool, device=device)
                                
        if self.training:
            # Randomly drop conditioning for classifier-free guidance
            drop_mask_text = (torch.rand(len(x), device=device) < self.cond_drop_prob)  # True = drop
            keep_mask = has_text & ~drop_mask_text  # keep only if has_text and not dropped
            text_embed = text_embed * keep_mask[:, None, None]  # broadcast multiply
        else:
            text_embed = text_embed * has_text[:, None, None]

        if self.training:
            drop_mask_rest = (torch.rand(len(x), device=device) >= self.cond_drop_prob)
            rest_embed = rest_embed * drop_mask_rest[:, None, None]
        else:
            if rest_mask!=None:
                rest_embed = rest_embed * rest_mask[:, None, None]
                
        return self._forward_cogx(x, rest_embed, text_embed, t_embed)


