# SPDX-License-Identifier: Apache-2.0

import torch
import torch.nn as nn
import torch.nn.functional as F
from diffusers.models.embeddings import PixArtAlphaTextProjection, TimestepEmbedding

from sglang.multimodal_gen.configs.models.dits.sana import SanaConfig
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm
from sglang.multimodal_gen.runtime.layers.linear import MergedColumnParallelLinear
from sglang.multimodal_gen.runtime.layers.visual_embedding import Timesteps
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
    LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger

logger = init_logger(__name__)


def _mps_safe_linear(linear: nn.Linear, x: torch.Tensor) -> torch.Tensor:
    if x.device.type != "mps":
        return linear(x)

    return F.linear(
        x.to(torch.float32),
        linear.weight.to(torch.float32),
        None if linear.bias is None else linear.bias.to(torch.float32),
    ).to(x.dtype)


def _mps_safe_conv2d(conv: nn.Conv2d, x: torch.Tensor) -> torch.Tensor:
    if x.device.type != "mps":
        return conv(x)

    return F.conv2d(
        x.to(torch.float32),
        conv.weight.to(torch.float32),
        None if conv.bias is None else conv.bias.to(torch.float32),
        conv.stride,
        conv.padding,
        conv.dilation,
        conv.groups,
    ).to(x.dtype)


def _mps_match_dtype(tensor: torch.Tensor, ref: torch.Tensor) -> torch.Tensor:
    if ref.device.type == "mps" and tensor.dtype != ref.dtype:
        return tensor.to(dtype=ref.dtype)
    return tensor


class SanaCombinedTimestepSizeEmbeddings(nn.Module):
    def __init__(self, embedding_dim):
        super().__init__()
        self.time_proj = Timesteps(
            num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0
        )
        self.timestep_embedder = TimestepEmbedding(
            in_channels=256, time_embed_dim=embedding_dim
        )

    def forward(self, timestep, hidden_dtype=None):
        timesteps_proj = self.time_proj(timestep)
        if hidden_dtype is not None:
            timesteps_proj = timesteps_proj.to(dtype=hidden_dtype)
        if timesteps_proj.device.type == "mps":
            embedder = self.timestep_embedder
            timesteps_emb = _mps_safe_linear(embedder.linear_1, timesteps_proj)
            if embedder.act is not None:
                timesteps_emb = embedder.act(timesteps_emb)
            timesteps_emb = _mps_safe_linear(embedder.linear_2, timesteps_emb)
            if embedder.post_act is not None:
                timesteps_emb = embedder.post_act(timesteps_emb)
        else:
            timesteps_emb = self.timestep_embedder(timesteps_proj)
        return timesteps_emb


class SanaAdaLayerNormSingle(nn.Module):
    def __init__(self, embedding_dim):
        super().__init__()
        self.emb = SanaCombinedTimestepSizeEmbeddings(embedding_dim)
        self.silu = nn.SiLU()
        self.linear = nn.Linear(embedding_dim, 6 * embedding_dim, bias=True)

    def forward(self, timestep, hidden_dtype=None):
        embedded_timestep = self.emb(timestep, hidden_dtype=hidden_dtype)
        out = _mps_safe_linear(self.linear, self.silu(embedded_timestep))
        return out, embedded_timestep


class SanaModulatedNorm(nn.Module):
    def __init__(self, dim, eps=1e-6):
        super().__init__()
        self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)

    def forward(self, x, temb, scale_shift_table):
        x = self.norm(x)
        scale_shift_table = _mps_match_dtype(scale_shift_table, temb)
        shift, scale = (scale_shift_table[None] + temb[:, None]).chunk(2, dim=1)
        x = x * (1 + scale) + shift
        return x


