from functools import partial, reduce
from typing import Callable, Optional

from einops import rearrange
from flash_attn import flash_attn_with_kvcache, flash_attn_func
import torch
from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import (
    checkpoint_wrapper,
    CheckpointImpl,
    apply_activation_checkpointing,
)
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.attention.flex_attention import flex_attention, create_block_mask

# drastic speedup
flex_attention = torch.compile(flex_attention)


class KVCache(nn.Module):
    def __init__(
        self,
        batch_size,
        seqlen,
        n_heads,
        head_size,
        device="cuda",
        dtype=torch.bfloat16,
    ):
        super().__init__()

        cache_shape = (batch_size, seqlen, n_heads, head_size)
        self.register_buffer(
            "k_cache",
            torch.zeros(cache_shape, device=device, dtype=dtype),
            persistent=False,
        )
        self.register_buffer(
            "v_cache",
            torch.zeros(cache_shape, device=device, dtype=dtype),
            persistent=False,
        )
        self.is_initialized = False

    def __len__(self):
        return self.k_cache.shape[1]


# 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 RotaryEmbedding(nn.Module):
    def __init__(self, dim, base=50_000):
        super().__init__()
        inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
        self.register_buffer("inv_freq", inv_freq)

    @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,
        qk_norm=False,
    ):
        super().__init__()
        self.dim = dim
        self.dim_heads = dim_heads
        self.qk_norm = qk_norm

        self.num_heads = dim // dim_heads

        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)

        self.kv_cache: Optional[KVCache] = None

    def forward(
        self,
        x,
        rotary_pos_emb=None,
        block_mask=None,
        pos=None,
    ):
        h = self.num_heads
        b, n, _ = x.shape

        # 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:
            freqs = rotary_pos_emb

            q_dtype, k_dtype = q.dtype, k.dtype
            q, k, freqs = [t.to(torch.float32) for t in (q, k, freqs)]

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

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

        # flex attention
        q, k, v = map(lambda t: t.to(torch.bfloat16), (q, k, v))

        if self.kv_cache is not None:
            q, k, v = map(lambda t: rearrange(t, "b h n d -> b n h d", h=h), (q, k, v))
            if pos.ndim == 1:
                pos = pos.unsqueeze(0).repeat_interleave(q.shape[0], dim=0)
            if not self.kv_cache.is_initialized:
                out = flash_attn_with_kvcache(
                    q,
                    self.kv_cache.k_cache,
                    self.kv_cache.v_cache,
                    k=k,
                    v=v,
                    cache_seqlens=pos[:, 0].int(),
                    causal=False,
                )
            else:
                out = flash_attn_func(
                    q,
                    torch.concat([self.kv_cache.k_cache, k], dim=1),
                    torch.concat([self.kv_cache.v_cache, v], dim=1),
                    causal=False,
                )
            out = rearrange(out, " b n h d -> b n (h d)")
            self.kv_cache.is_initialized = True
        else:
            out = flex_attention(q, k, v, block_mask=block_mask)
            out = rearrange(out, " b h n d -> b n (h d)")

        out = out.to(q.dtype)

        # 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,
        layer_ix=-1,
        qk_norm=False,
    ):
        super().__init__()
        self.dim = dim
        self.dim_heads = dim_heads

        self.pre_norm = LayerNorm(dim)

        self.self_attn = Attention(
            dim,
            dim_heads=dim_heads,
            qk_norm=qk_norm,
        )

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

        self.layer_ix = layer_ix

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

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

        return x


def _custom_mask_mod(cumulative_sizes, q_idx, kv_idx):
    q_in_misc = q_idx < cumulative_sizes[0]
    q_in_audio = (q_idx < cumulative_sizes[1]) & (q_idx >= cumulative_sizes[0])

    kv_in_misc = kv_idx < cumulative_sizes[0]
    kv_in_audio = (kv_idx < cumulative_sizes[1]) & (kv_idx >= cumulative_sizes[0])

    same_block = (q_in_misc == kv_in_misc) | (q_in_audio == kv_in_audio)

    return same_block | q_in_audio


