# Copied and adapted from: https://github.com/descriptinc/descript-audio-codec

# SPDX-License-Identifier: MIT

import math
from bisect import bisect_right
from typing import Union

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

from sglang.multimodal_gen.configs.models.vaes.dac import DacVAEConfig
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
    LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.vaes.common import (
    DiagonalGaussianDistribution,
)


def _snake(x, alpha):
    shape = x.shape
    x = x.reshape(shape[0], shape[1], -1)
    x = x + (alpha + 1e-9).reciprocal() * torch.sin(alpha * x).pow(2)
    x = x.reshape(shape)
    return x


# Scripting this brings model speed up 1.4x
snake = torch.jit.script(_snake)


# ROCm HIPRTC can fail to compile the scripted bf16 Snake kernel.
def _should_use_eager_snake_on_rocm_bf16(x: torch.Tensor, alpha: torch.Tensor) -> bool:
    return (
        torch.version.hip is not None
        and (x.is_cuda or alpha.is_cuda)
        and (x.dtype == torch.bfloat16 or alpha.dtype == torch.bfloat16)
    )


class Snake1d(nn.Module):
    def __init__(self, channels):
        super().__init__()
        self.alpha = nn.Parameter(torch.ones(1, channels, 1))

    def forward(self, x):
        if _should_use_eager_snake_on_rocm_bf16(x, self.alpha):
            return _snake(x, self.alpha)
        return snake(x, self.alpha)


class VectorQuantize(nn.Module):
    """
    Implementation of VQ similar to Karpathy's repo:
    https://github.com/karpathy/deep-vector-quantization
    Additionally uses following tricks from Improved VQGAN
    (https://arxiv.org/pdf/2110.04627.pdf):
        1. Factorized codes: Perform nearest neighbor lookup in low-dimensional space
            for improved codebook usage
        2. l2-normalized codes: Converts euclidean distance to cosine similarity which
            improves training stability
    """

    def __init__(self, input_dim: int, codebook_size: int, codebook_dim: int):
        super().__init__()
        self.codebook_size = codebook_size
        self.codebook_dim = codebook_dim

        self.in_proj = nn.Conv1d(input_dim, codebook_dim, kernel_size=1)
        self.out_proj = nn.Conv1d(codebook_dim, input_dim, kernel_size=1)
        self.codebook = nn.Embedding(codebook_size, codebook_dim)

    def forward(self, z):
        """Quantize the input tensor using a fixed codebook and return the corresponding codebook vectors.

        Args:
            z (torch.Tensor): Input tensor with shape ``[B, D, T]``.

        Returns:
            tuple: A tuple containing:
                - z_q (torch.Tensor): Quantized continuous representation with shape ``[B, D, T]``.
                - commitment_loss (torch.Tensor): Commitment loss scalar to train encoder to predict
                  vectors closer to codebook entries.
                - codebook_loss (torch.Tensor): Codebook loss scalar to update the codebook.
                - indices (torch.Tensor): Codebook indices (quantized discrete representation) with shape ``[B, T]``.
                - z_e (torch.Tensor): Projected latents (continuous representation before quantization) with shape ``[B, D, T]``.
        """

        # Factorized codes (ViT-VQGAN) Project input into low-dimensional space
        z_e = self.in_proj(z)  # z_e : (B x D x T)
        z_q, indices = self.decode_latents(z_e)

        commitment_loss = F.mse_loss(z_e, z_q.detach(), reduction="none").mean([1, 2])
        codebook_loss = F.mse_loss(z_q, z_e.detach(), reduction="none").mean([1, 2])

        z_q = (
            z_e + (z_q - z_e).detach()
        )  # noop in forward pass, straight-through gradient estimator in backward pass

        z_q = self.out_proj(z_q)

        return z_q, commitment_loss, codebook_loss, indices, z_e

    def embed_code(self, embed_id):
        return F.embedding(embed_id, self.codebook.weight)

    def decode_code(self, embed_id):
        return self.embed_code(embed_id).transpose(1, 2)

    def decode_latents(self, latents):
        encodings = rearrange(latents, "b d t -> (b t) d")
        codebook = self.codebook.weight  # codebook: (N x D)

        # L2 normalize encodings and codebook (ViT-VQGAN)
        encodings = F.normalize(encodings)
        codebook = F.normalize(codebook)

        # Compute euclidean distance with codebook
        dist = (
            encodings.pow(2).sum(1, keepdim=True)
            - 2 * encodings @ codebook.t()
            + codebook.pow(2).sum(1, keepdim=True).t()
        )
        indices = rearrange((-dist).max(1)[1], "(b t) -> b t", b=latents.size(0))
        z_q = self.decode_code(indices)
        return z_q, indices


