"""
Shared conditioning embedding modules for diffusion transformers.

Previously duplicated across:
  - medlat/generators/non_autoregressive/dit/models.py
  - medlat/generators/non_autoregressive/uvit/models.py
  - medlat/diffusion/diffloss.py
"""

import math
import torch
import torch.nn as nn


def modulate(x, shift, scale):
    """AdaLN modulation for spatial token sequences.

    Expects x of shape (B, L, D) and shift/scale of shape (B, D).
    """
    return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)


class TimestepEmbedder(nn.Module):
    """
    Embeds scalar timesteps into vector representations.
    """
    def __init__(self, hidden_size, frequency_embedding_size=256):
        super().__init__()
        self.mlp = nn.Sequential(
            nn.Linear(frequency_embedding_size, hidden_size, bias=True),
            nn.SiLU(),
            nn.Linear(hidden_size, hidden_size, bias=True),
        )
        self.frequency_embedding_size = frequency_embedding_size

    @staticmethod
    def timestep_embedding(t, dim, max_period=10000):
        """
        Create sinusoidal timestep embeddings.
        :param t: a 1-D Tensor of N indices, one per batch element.
                          These may be fractional.
        :param dim: the dimension of the output.
        :param max_period: controls the minimum frequency of the embeddings.
        :return: an (N, D) Tensor of positional embeddings.
        """
        # https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py
        half = dim // 2
        freqs = torch.exp(
            -math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
        ).to(device=t.device)
        args = t[:, None].float() * freqs[None]
        embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
        if dim % 2:
            embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
        return embedding

    def forward(self, t):
        t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
        t_emb = self.mlp(t_freq)
        return t_emb


class LabelEmbedder(nn.Module):
    """
    Embeds class labels into vector representations. Also handles label dropout for classifier-free guidance.
    """
    def __init__(self, num_classes, hidden_size, dropout_prob):
        super().__init__()
        use_cfg_embedding = dropout_prob > 0
        self.embedding_table = nn.Embedding(num_classes + use_cfg_embedding, hidden_size)
        self.num_classes = num_classes
        self.dropout_prob = dropout_prob

    def token_drop(self, labels, force_drop_ids=None):
        """
        Drops labels to enable classifier-free guidance.
        """
        if force_drop_ids is None:
            drop_ids = torch.rand(labels.shape[0], device=labels.device) < self.dropout_prob
        else:
            drop_ids = force_drop_ids == 1
        labels = torch.where(drop_ids, self.num_classes, labels)
        return labels

    def forward(self, labels, train, force_drop_ids=None):
        use_dropout = self.dropout_prob > 0
        if (train and use_dropout) or (force_drop_ids is not None):
            labels = self.token_drop(labels, force_drop_ids)
        embeddings = self.embedding_table(labels)
        return embeddings


class DatasetEmbedder(nn.Module):
    """
    Embeds dataset IDs into vector representations. Also handles label dropout for classifier-free guidance.
    """
    def __init__(self, num_datasets, hidden_size, dropout_prob):
        super().__init__()
        use_cfg_embedding = dropout_prob > 0
        self.embedding_table = nn.Embedding(num_datasets + use_cfg_embedding, hidden_size)
        self.num_datasets = num_datasets
        self.dropout_prob = dropout_prob

    def token_drop(self, dataset_ids, force_drop_ids=None):
        """
        Drops dataset IDs to enable classifier-free guidance.
        """
        if force_drop_ids is None:
            drop_ids = torch.rand(dataset_ids.shape[0], device=dataset_ids.device) < self.dropout_prob
        else:
            drop_ids = force_drop_ids == 1
        dataset_ids = torch.where(drop_ids, self.num_datasets, dataset_ids)
        return dataset_ids

    def forward(self, dataset_ids, train, force_drop_ids=None):
        use_dropout = self.dropout_prob > 0
        if (train and use_dropout) or (force_drop_ids is not None):
            dataset_ids = self.token_drop(dataset_ids, force_drop_ids)
        embeddings = self.embedding_table(dataset_ids)
        return embeddings


class FinalLayer(nn.Module):
    """
    AdaLN final layer shared by DiT and UViT.
    Projects patch tokens back to pixel space with adaptive layer norm.
    """
    def __init__(self, hidden_size, patch_size, out_channels, cond_dim=None):
        super().__init__()
        self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
        self.linear = nn.Linear(hidden_size, math.prod(patch_size) * out_channels, bias=True)
        self.adaLN_modulation = nn.Sequential(
            nn.SiLU(),
            nn.Linear(cond_dim or hidden_size, 2 * hidden_size, bias=True)
        )

    def forward(self, x, c):
        shift, scale = self.adaLN_modulation(c).chunk(2, dim=1)
        x = modulate(self.norm_final(x), shift, scale)
        x = self.linear(x)
        return x