def _create_block_mask(attn_block_sizes, b, num_heads, device):
    cumulative_sizes = torch.cumsum(
        torch.tensor(attn_block_sizes, dtype=torch.long, device=device), dim=0
    )
    seq_len = cumulative_sizes[-1]

    def mask_mod(b, h, q, k):
        return _custom_mask_mod(cumulative_sizes, q, k)

    block_mask = create_block_mask(
        mask_mod, B=b, H=num_heads, Q_LEN=seq_len, KV_LEN=seq_len, device=device, _compile=True
    )
    return block_mask


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

        self.dim = dim
        self.depth = depth
        self.layers = nn.ModuleList([])
        assert dim % dim_heads == 0
        self.dim_heads = dim_heads
        self.num_heads = dim // dim_heads

        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)

        assert attn_block_sizes is not None
        self.attn_block_sizes = attn_block_sizes
        self.block_mask = None

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

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

    def setup_caches(self, batch_size: int, seqlen: int):
        for b in self.layers:
            b.self_attn.kv_cache = KVCache(
                batch_size,
                seqlen,
                self.num_heads,
                self.dim_heads,
                dtype=self.layers[0].self_attn.to_out.weight.dtype,
                device=self.device,
            )

    def forward(
        self,
        x,
        prepend_embeds=None,
    ):
        # Direct implementation for compile compatibility
        # Don't call _core_forward to avoid conditional logic with stop_layer_idx
        b, _, 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=1)
        else:
            prepend_length = 0

        assert x.shape[1] == sum(self.attn_block_sizes), f"{x.shape[1]} != {self.attn_block_sizes}"

        if self.layers[0].self_attn.kv_cache is not None:
            assert len(self.attn_block_sizes) == 2
            if not self.layers[0].self_attn.kv_cache.is_initialized:
                x_pre = x[:, : self.attn_block_sizes[0]]
                pos = torch.arange(self.attn_block_sizes[0], device=device)
                rotary_pos_emb = self.rotary_pos_emb(pos)
                for layer in self.layers:
                    x_pre = layer(
                        x_pre,
                        rotary_pos_emb=rotary_pos_emb,
                        pos=pos,
                    )
            x = x[:, self.attn_block_sizes[0] :]
            pos = self.attn_block_sizes[0] + torch.arange(self.attn_block_sizes[1], device=device)
            prepen_cut = 1
        else:
            # refresh block mask if needed
            if self.block_mask is None or self.block_mask.shape[0] != b:
                print("refreshing block mask...")
                self.block_mask = _create_block_mask(self.attn_block_sizes, b, self.num_heads, x.device)

            pos = torch.arange(x.shape[1], device=device)
            prepen_cut = prepend_length

        rotary_pos_emb = self.rotary_pos_emb(pos)

        for layer in self.layers:
            x = layer(
                x,
                rotary_pos_emb=rotary_pos_emb,
                block_mask=self.block_mask,
                pos=pos,
            )

        x = self.project_out(x)
        x = x[:, prepen_cut:]

        return x

    def forward_intermediate(
        self,
        x,
        prepend_embeds=None,
        intermediate_layer_idx=None,
    ):
        if intermediate_layer_idx is None:
            raise ValueError("intermediate_layer_idx must be provided")
        return self._core_forward(
            x,
            prepend_embeds=prepend_embeds,
            stop_layer_idx=intermediate_layer_idx,
        )

    def _core_forward(self, x, prepend_embeds=None, stop_layer_idx=None):
        b, _, 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=1)
        else:
            prepend_length = 0

        assert x.shape[1] == sum(self.attn_block_sizes), f"{x.shape[1]} != {self.attn_block_sizes}"

        if self.layers[0].self_attn.kv_cache is not None:
            assert len(self.attn_block_sizes) == 2
            if not self.layers[0].self_attn.kv_cache.is_initialized:
                x_pre = x[:, : self.attn_block_sizes[0]]
                pos = torch.arange(self.attn_block_sizes[0], device=device)
                rotary_pos_emb = self.rotary_pos_emb(pos)
                for layer in self.layers:
                    x_pre = layer(
                        x_pre,
                        rotary_pos_emb=rotary_pos_emb,
                        pos=pos,
                    )
            x = x[:, self.attn_block_sizes[0] :]
            pos = self.attn_block_sizes[0] + torch.arange(self.attn_block_sizes[1], device=device)
            prepen_cut = 1
        else:
            # refresh block mask if needed
            if self.block_mask is None or self.block_mask.shape[0] != b:
                print("refreshing block mask...")
                self.block_mask = _create_block_mask(self.attn_block_sizes, b, self.num_heads, x.device)

            pos = torch.arange(x.shape[1], device=device)
            prepen_cut = prepend_length

        rotary_pos_emb = self.rotary_pos_emb(pos)

        for i, layer in enumerate(self.layers):
            x = layer(
                x,
                rotary_pos_emb=rotary_pos_emb,
                block_mask=self.block_mask,
                pos=pos,
            )
            if stop_layer_idx is not None and i == stop_layer_idx:
                return x[:, prepen_cut:]

        if stop_layer_idx is not None:
            raise ValueError(f"intermediate_layer_idx {stop_layer_idx} out of range")

        x = self.project_out(x)
        x = x[:, prepen_cut:]

        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)