class ResidualVectorQuantize(nn.Module):
    """
    Introduced in SoundStream: An end2end neural audio codec
    https://arxiv.org/abs/2107.03312
    """

    def __init__(
        self,
        input_dim: int = 512,
        n_codebooks: int = 9,
        codebook_size: int = 1024,
        codebook_dim: Union[int, list] = 8,
        quantizer_dropout: float = 0.0,
    ):
        super().__init__()
        if isinstance(codebook_dim, int):
            codebook_dim = [codebook_dim for _ in range(n_codebooks)]

        self.n_codebooks = n_codebooks
        self.codebook_dim = codebook_dim
        self.codebook_size = codebook_size
        dim_offsets = [0]
        for dim in self.codebook_dim:
            dim_offsets.append(dim_offsets[-1] + dim)
        self._codebook_dim_offsets = tuple(dim_offsets)

        self.quantizers = nn.ModuleList(
            [
                VectorQuantize(input_dim, codebook_size, codebook_dim[i])
                for i in range(n_codebooks)
            ]
        )
        self.quantizer_dropout = quantizer_dropout

    def forward(self, z, n_quantizers: int = None):
        """Quantize the input tensor using a fixed set of codebooks and return the corresponding codebook vectors.

        Args:
            z (torch.Tensor): Input tensor with shape ``[B, D, T]``.
            n_quantizers (int, optional): Number of quantizers to use. If ``None``,
                all quantizers are used. When ``n_quantizers`` < ``self.n_codebooks``,
                quantizer dropout is applied. Note: if ``self.quantizer_dropout`` > 0
                and in training mode, this argument is ignored and a random number of
                quantizers is used.

        Returns:
            tuple: A tuple containing:
                - z_q (torch.Tensor): Quantized continuous representation with shape ``[B, D, T]``.
                - codes (torch.Tensor): Codebook indices for each codebook with shape ``[B, N, T]``
                  (quantized discrete representation of input).
                - latents (torch.Tensor): Projected latents with shape ``[B, N*D, T]``
                  (continuous representation before quantization).
                - commitment_loss (torch.Tensor): Commitment loss scalar to train encoder to predict
                  vectors closer to codebook entries.
                - codebook_loss (torch.Tensor): Codebook loss scalar to update the codebook.
        """
        z_q = 0
        residual = z
        commitment_loss = 0
        codebook_loss = 0

        codebook_indices = []
        latents = []

        if n_quantizers is None:
            n_quantizers = self.n_codebooks
        quantizers = self.quantizers
        if self.training:
            batch_size = z.shape[0]
            device = z.device
            n_quantizers = torch.full(
                (batch_size,),
                self.n_codebooks + 1,
                device=device,
                dtype=torch.long,
            )
            if self.quantizer_dropout > 0:
                dropout = torch.randint(
                    1,
                    self.n_codebooks + 1,
                    (batch_size,),
                    device=device,
                )
                n_dropout = int(batch_size * self.quantizer_dropout)
                if n_dropout > 0:
                    n_quantizers[:n_dropout] = dropout[:n_dropout]

            for i, quantizer in enumerate(quantizers):
                z_q_i, commitment_loss_i, codebook_loss_i, indices_i, z_e_i = quantizer(
                    residual
                )

                # Create mask to apply quantizer dropout
                mask = i < n_quantizers
                z_q = z_q + z_q_i * mask[:, None, None]
                residual = residual - z_q_i

                # Sum losses
                commitment_loss += (commitment_loss_i * mask).mean()
                codebook_loss += (codebook_loss_i * mask).mean()

                codebook_indices.append(indices_i)
                latents.append(z_e_i)
        else:
            for i, quantizer in enumerate(quantizers):
                if i >= n_quantizers:
                    break
                z_q_i, commitment_loss_i, codebook_loss_i, indices_i, z_e_i = quantizer(
                    residual
                )
                z_q = z_q + z_q_i
                residual = residual - z_q_i

                commitment_loss += commitment_loss_i.mean()
                codebook_loss += codebook_loss_i.mean()

                codebook_indices.append(indices_i)
                latents.append(z_e_i)

        codes = torch.stack(codebook_indices, dim=1)
        latents = torch.cat(latents, dim=1)

        return z_q, codes, latents, commitment_loss, codebook_loss

    def from_codes(self, codes: torch.Tensor):
        """Reconstruct the continuous representation from quantized codes.

        Args:
            codes (torch.Tensor): Quantized discrete representation with shape ``[B, N, T]``.

        Returns:
            tuple: A tuple containing:
                - z_q (torch.Tensor): Quantized continuous representation with shape ``[B, D, T]``.
                - z_p (torch.Tensor): Concatenated latent space representation with shape ``[B, N*D, T]``.
                - codes (torch.Tensor): Original input codebook indices with shape ``[B, N, T]``.
        """
        z_q = 0.0
        z_p = []
        n_codebooks = codes.shape[1]
        for i in range(n_codebooks):
            z_p_i = self.quantizers[i].decode_code(codes[:, i, :])
            z_p.append(z_p_i)

            z_q_i = self.quantizers[i].out_proj(z_p_i)
            z_q = z_q + z_q_i
        return z_q, torch.cat(z_p, dim=1), codes

    def from_latents(self, latents: torch.Tensor):
        """Reconstruct the continuous representation from unquantized latents.

        Args:
            latents (torch.Tensor): Continuous representation after projection with shape ``[B, N*D, T]``.

        Returns:
            tuple: A tuple containing:
                - z_q (torch.Tensor): Quantized representation of full-projected space with shape ``[B, D, T]``.
                - z_p (torch.Tensor): Quantized representation of latent space with shape ``[B, N*D, T]``.
                - codes (torch.Tensor): Codebook indices with shape ``[B, N, T]``.
        """
        z_q = 0
        z_p = []
        codes = []
        dims = self._codebook_dim_offsets
        n_codebooks = bisect_right(dims, latents.shape[1]) - 1
        for i in range(n_codebooks):
            j, k = dims[i], dims[i + 1]
            z_p_i, codes_i = self.quantizers[i].decode_latents(latents[:, j:k, :])
            z_p.append(z_p_i)
            codes.append(codes_i)

            z_q_i = self.quantizers[i].out_proj(z_p_i)
            z_q = z_q + z_q_i

        return z_q, torch.cat(z_p, dim=1), torch.stack(codes, dim=1)