class GLUMBConv(nn.Module):
    """Gated Linear Unit with Multi-Branch Convolution."""

    def __init__(self, in_channels, out_channels, expand_ratio=2.5):
        super().__init__()
        hidden_channels = int(expand_ratio * in_channels)
        self.nonlinearity = nn.SiLU()
        self.conv_inverted = nn.Conv2d(in_channels, hidden_channels * 2, 1, 1, 0)
        self.conv_depth = nn.Conv2d(
            hidden_channels * 2,
            hidden_channels * 2,
            3,
            1,
            1,
            groups=hidden_channels * 2,
        )
        self.conv_point = nn.Conv2d(hidden_channels, out_channels, 1, 1, 0, bias=False)

    def forward(self, hidden_states):
        hidden_states = _mps_safe_conv2d(self.conv_inverted, hidden_states)
        hidden_states = self.nonlinearity(hidden_states)
        hidden_states = _mps_safe_conv2d(self.conv_depth, hidden_states)
        hidden_states, gate = torch.chunk(hidden_states, 2, dim=1)
        hidden_states = hidden_states * self.nonlinearity(gate)
        hidden_states = _mps_safe_conv2d(self.conv_point, hidden_states)
        return hidden_states


class SanaLinearAttention(nn.Module):
    """Linear attention with O(N*D^2) complexity instead of O(N^2*D)."""

    def __init__(self, query_dim, num_heads, head_dim, bias=False):
        super().__init__()
        inner_dim = num_heads * head_dim
        self.num_heads = num_heads
        self.head_dim = head_dim

        self.inner_dim = inner_dim
        # Self-attention q/k/v share the same input -> one packed GEMM.
        self.to_qkv = MergedColumnParallelLinear(
            query_dim, [inner_dim, inner_dim, inner_dim], bias=bias, gather_output=True
        )
        self.to_out = nn.ModuleList(
            [nn.Linear(inner_dim, query_dim, bias=True), nn.Identity()]
        )

    def forward(self, hidden_states):
        B, S, _ = hidden_states.shape

        qkv, _ = self.to_qkv(hidden_states)
        query, key, value = qkv.split(
            [self.inner_dim, self.inner_dim, self.inner_dim], dim=-1
        )

        query = query.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
        key = key.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
        value = value.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
        query = F.relu(query)
        key = F.relu(key)

        kv = torch.matmul(key.transpose(-2, -1), value)  # (B, H, D, D)
        qkv = torch.matmul(query, kv)  # (B, H, S, D)

        key_sum = key.sum(dim=-2, keepdim=True)  # (B, H, 1, D)
        normalizer = torch.matmul(query, key_sum.transpose(-2, -1)).clamp(min=1e-6)
        hidden_states = qkv / normalizer

        hidden_states = hidden_states.transpose(1, 2).reshape(B, S, -1)
        hidden_states = _mps_safe_linear(self.to_out[0], hidden_states)
        return hidden_states


class SanaCrossAttention(nn.Module):
    def __init__(self, query_dim, cross_attention_dim, num_heads, head_dim, bias=False):
        super().__init__()
        inner_dim = num_heads * head_dim
        self.num_heads = num_heads
        self.head_dim = head_dim

        self.inner_dim = inner_dim
        self.to_q = nn.Linear(query_dim, inner_dim, bias=bias)
        # k/v share the (step-invariant) encoder input -> one packed GEMM.
        self.to_kv = MergedColumnParallelLinear(
            cross_attention_dim, [inner_dim, inner_dim], bias=bias, gather_output=True
        )
        self.to_out = nn.ModuleList(
            [nn.Linear(inner_dim, query_dim, bias=True), nn.Identity()]
        )

    def forward(
        self, hidden_states, encoder_hidden_states, encoder_attention_mask=None
    ):
        B, S, _ = hidden_states.shape
        T = encoder_hidden_states.shape[1]

        query = _mps_safe_linear(self.to_q, hidden_states)
        kv, _ = self.to_kv(encoder_hidden_states)
        key, value = kv.split([self.inner_dim, self.inner_dim], dim=-1)

        query = query.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
        key = key.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
        value = value.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)

        attn_mask = None
        if encoder_attention_mask is not None:
            attn_mask = encoder_attention_mask.bool()
            attn_mask = attn_mask[:, None, None, :].expand(B, self.num_heads, S, T)

        hidden_states = F.scaled_dot_product_attention(
            query, key, value, attn_mask=attn_mask
        )
        hidden_states = hidden_states.transpose(1, 2).reshape(B, S, -1)
        hidden_states = _mps_safe_linear(self.to_out[0], hidden_states)
        return hidden_states


