import torch
from einops import rearrange, repeat
from torch import einsum, Tensor
from torch.nn import Module


def rotate_half(x: Tensor, interleaved=True) -> Tensor:
    if not interleaved:
        # x_half1, x_half2 = x.chunk(2, dim=-1)
        # Using torch.split instead of chunk for ONNX export compatibility.
        x1, x2 = torch.split(x, x.size(-1) // 2, dim=-1)
        return torch.cat((-x2, x1), dim=-1)
    else:
        x = rearrange(x, '... (d r) -> ... d r', r=2)
        x1, x2 = x.unbind(dim=-1)
        x = torch.stack((-x2, x1), dim=-1)
        return rearrange(x, '... d r -> ... (d r)')


def apply_rotary_emb(freqs: Tensor, t: Tensor, interleaved=True) -> Tensor:
    rot_dim = freqs.shape[-1]
    t_to_rotate = t[..., :rot_dim]
    t_pass_through = t[..., rot_dim:]

    t_rotated = (t_to_rotate * freqs.cos()) + (rotate_half(t_to_rotate, interleaved) * freqs.sin())

    return torch.cat((t_rotated, t_pass_through), dim=-1)


class RotaryEmbedding(Module):
    def __init__(
            self,
            dim,
            theta=10000,
            max_seq_len=8192,
            interleaved: bool = True
    ):
        super().__init__()
        self.interleaved = interleaved
        self.cached_freqs_seq_len = max_seq_len
        inv_freq = 1. / (theta ** (torch.arange(0, dim, 2).float() / dim))
        self.register_buffer('inv_freq', inv_freq, persistent=False)
        self.register_buffer('cached_freqs', self._precompute_cache(max_seq_len), persistent=False)

    def _precompute_cache(self, seq_len: int):
        seq = torch.arange(seq_len, device=self.inv_freq.device, dtype=self.inv_freq.dtype)
        freqs = einsum('i, j -> i j', seq, self.inv_freq)
        if self.interleaved:
            freqs = repeat(freqs, '... n -> ... (n r)', r=2)
        else:
            freqs = torch.cat((freqs, freqs), dim=-1)
        return freqs

    def forward(self, seq_len: int) -> Tensor:
        if seq_len > self.cached_freqs_seq_len:
            raise RuntimeError("sequence exceeds RoPE max_seq_len!")
        return self.cached_freqs[0: seq_len].detach()

    def rotate_queries_or_keys(self, t: Tensor) -> Tensor:
        device, dtype, seq_len = t.device, t.dtype, t.shape[-2]
        freqs = self.forward(seq_len=seq_len)

        return apply_rotary_emb(freqs.to(device=device, dtype=dtype), t, self.interleaved)