class ResidualUnit(nn.Module):
    def __init__(self, dim: int = 16, dilation: int = 1):
        super().__init__()
        pad = ((7 - 1) * dilation) // 2
        self.block = nn.Sequential(
            Snake1d(dim),
            nn.Conv1d(dim, dim, kernel_size=7, dilation=dilation, padding=pad),
            Snake1d(dim),
            nn.Conv1d(dim, dim, kernel_size=1),
        )

    def forward(self, x):
        y = self.block(x)
        pad = (x.shape[-1] - y.shape[-1]) // 2
        if pad > 0:
            x = x[..., pad:-pad]
        return x + y


class EncoderBlock(nn.Module):
    def __init__(self, dim: int = 16, stride: int = 1):
        super().__init__()
        self.block = nn.Sequential(
            ResidualUnit(dim // 2, dilation=1),
            ResidualUnit(dim // 2, dilation=3),
            ResidualUnit(dim // 2, dilation=9),
            Snake1d(dim // 2),
            nn.Conv1d(
                dim // 2,
                dim,
                kernel_size=2 * stride,
                stride=stride,
                padding=math.ceil(stride / 2),
            ),
        )

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


class Encoder(nn.Module):
    def __init__(
        self,
        d_model: int = 64,
        strides: list = [2, 4, 8, 8],
        d_latent: int = 64,
    ):
        super().__init__()
        # Create first convolution
        self.block = [nn.Conv1d(1, d_model, kernel_size=7, padding=3)]

        # Create EncoderBlocks that double channels as they downsample by `stride`
        for stride in strides:
            d_model *= 2
            self.block += [EncoderBlock(d_model, stride=stride)]

        # Create last convolution
        self.block += [
            Snake1d(d_model),
            nn.Conv1d(d_model, d_latent, kernel_size=3, padding=1),
        ]

        # Wrap black into nn.Sequential
        self.block = nn.Sequential(*self.block)
        self.enc_dim = d_model

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


class DecoderBlock(nn.Module):
    def __init__(self, input_dim: int = 16, output_dim: int = 8, stride: int = 1):
        super().__init__()
        self.block = nn.Sequential(
            Snake1d(input_dim),
            nn.ConvTranspose1d(
                input_dim,
                output_dim,
                kernel_size=2 * stride,
                stride=stride,
                padding=math.ceil(stride / 2),
                output_padding=stride % 2,
            ),
            ResidualUnit(output_dim, dilation=1),
            ResidualUnit(output_dim, dilation=3),
            ResidualUnit(output_dim, dilation=9),
        )

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


class Decoder(nn.Module):
    def __init__(
        self,
        input_channel,
        channels,
        rates,
        d_out: int = 1,
    ):
        super().__init__()

        # Add first conv layer
        layers = [nn.Conv1d(input_channel, channels, kernel_size=7, padding=3)]

        # Add upsampling + MRF blocks
        for i, stride in enumerate(rates):
            input_dim = channels // 2**i
            output_dim = channels // 2 ** (i + 1)
            layers += [DecoderBlock(input_dim, output_dim, stride)]

        # Add final conv layer
        layers += [
            Snake1d(output_dim),
            nn.Conv1d(output_dim, d_out, kernel_size=7, padding=3),
            nn.Tanh(),
        ]

        self.model = nn.Sequential(*layers)

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


class DAC(nn.Module, LayerwiseOffloadableModuleMixin):
    layerwise_offload_dit_group_enabled = False
    layer_names = ["encoder.block", "decoder.model"]

    def __init__(
        self,
        config: DacVAEConfig,
    ):
        super().__init__()

        self.continuous = config.continuous
        self.decoder_dim = config.decoder_dim
        self.decoder_rates = config.decoder_rates
        self.encoder_dim = config.encoder_dim
        self.encoder_rates = config.encoder_rates
        self.hop_length = math.prod(config.encoder_rates)
        self.sample_rate = config.sample_rate

        if config.latent_dim is None:
            latent_dim = config.encoder_dim * (2 ** len(config.encoder_rates))
        else:
            latent_dim = config.latent_dim

        self.latent_dim = latent_dim

        if config.load_encoder:
            self.encoder = Encoder(config.encoder_dim, config.encoder_rates, latent_dim)

        if not config.continuous:
            self.n_codebooks = config.n_codebooks
            self.codebook_size = config.codebook_size
            self.codebook_dim = config.codebook_dim
            self.quantizer = ResidualVectorQuantize(
                input_dim=latent_dim,
                n_codebooks=config.n_codebooks,
                codebook_size=config.codebook_size,
                codebook_dim=config.codebook_dim,
                quantizer_dropout=config.quantizer_dropout,
            )
        else:
            self.quant_conv = torch.nn.Conv1d(latent_dim, 2 * latent_dim, 1)
            self.post_quant_conv = torch.nn.Conv1d(latent_dim, latent_dim, 1)

        if config.load_decoder:
            self.decoder = Decoder(
                latent_dim,
                config.decoder_dim,
                config.decoder_rates,
            )

        self.apply(self.init_weights)

    @staticmethod
    def init_weights(m):
        if isinstance(m, nn.Conv1d):
            nn.init.trunc_normal_(m.weight, std=0.02)
            nn.init.constant_(m.bias, 0)

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

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

    def preprocess(self, audio_data, sample_rate):
        if sample_rate is None:
            sample_rate = self.sample_rate
        assert sample_rate == self.sample_rate

        length = audio_data.shape[-1]
        right_pad = math.ceil(length / self.hop_length) * self.hop_length - length
        audio_data = nn.functional.pad(audio_data, (0, right_pad))

        return audio_data

    def encode(
        self,
        audio_data: torch.Tensor,
        n_quantizers: int = None,
    ):
        """Encode audio data into latent representations.

        This method processes audio through the encoder network and optionally applies
        vector quantization (in VQ mode) or projects to a Gaussian distribution (in
        continuous mode) to produce latent representations.

        Args:
            audio_data (torch.Tensor): Audio data to encode, with shape ``[B, 1, T]``.
            n_quantizers (int, optional): Number of quantizers to use. If ``None``,
                all quantizers are used. Only applicable in VQ mode (``continuous=False``).

        Returns:
            tuple: A tuple containing:
                - z (torch.Tensor): Encoded representation. In VQ mode, this is the
                  quantized continuous representation with shape ``[B, D, T]``. In
                  continuous mode, this is a ``DiagonalGaussianDistribution`` object.
                - codes (torch.Tensor or None): Codebook indices with shape ``[B, N, T]``
                  in VQ mode, ``None`` in continuous mode.
                - latents (torch.Tensor or None): Projected latents with shape ``[B, N*D, T]``
                  in VQ mode, ``None`` in continuous mode.
                - commitment_loss (torch.Tensor): Commitment loss scalar.
                - codebook_loss (torch.Tensor): Codebook loss scalar.

        Note:
            In continuous mode, the encoded representation is projected through a
            quantization convolution layer and wrapped in a ``DiagonalGaussianDistribution``
            for VAE training.
        """
        z = self.encoder(audio_data)  # [B x D x T]
        if not self.continuous:
            z, codes, latents, commitment_loss, codebook_loss = self.quantizer(
                z, n_quantizers
            )
        else:
            z = self.quant_conv(z)  # [B x 2D x T]
            z = DiagonalGaussianDistribution(z)
            codes, latents, commitment_loss, codebook_loss = None, None, 0, 0

        return z, codes, latents, commitment_loss, codebook_loss

    def decode(self, z: torch.Tensor):
        """Decode latent representations back to audio waveforms.

        This method takes latent representations (either quantized from VQ mode or sampled
        from the posterior in continuous mode) and reconstructs the corresponding audio
        through the decoder network.

        Args:
            z (torch.Tensor): Latent representation to decode, with shape ``[B, D, T]``.
                In VQ mode (``continuous=False``), this is the quantized continuous
                representation. In continuous mode (``continuous=True``), this is sampled
                from the posterior distribution.

        Returns:
            torch.Tensor: Decoded audio data with shape ``[B, 1, T']``. The output length
            T' is determined by the decoder's upsampling rates and may differ from the
            input temporal dimension T.

        Note:
            In continuous mode (``continuous=True``), the input is first passed through
            a post-quantization convolution layer before being fed to the decoder.
        """
        if not self.continuous:
            audio = self.decoder(z)
        else:
            z = self.post_quant_conv(z)
            audio = self.decoder(z)

        return audio

    def forward(
        self,
        audio_data: torch.Tensor,
        sample_rate: int = None,
        n_quantizers: int = None,
    ):
        """Model forward pass.

        Args:
            audio_data (torch.Tensor): Audio to encode, shape [B, 1, T].
            sample_rate (int, optional): Sample rate in Hz. Defaults to
                ``self.sample_rate`` when ``None``.
            n_quantizers (int, optional): Number of quantizers to use. When ``None``,
                all quantizers are used. Only used in VQ mode (``continuous=False``).

        Returns:
            dict: A dictionary containing different keys depending on the mode:

            **VQ Mode (``continuous=False``):**
                - "audio" (torch.Tensor): Decoded audio, shape [B, 1, length].
                - "z" (torch.Tensor): Quantized continuous representation, shape [B, D, T].
                - "codes" (torch.Tensor): Codebook indices, shape [B, N, T].
                - "latents" (torch.Tensor): Projected latents, shape [B, N*D, T].
                - "vq/commitment_loss" (torch.Tensor): Commitment loss.
                - "vq/codebook_loss" (torch.Tensor): Codebook loss.

            **Continuous Mode (``continuous=True``):**
                - "audio" (torch.Tensor): Decoded audio, shape [B, 1, length].
                - "z" (torch.Tensor): Latent representation, shape [B, D, T].
                - "kl_loss" (torch.Tensor): KL divergence loss (for VAE training).
        """
        length = audio_data.shape[-1]
        audio_data = self.preprocess(audio_data, sample_rate)
        if not self.continuous:
            z, codes, latents, commitment_loss, codebook_loss = self.encode(
                audio_data, n_quantizers
            )

            x = self.decode(z)
            return {
                "audio": x[..., :length],
                "z": z,
                "codes": codes,
                "latents": latents,
                "vq/commitment_loss": commitment_loss,
                "vq/codebook_loss": codebook_loss,
            }
        else:
            posterior, _, _, _, _ = self.encode(audio_data, n_quantizers)
            z = posterior.sample()
            x = self.decode(z)

            kl_loss = posterior.kl(dims=(1, 2))
            kl_loss = kl_loss.mean()

            return {
                "audio": x[..., :length],
                "z": z,
                "kl_loss": kl_loss,
            }


EntryClass = DAC
