from functools import partial, reduce
from typing import Callable

import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange


try:
    from flash_attn_interface import flash_attn_func
except ImportError:
    print("Failed to import flash_attn_3.")
    try:
        from flash_attn import flash_attn_func
    except ImportError:
        print("Failed to import flash_attn. You won't be able to use the Diffusion model.")
        flash_attn_func = None
        flash_attn_varlen_func = None

# from flash_attn_interface import flash_attn_func
from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import (
    checkpoint_wrapper,
    CheckpointImpl,
    apply_activation_checkpointing,
)


# Custom Layer Normalization with bias control
class LayerNorm(nn.Module):
    def __init__(self, dim, bias=False):
        super().__init__()
        self.gamma = nn.Parameter(torch.ones(dim))
        if bias:
            self.beta = nn.Parameter(torch.zeros(dim))
        else:
            self.register_buffer("beta", torch.zeros(dim))

    def forward(self, x):
        return F.layer_norm(x, x.shape[-1:], weight=self.gamma, bias=self.beta)


class ScaledSinusoidalEmbedding(nn.Module):
    def __init__(self, dim, theta=10000):
        super().__init__()
        assert (dim % 2) == 0, "dimension must be divisible by 2"
        self.scale = nn.Parameter(torch.ones(1) * dim**-0.5)

        half_dim = dim // 2
        freq_seq = torch.arange(half_dim, dtype=torch.float32) / half_dim
        inv_freq = theta**-freq_seq
        self.register_buffer("inv_freq", inv_freq, persistent=True)

    @torch.autocast("cuda", enabled=False)
    def forward(self, x, pos=None, seq_start_pos=None):
        seq_len, device = x.shape[1], x.device

        if pos is None:
            pos = torch.arange(seq_len, device=device, dtype=torch.float32)

        if seq_start_pos is not None:
            pos = pos - seq_start_pos[..., None]

        emb = torch.outer(pos, self.inv_freq)
        emb = torch.cat((emb.sin(), emb.cos()), dim=-1)
        return emb * self.scale


class RotaryEmbedding(nn.Module):
    def __init__(self, dim, base=10000):
        super().__init__()
        inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
        self.register_buffer("inv_freq", inv_freq)

    def forward_from_seq_len(self, seq_len):
        device = self.inv_freq.device
        t = torch.arange(seq_len, device=device)
        return self.forward(t)

    @torch.autocast("cuda", enabled=False)
    def forward(self, t):
        t = t.to(torch.float32)
        freqs = torch.outer(t, self.inv_freq)
        freqs = torch.cat((freqs, freqs), dim=-1)
        return freqs


def rotate_half(x):
    x = rearrange(x, "... (j d) -> ... j d", j=2)
    x1, x2 = x.unbind(dim=-2)
    return torch.cat((-x2, x1), dim=-1)


@torch.autocast("cuda", enabled=False)
def apply_rotary_pos_emb(t, freqs):
    out_dtype = t.dtype

    # cast to float32 if necessary for numerical stability
    dtype = reduce(torch.promote_types, (t.dtype, freqs.dtype, torch.float32))
    rot_dim, seq_len = freqs.shape[-1], t.shape[-2]
    freqs, t = freqs.to(dtype), t.to(dtype)
    freqs = freqs[-seq_len:, :]

    if t.ndim == 4 and freqs.ndim == 3:
        freqs = rearrange(freqs, "b n d -> b 1 n d")

    # partial rotary embeddings, Wang et al. GPT-J
    t, t_unrotated = t[..., :rot_dim], t[..., rot_dim:]
    t = t * freqs.cos() + rotate_half(t) * freqs.sin()

    t, t_unrotated = t.to(out_dtype), t_unrotated.to(out_dtype)

    return torch.cat((t, t_unrotated), dim=-1)


class Attention(nn.Module):
    def __init__(
        self,
        dim,
        dim_heads=64,
        causal=False,
        is_cross=False,
        qk_norm=False,
    ):
        super().__init__()
        self.dim = dim
        self.dim_heads = dim_heads
        self.causal = causal
        self.is_cross = is_cross
        self.qk_norm = qk_norm

        self.num_heads = dim // dim_heads

        if self.is_cross:
            self.to_q = nn.Linear(dim, dim, bias=False)
            self.to_kv = nn.Linear(dim, dim * 2, bias=False)
        else:
            self.to_qkv = nn.Linear(dim, dim * 3, bias=False)

        if self.qk_norm:
            self.q_ln = nn.LayerNorm(dim_heads)
            self.k_ln = nn.LayerNorm(dim_heads)

        self.to_out = nn.Linear(dim, dim, bias=False)

    def forward(
        self,
        x,
        context=None,
        rotary_pos_emb=None,
        causal=None,
    ):
        h, has_context = self.num_heads, context is not None
        assert has_context == self.is_cross

        if has_context:
            # Use separate linear projections for q and k/v
            q = self.to_q(x)
            k, v = self.to_kv(context).chunk(2, dim=-1)

        else:
            # Use fused linear projection
            q, k, v = self.to_qkv(x).chunk(3, dim=-1)

        q, k, v = map(lambda t: rearrange(t, "b n (h d) -> b h n d", h=h), (q, k, v))

        # Normalize q and k for cosine sim attention
        if self.qk_norm:
            q = self.q_ln(q)
            k = self.k_ln(k)

        if rotary_pos_emb is not None and not has_context:
            freqs = rotary_pos_emb

            q_dtype = q.dtype
            k_dtype = k.dtype

            q = q.to(torch.float32)
            k = k.to(torch.float32)
            freqs = freqs.to(torch.float32)

            q = apply_rotary_pos_emb(q, freqs)
            k = apply_rotary_pos_emb(k, freqs)

            q = q.to(q_dtype)
            k = k.to(k_dtype)

        n, device = q.shape[-2], q.device

        causal = self.causal if causal is None else causal

        if n == 1 and causal:
            causal = False

        # TODO: is it an issue to always cast to bfloat16 here??
        fa_dtype_in = q.dtype
        q, k, v = map(
            lambda t: rearrange(t, "b h n d -> b n h d").to(torch.bfloat16),
            (q, k, v),
        )

        out = flash_attn_func(q, k, v, causal=causal)
        if isinstance(out, tuple):  # not sure why this is a tuple with fa3
            out = out[0]
        out = rearrange(out.to(fa_dtype_in), "b n h d -> b h n d")

        # merge heads
        out = rearrange(out, " b h n d -> b n (h d)")

        # Communicate between heads
        out = self.to_out(out)
        return out


