import math

from einops import rearrange
import torch
from torch import nn
import numpy as np
import torch.distributed

from base import ContinuousTransformer, ScaledSinusoidalEmbedding


class FourierFeatures(nn.Module):
    def __init__(self, in_features, out_features):
        super().__init__()
        assert out_features % 2 == 0
        self.weight = nn.Parameter(torch.zeros(out_features // 2, in_features))

    def forward(self, input):
        f = 2 * math.pi * input @ self.weight.T
        return torch.cat([f.cos(), f.sin()], dim=-1)


class SemanticConditioner(nn.Module):
    def __init__(
        self,
        vocab_size: int = 4001,
        input_dim: int = 768,
        output_dim: int = 1536,
    ):
        super().__init__()
        self.vocab_size = vocab_size
        self.embedding = torch.nn.Embedding(self.vocab_size, input_dim)
        self.proj_out = nn.Linear(input_dim, output_dim)
        self.pos_embedding = ScaledSinusoidalEmbedding(output_dim)

    def _initialize(self, centroid_path):
        centroids = np.load(centroid_path)[0]
        # create a new tensor with an extra row for the pad token
        centroids = np.concatenate([centroids, np.random.randn(1, centroids.shape[1])], axis=0)
        assert self.embedding.weight.shape == centroids.shape
        self.embedding.weight.data.copy_(torch.tensor(centroids, dtype=torch.float32))

    def forward(self, x):
        x = self.embedding(x)
        x = self.proj_out(x)
        x = x + self.pos_embedding(x)
        return x  # [batch, sequence, channels]


class TextConditioner(nn.Module):
    def __init__(
        self,
        vocab_size: int = 60001,
        output_dim: int = 1536,
    ):
        super().__init__()
        self.embedding = torch.nn.Embedding(vocab_size, output_dim)
        self.pos_embedding = ScaledSinusoidalEmbedding(output_dim)

    def forward(self, x):
        x = self.embedding(x)
        x = x + self.pos_embedding(x)
        return x  # [batch, sequence, channels]


class LatentContextConditioner(nn.Module):
    def __init__(
        self,
        io_channels: int = 128,
        output_dim: int = 1536,
    ):
        super().__init__()
        self.proj_out = nn.Linear(io_channels, output_dim)
        self.pos_embedding = ScaledSinusoidalEmbedding(output_dim)

    def forward(self, x):
        x = self.proj_out(x)
        x = x + self.pos_embedding(x)
        return x


class DiffusionTransformer(nn.Module):
    def __init__(
        self,
        io_hz=100,
        io_channels=128,
        embed_dim=1536,
        depth=24,
        n_heads=24,
        qk_norm=True,
        block_size=3000,
        cond_semantic_n_vocab=4001,
        cond_semantic_len=750,
        cond_text_n_vocab=60001,
        cond_text_len=2560,
        ctx_len=None,
    ):
        super().__init__()
        self.io_hz = io_hz
        self.io_channels = io_channels
        self.embed_dim = embed_dim
        self.depth = depth
        self.n_heads = n_heads
        self.qk_norm = qk_norm
        self.block_size = block_size
        self.cond_semantic_n_vocab = cond_semantic_n_vocab
        self.cond_semantic_len = cond_semantic_len
        self.cond_text_n_vocab = cond_text_n_vocab
        self.cond_text_len = cond_text_len

        self.semantic_conditioner = SemanticConditioner(
            vocab_size=cond_semantic_n_vocab,
            output_dim=embed_dim,
        )
        self.text_conditioner = TextConditioner(vocab_size=cond_text_n_vocab, output_dim=embed_dim)
        self.ctx_len = ctx_len
        if ctx_len is not None:
            self.ctx_conditioner = LatentContextConditioner(
                io_channels=io_channels, output_dim=embed_dim
            )
            self.vae_pad_embed = nn.Parameter(torch.zeros(io_channels))

        # Timestep embeddings
        timestep_features_dim = 256
        self.timestep_features = FourierFeatures(1, timestep_features_dim)
        self.to_timestep_embed = nn.Sequential(
            nn.Linear(timestep_features_dim, embed_dim, bias=True),
            nn.SiLU(),
            nn.Linear(embed_dim, embed_dim, bias=True),
        )

        # Conditioning tokens
        self.to_cond_embed = nn.Sequential(
            nn.Linear(embed_dim, embed_dim, bias=False),
            nn.SiLU(),
            nn.Linear(embed_dim, embed_dim, bias=False),
        )

        # Transformer
        assert embed_dim % n_heads == 0
        dim_heads = embed_dim // n_heads
        self.transformer = ContinuousTransformer(
            embed_dim,
            depth,
            dim_heads=dim_heads,
            dim_in=io_channels,
            dim_out=io_channels,
            causal=False,
            qk_norm=qk_norm,
        )
        self.preprocess_conv = nn.Conv1d(io_channels, io_channels, 1, bias=False)
        self.postprocess_conv = nn.Conv1d(io_channels, io_channels, 1, bias=False)

    @property
    def device(self):
        return next(self.parameters()).device

    @property
    def dtype(self):
        return next(self.parameters()).dtype

    def _initialize(self, semantic_centroid_path=None):
        # Note: this is simple, but so far just as good as other inits
        nn.init.zeros_(self.preprocess_conv.weight)
        nn.init.zeros_(self.postprocess_conv.weight)
        nn.init.normal_(self.timestep_features.weight)
        for layer in self.transformer.layers:
            nn.init.zeros_(layer.ff.ff[1].weight)
            nn.init.zeros_(layer.ff.ff[1].bias)
            nn.init.zeros_(layer.self_attn.to_out.weight)
            nn.init.zeros_(layer.cross_attn.to_out.weight)
        with torch.no_grad():
            for param in self.parameters():
                param *= 0.5
        if semantic_centroid_path is not None:
            self.semantic_conditioner._initialize(semantic_centroid_path)

    def _forward(
        self,
        x,
        t,
        cross_attn_cond=None,
    ):
        if cross_attn_cond is not None:
            cross_attn_cond = self.to_cond_embed(cross_attn_cond)

        # Get the batch of timestep embeddings
        timestep_embed = self.to_timestep_embed(self.timestep_features(t[:, None]))  # (b, embed_dim)
        prepend_inputs = timestep_embed.unsqueeze(1)
        prepend_length = prepend_inputs.shape[1]

        x = self.preprocess_conv(x) + x

        x = rearrange(x, "b c t -> b t c")

        output = self.transformer(
            x,
            context=cross_attn_cond,
            prepend_embeds=prepend_inputs,
        )

        output = rearrange(output, "b t c -> b c t")[:, :, prepend_length:]

        output = self.postprocess_conv(output) + output

        return output

    def forward(
        self,
        x,
        t,
        text_codes=None,
        semantic_codes=None,
        ctx_vae=None,
        ctx_mask=None,
        future_ctx_vae=None,
        future_ctx_mask=None,
    ):
        assert text_codes is not None
        assert semantic_codes is not None
        assert torch.all(text_codes < self.cond_text_n_vocab), text_codes.max()
        assert torch.all(semantic_codes < self.cond_semantic_n_vocab)
        text_embeds = self.text_conditioner(text_codes)
        semantic_embeds = self.semantic_conditioner(semantic_codes)
        cross_attn_cond = torch.concat([text_embeds, semantic_embeds], dim=1)
        if self.ctx_len is not None:
            # ctx_vae: [batch, channels, seq_len]
            # ctx_mask: [batch, seq_len]
            # Expand vae_pad_embed to match input shape
            expanded_vae_pad = self.vae_pad_embed.view(1, -1, 1).expand(
                ctx_vae.shape[0], -1, ctx_vae.shape[2]
            )
            # Invert the mask (1 where we want to replace, 0 elsewhere)
            inverted_ctx_mask = (~ctx_mask).float().unsqueeze(1)
            # Use the mask to blend the original input with the pad embedding
            ctx_vae_input = (
                ctx_vae * ctx_mask.float().unsqueeze(1) + expanded_vae_pad * inverted_ctx_mask
            )
            ctx_vae_input = ctx_vae_input.permute(0, 2, 1).to(x.dtype)
            ctx_embeds = self.ctx_conditioner(ctx_vae_input)

            cross_attn_cond = torch.concat([cross_attn_cond, ctx_embeds], dim=1)

        return self._forward(x, t, cross_attn_cond=cross_attn_cond)

    def forward_inference(
        self,
        x,
        t,
        cross_attn_cond=None,
        no_text_cross_attn_cond=None,
        no_tag_cross_attn_cond=None,
        no_sem_cross_attn_cond=None,
        no_ctx_cross_attn_cond=None,
        text_cfg_scale=1.0,
        tag_cfg_scale=1.0,
        sem_cfg_scale=1.0,
        ctx_cfg_scale=1.0,
    ):
        assert cross_attn_cond is not None
        assert no_text_cross_attn_cond is not None
        has_text_cfg = text_cfg_scale != 1.0 and no_text_cross_attn_cond is not None
        has_tag_cfg = tag_cfg_scale != 1.0 and no_tag_cross_attn_cond is not None
        has_sem_cfg = sem_cfg_scale != 1.0 and no_sem_cross_attn_cond is not None
        has_ctx_cfg = ctx_cfg_scale != 1.0 and no_ctx_cross_attn_cond is not None
        n_rep = 1 + int(has_text_cfg) + int(has_tag_cfg) + int(has_sem_cfg) + int(has_ctx_cfg)

        # Classifier-free guidance

        # Concatenate conditioned and unconditioned inputs on the batch dimension
        batch_inputs = torch.cat([x] * n_rep, dim=0)
        batch_timestep = torch.cat([t] * n_rep, dim=0)

        # Handle CFG for cross-attention conditioning
        if has_text_cfg:
            cross_attn_cond = torch.cat(
                [
                    cross_attn_cond,
                    no_text_cross_attn_cond,
                ],
                dim=0,
            )
        if has_tag_cfg:
            cross_attn_cond = torch.cat([cross_attn_cond, no_tag_cross_attn_cond], dim=0)
        if has_sem_cfg:
            cross_attn_cond = torch.cat([cross_attn_cond, no_sem_cross_attn_cond], dim=0)
        if has_ctx_cfg:
            cross_attn_cond = torch.cat([cross_attn_cond, no_ctx_cross_attn_cond], dim=0)

        batch_output = self._forward(
            batch_inputs,
            batch_timestep,
            cross_attn_cond=cross_attn_cond,
        )

        cfg_list = []
        if has_text_cfg:
            cfg_list.append(text_cfg_scale)
        if has_tag_cfg:
            cfg_list.append(tag_cfg_scale)
        if has_sem_cfg:
            cfg_list.append(sem_cfg_scale)
        if has_ctx_cfg:
            cfg_list.append(ctx_cfg_scale)
        if len(cfg_list) == 4:
            cond_output, no_out_1, no_out_2, no_out_3, no_out_4 = torch.chunk(batch_output, 5, dim=0)
            batch_output = (
                cond_output
                + (cond_output - no_out_1) * (cfg_list[0] - 1.0)
                + (cond_output - no_out_2) * (cfg_list[1] - 1.0)
                + (cond_output - no_out_3) * (cfg_list[2] - 1.0)
                + (cond_output - no_out_4) * (cfg_list[3] - 1.0)
            )
        elif len(cfg_list) == 3:
            cond_output, no_out_1, no_out_2, no_out_3 = torch.chunk(batch_output, 4, dim=0)
            batch_output = (
                cond_output
                + (cond_output - no_out_1) * (cfg_list[0] - 1.0)
                + (cond_output - no_out_2) * (cfg_list[1] - 1.0)
                + (cond_output - no_out_3) * (cfg_list[2] - 1.0)
            )
        elif len(cfg_list) == 2:
            cond_output, no_out_1, no_out_2 = torch.chunk(batch_output, 3, dim=0)
            batch_output = (
                cond_output
                + (cond_output - no_out_1) * (cfg_list[0] - 1.0)
                + (cond_output - no_out_2) * (cfg_list[1] - 1.0)
            )
        elif len(cfg_list) == 1:
            cond_output, no_out_1 = torch.chunk(batch_output, 2, dim=0)
            batch_output = cond_output + (cond_output - no_out_1) * (cfg_list[0] - 1.0)
        else:
            batch_output = batch_output

        return batch_output
