"""ScrollCVAE — generates realistic scroll wheel event sequences.

Architecture mirrors JointCVAE but smaller (scroll sequences are simpler):
- Encoder: bidirectional GRU(hidden=64, layers=2)
- Decoder: unidirectional GRU(hidden=64, layers=2)
- Input/output: (delta_norm, log_Δt) per time step
- Condition: [dist_norm, log_dist, direction, viewport_norm, mode_onehot×3] = 7 dims
"""
from __future__ import annotations

import torch
import torch.nn as nn
from torch.distributions import Normal


class ScrollCVAE(nn.Module):
    def __init__(
        self,
        seq_len: int = 32,
        latent_dim: int = 16,
        hidden: int = 64,
        cond_dim: int = 7,
    ):
        super().__init__()
        self.seq_len = seq_len
        self.latent_dim = latent_dim
        self.hidden = hidden
        self.cond_dim = cond_dim
        self.feat_dim = 2  # (delta_norm, log_Δt)

        self.enc_gru = nn.GRU(
            input_size=self.feat_dim + cond_dim,
            hidden_size=hidden,
            num_layers=2,
            batch_first=True,
            bidirectional=True,
        )
        self.enc_mu = nn.Linear(hidden * 2, latent_dim)
        self.enc_logvar = nn.Linear(hidden * 2, latent_dim)

        self.dec_h0 = nn.Linear(latent_dim + cond_dim, hidden * 2)
        self.dec_gru = nn.GRU(
            input_size=latent_dim + cond_dim,
            hidden_size=hidden,
            num_layers=2,
            batch_first=True,
        )
        self.dec_out = nn.Linear(hidden, self.feat_dim)

    def encode(self, seq: torch.Tensor, cond: torch.Tensor):
        B, T, _ = seq.shape
        c_exp = cond.unsqueeze(1).expand(B, T, self.cond_dim)
        x_in = torch.cat([seq, c_exp], dim=-1)
        _, h_n = self.enc_gru(x_in)
        h_cat = torch.cat([h_n[-2], h_n[-1]], dim=-1)
        return self.enc_mu(h_cat), self.enc_logvar(h_cat)

    def decode(self, z: torch.Tensor, cond: torch.Tensor):
        B = z.shape[0]
        zc = torch.cat([z, cond], dim=-1)
        h0_flat = self.dec_h0(zc)
        h0 = h0_flat.view(B, 2, self.hidden).permute(1, 0, 2).contiguous()
        inp = zc.unsqueeze(1).expand(B, self.seq_len, -1)
        out, _ = self.dec_gru(inp, h0)
        return self.dec_out(out)

    def reparameterise(self, mu, logvar):
        std = torch.exp(0.5 * logvar)
        return Normal(mu, std).rsample()

    def forward(self, seq, cond):
        mu, logvar = self.encode(seq, cond)
        z = self.reparameterise(mu, logvar)
        recon = self.decode(z, cond)
        return recon, mu, logvar