class GLU(nn.Module):
    def __init__(
        self,
        dim_in,
        dim_out,
        activation: Callable,
    ):
        super().__init__()
        self.act = activation
        self.proj = nn.Linear(dim_in, dim_out * 2)

    def forward(self, x):
        x = self.proj(x)
        x, gate = x.chunk(2, dim=-1)
        return x * self.act(gate)


class FeedForward(nn.Module):
    def __init__(
        self,
        dim,
        dim_out=None,
        mult=4,
    ):
        super().__init__()
        inner_dim = int(dim * mult)

        # Default to SwiGLU

        activation = nn.SiLU()

        dim_out = dim if dim_out is None else dim_out

        linear_in = GLU(dim, inner_dim, activation)

        linear_out = nn.Linear(inner_dim, dim_out, bias=True)

        self.ff = nn.Sequential(
            linear_in,
            linear_out,
        )

    def forward(self, x):
        return self.ff(x)


class TransformerBlock(nn.Module):
    def __init__(
        self,
        dim,
        dim_heads=64,
        causal=False,
        layer_ix=-1,
        qk_norm=False,
    ):
        super().__init__()
        self.dim = dim
        self.dim_heads = dim_heads
        self.causal = causal

        self.pre_norm = LayerNorm(dim)

        self.self_attn = Attention(
            dim,
            dim_heads=dim_heads,
            causal=causal,
            is_cross=False,
            qk_norm=qk_norm,
        )

        self.cross_attend_norm = LayerNorm(dim)
        self.cross_attn = Attention(
            dim,
            dim_heads=dim_heads,
            causal=causal,
            is_cross=True,
            qk_norm=qk_norm,
        )

        self.ff_norm = LayerNorm(dim)
        self.ff = FeedForward(dim)

        self.layer_ix = layer_ix

    def forward(
        self,
        x,
        context=None,
        rotary_pos_emb=None,
    ):
        x = x + self.self_attn(
            self.pre_norm(x),
            rotary_pos_emb=rotary_pos_emb,
        )

        if context is not None:
            x = x + self.cross_attn(
                self.cross_attend_norm(x),
                context=context,
            )

        x = x + self.ff(self.ff_norm(x))

        return x


class ContinuousTransformer(nn.Module):
    def __init__(
        self,
        dim,
        depth,
        dim_heads=64,
        dim_in=None,
        dim_out=None,
        causal=False,
        qk_norm=False,
    ):
        super().__init__()

        self.dim = dim
        self.depth = depth
        self.causal = causal
        self.layers = nn.ModuleList([])

        self.project_in = nn.Linear(dim_in, dim, bias=False)
        self.project_out = nn.Linear(dim, dim_out, bias=False)

        self.rotary_pos_emb = RotaryEmbedding(dim_heads)

        for i in range(depth):
            self.layers.append(
                TransformerBlock(
                    dim,
                    dim_heads=dim_heads,
                    causal=causal,
                    layer_ix=i,
                    qk_norm=qk_norm,
                )
            )

    def forward(
        self,
        x,
        context=None,
        prepend_embeds=None,
    ):
        batch, seq, device = *x.shape[:2], x.device

        x = self.project_in(x)

        if prepend_embeds is not None:
            prepend_length, prepend_dim = prepend_embeds.shape[1:]

            assert prepend_dim == x.shape[-1], "prepend dimension must match sequence dimension"

            x = torch.cat((prepend_embeds, x), dim=-2)

        # Attention layers

        rotary_pos_emb = self.rotary_pos_emb.forward_from_seq_len(x.shape[1])

        # Iterate over the transformer layers
        for layer in self.layers:
            x = layer(
                x,
                context=context,
                rotary_pos_emb=rotary_pos_emb,
            )

        x = self.project_out(x)

        return x


non_reentrant_wrapper = partial(
    checkpoint_wrapper,
    checkpoint_impl=CheckpointImpl.NO_REENTRANT,
)


def check_fn(submodule):
    return isinstance(submodule, TransformerBlock)


def apply_fsdp_checkpointing(model):
    apply_activation_checkpointing(model, checkpoint_wrapper_fn=non_reentrant_wrapper, check_fn=check_fn)