class SanaTransformerBlock(nn.Module):
    def __init__(
        self,
        dim,
        num_attention_heads,
        attention_head_dim,
        num_cross_attention_heads,
        cross_attention_head_dim,
        cross_attention_dim,
        mlp_ratio,
        norm_eps,
        attention_bias=False,
    ):
        super().__init__()

        self.scale_shift_table = nn.Parameter(torch.randn(6, dim) / dim**0.5)

        self.norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=norm_eps)
        self.attn1 = SanaLinearAttention(
            query_dim=dim,
            num_heads=num_attention_heads,
            head_dim=attention_head_dim,
            bias=attention_bias,
        )

        self.norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=norm_eps)
        self.attn2 = SanaCrossAttention(
            query_dim=dim,
            cross_attention_dim=cross_attention_dim,
            num_heads=num_cross_attention_heads,
            head_dim=cross_attention_head_dim,
            bias=True,
        )

        self.ff = GLUMBConv(in_channels=dim, out_channels=dim, expand_ratio=mlp_ratio)

    def forward(
        self,
        hidden_states,
        encoder_hidden_states,
        timestep,
        height,
        width,
        encoder_attention_mask=None,
    ):
        batch_size = hidden_states.shape[0]

        scale_shift_table = _mps_match_dtype(self.scale_shift_table, timestep)
        shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
            scale_shift_table[None] + timestep.reshape(batch_size, 6, -1)
        ).chunk(6, dim=1)

        norm_hidden = self.norm1(hidden_states)
        norm_hidden = norm_hidden * (1 + scale_msa) + shift_msa
        attn_output = self.attn1(norm_hidden)
        hidden_states = hidden_states + gate_msa * attn_output

        attn_output = self.attn2(
            hidden_states, encoder_hidden_states, encoder_attention_mask
        )
        hidden_states = hidden_states + attn_output

        norm_hidden = self.norm2(hidden_states)
        norm_hidden = norm_hidden * (1 + scale_mlp) + shift_mlp
        norm_hidden = norm_hidden.unflatten(1, (height, width)).permute(0, 3, 1, 2)
        ff_output = self.ff(norm_hidden)
        ff_output = ff_output.flatten(2, 3).permute(0, 2, 1)
        hidden_states = hidden_states + gate_mlp * ff_output

        return hidden_states


class SanaTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):

    _fsdp_shard_conditions = [
        lambda n, m: isinstance(m, SanaTransformerBlock),
    ]
    _compile_conditions = [
        lambda n, m: isinstance(m, SanaTransformerBlock),
    ]
    param_names_mapping = SanaConfig().arch_config.param_names_mapping
    reverse_param_names_mapping = {}

    def __init__(self, config: SanaConfig, hf_config=None, **kwargs):
        super().__init__(config, hf_config=hf_config or {}, **kwargs)

        arch = config.arch_config
        self.out_channels = arch.out_channels
        self.patch_size = arch.patch_size
        self.inner_dim = arch.num_attention_heads * arch.attention_head_dim

        self.hidden_size = self.inner_dim
        self.num_attention_heads = arch.num_attention_heads
        self.num_channels_latents = arch.num_channels_latents

        self.patch_embed = nn.ModuleDict(
            {
                "proj": nn.Conv2d(
                    arch.in_channels,
                    self.inner_dim,
                    kernel_size=arch.patch_size,
                    stride=arch.patch_size,
                    bias=True,
                ),
            }
        )
        self.time_embed = SanaAdaLayerNormSingle(self.inner_dim)
        self.caption_projection = PixArtAlphaTextProjection(
            in_features=arch.caption_channels,
            hidden_size=self.inner_dim,
        )

        self.caption_norm = RMSNorm(self.inner_dim)

        self.transformer_blocks = nn.ModuleList(
            [
                SanaTransformerBlock(
                    dim=self.inner_dim,
                    num_attention_heads=arch.num_attention_heads,
                    attention_head_dim=arch.attention_head_dim,
                    num_cross_attention_heads=arch.num_cross_attention_heads,
                    cross_attention_head_dim=arch.cross_attention_head_dim,
                    cross_attention_dim=arch.cross_attention_dim,
                    mlp_ratio=arch.mlp_ratio,
                    norm_eps=arch.norm_eps,
                    attention_bias=False,
                )
                for _ in range(arch.num_layers)
            ]
        )
        self.scale_shift_table = nn.Parameter(
            torch.randn(2, self.inner_dim) / self.inner_dim**0.5
        )

        self.norm_out = SanaModulatedNorm(self.inner_dim, eps=arch.norm_eps)

        self.proj_out = nn.Linear(
            self.inner_dim,
            arch.patch_size * arch.patch_size * self.out_channels,
            bias=True,
        )

        self.layer_names = ["transformer_blocks"]

    def forward(
        self,
        hidden_states: torch.Tensor,
        encoder_hidden_states: torch.Tensor = None,
        timestep: torch.LongTensor = None,
        guidance: torch.Tensor = None,
        encoder_attention_mask: torch.Tensor = None,
        **kwargs,
    ) -> torch.Tensor:

        # Input validation - fail fast
        if encoder_hidden_states is None:
            raise ValueError("SANA forward pass requires encoder_hidden_states")

        batch_size, channels, height, width = hidden_states.shape
        p = self.patch_size
        post_patch_height = height // p
        post_patch_width = width // p

        hidden_states = _mps_safe_conv2d(self.patch_embed["proj"], hidden_states)
        hidden_states = hidden_states.flatten(2).transpose(1, 2)

        timestep_emb, embedded_timestep = self.time_embed(
            timestep, hidden_dtype=hidden_states.dtype
        )

        if isinstance(encoder_attention_mask, (list, tuple)):
            encoder_attention_mask = encoder_attention_mask[0]

        if encoder_hidden_states.device.type == "mps":
            encoder_hidden_states = _mps_safe_linear(
                self.caption_projection.linear_1, encoder_hidden_states
            )
            encoder_hidden_states = self.caption_projection.act_1(encoder_hidden_states)
            encoder_hidden_states = _mps_safe_linear(
                self.caption_projection.linear_2, encoder_hidden_states
            )
        else:
            encoder_hidden_states = self.caption_projection(encoder_hidden_states)
        if encoder_hidden_states.shape[0] != batch_size:
            encoder_hidden_states = encoder_hidden_states.expand(
                batch_size, -1, -1
            ).contiguous()
        encoder_hidden_states = encoder_hidden_states.view(
            batch_size, -1, hidden_states.shape[-1]
        )
        encoder_hidden_states = self.caption_norm(encoder_hidden_states)

        if (
            encoder_attention_mask is not None
            and encoder_attention_mask.shape[0] != batch_size
        ):
            encoder_attention_mask = encoder_attention_mask.expand(
                batch_size, -1
            ).contiguous()

        for block in self.transformer_blocks:
            hidden_states = block(
                hidden_states,
                encoder_hidden_states,
                timestep_emb,
                post_patch_height,
                post_patch_width,
                encoder_attention_mask=encoder_attention_mask,
            )
        hidden_states = self.norm_out(
            hidden_states, embedded_timestep, self.scale_shift_table
        )
        hidden_states = _mps_safe_linear(self.proj_out, hidden_states)
        hidden_states = hidden_states.reshape(
            batch_size, post_patch_height, post_patch_width, p, p, self.out_channels
        )
        hidden_states = hidden_states.permute(0, 5, 1, 3, 2, 4)
        hidden_states = hidden_states.reshape(
            batch_size, self.out_channels, height, width
        )

        return hidden_states


EntryClass = SanaTransformer2DModel
