from model import (
    Transformer,
    ModelArgs,
    MusicalPositionEmbedTransformer,
)
from model_onnx import (
    Transformer as TransformerONNX,
    TransformerOneStep as TransformerOneStepONNX,
)
from data import Vocab
from data_gen import Batch
import math
import torch
import pytest
import random
from test_fixtures import LittleTrainer

s0 = [
    int(
        (
            2 * (i + 1)
            + round(math.sqrt(2 * (i + 1)))
            - round(math.sqrt(2 * (i + 1))) ** 2
        )
        / 2
        + 1
    )
    for i in range(100)
]

s1 = [2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 0, 0, 0]

s2 = [random.randint(2, 127) for i in range(128)]
s3 = [random.randint(2, 10) for i in range(48)]


# training on a batch that is empty (all pads) should not crash
@pytest.mark.parametrize("enable_flash", [True, False])
def test_empty_batch(enable_flash):
    smallargs = ModelArgs(256, 4, 4, 128, dropout=0.1, enable_flash=enable_flash)
    t = Transformer(smallargs).cuda()
    trainer = LittleTrainer(t)

    data = torch.zeros(1, 10, dtype=torch.long)
    batch_size = 16
    input = data[:, :-1].repeat(batch_size, 1).cuda()
    target = data[:, 1:].repeat(batch_size, 1).cuda()

    loss, last_pred = trainer.step(input, target)

    assert loss == 0
    for param in t.parameters():
        assert (param.grad != 0).view(-1).sum() == 0


@pytest.mark.parametrize("sequence", [s0, s1, s2, s3])
def test_simple_sequences(sequence):
    smallargs = ModelArgs(256, 4, 4, 128, dropout=0.1)
    t = Transformer(smallargs).cuda()
    trainer = LittleTrainer(t)

    seq_len = min(512, len(sequence))
    data = torch.zeros(1, seq_len, dtype=torch.long)
    data[0, 0] = 1
    data[0, 1:seq_len] = torch.tensor(sequence[: seq_len - 1], dtype=torch.long)

    input = data[:, :-1]
    target = data[:, 1:]

    batch_size = 16
    input = input.repeat(batch_size, 1).cuda()
    target = target.repeat(batch_size, 1).cuda()

    for i in range(100):
        _, last_pred = trainer.step(input, target)

    last_res = torch.argmax(last_pred, dim=-1)
    last_res[target == 0] = 0

    last_res = last_res.cpu()
    target = target.cpu()

    match_count = 0
    for i in range(batch_size):
        if torch.equal(last_res[i], target[i]):
            match_count += 1

    assert match_count / batch_size > 0.9

    # and using cache
    t.eval()
    with torch.no_grad():
        for i in range(seq_len - 1):
            pred = t.forward(input[0, i].view(1, 1), start_pos=i)
            if target[0, i].item() != 0:
                assert torch.argmax(pred, dim=-1) == target[0, i]


@pytest.mark.parametrize("enable_flash", [True, False])
@pytest.mark.parametrize("enable_cross", [True, False])
def test_causal(enable_flash, enable_cross):
    smallargs = ModelArgs(
        256,
        4,
        4,
        128,
        dropout=0.1,
        enable_flash=enable_flash,
        enable_cross_attention=enable_cross,
    )
    t = Transformer(smallargs).cuda()
    trainer = LittleTrainer(t)

    data = torch.arange(1, 21, dtype=torch.long).view(1, -1).repeat(8, 1)
    data[:, 0] = 1
    for i in range(data.shape[0]):
        data[i, 3:] = i + 2
    data = data.cuda()
    input = data[:, :-1]
    target = data[:, 1:]

    if enable_cross:
        encoder_out = (
            torch.normal(0, 0.2, (1, 5, smallargs.cross_attention_embedding_dim))
            .repeat(data.shape[0], 1, 1)
            .cuda()
        )
    else:
        encoder_out = None

    for i in range(200):
        _, last_pred = trainer.step(input, target, encoder_out=encoder_out)

    # no good chance of guessing the rest given only the first 3
    t.eval()

    pred_first3 = t.forward(input[:, :3], start_pos=0, encoder_out=encoder_out)[0, 2]
    assert (
        pred_first3[2:10].min() > pred_first3[0:2].max()
        and pred_first3[2:10].min() > pred_first3[10:].max()
    )
    assert (pred_first3[2:10] - pred_first3[2]).abs().max() < 1.0

    # no good chance of guessing index 2 given the entire sequence (i.e. model is causal)
    pred_full = t.forward(input, start_pos=0, encoder_out=encoder_out)[0, 2]

    assert pred_first3.allclose(pred_full)


@pytest.mark.parametrize("enable_flash", [True, False])
def test_overfit(enable_flash):
    smallargs = ModelArgs(256, 4, 4, 128, enable_flash=enable_flash)
    t = Transformer(smallargs)
    trainer = LittleTrainer(t)

    seq_len = 128
    batch_size = 16
    data = torch.zeros(batch_size, seq_len, dtype=torch.long)
    data[:, 0] = 1  # start
    for i in range(batch_size):
        data[i, 1] = i + 2  # sequence id
        data[i, 2:] = torch.randint(2, 127, (seq_len - 2,), dtype=torch.long)

    input = data[:, :-1]
    target = data[:, 1:]

    t.cuda()
    input = input.cuda()
    target = target.cuda()

    for i in range(300):
        trainer.step(input, target)

    total = 0
    matches = 0
    for i in range(2, seq_len):
        sampler_in = input[:, :i]
        pred = torch.argmax(
            t.forward(sampler_in, 0)[:, -1],
            dim=-1,
        )
        matches += torch.count_nonzero(pred == target[:, i - 1]).item()
        total += batch_size

    print(matches / total)
    assert matches / total > 0.98


def test_aux_embeddings():
    smallargs = ModelArgs(256, 4, 4, 128, dropout=0.1)
    v = Vocab(1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 128, 1, 128)
    t = MusicalPositionEmbedTransformer(v, smallargs)
    trainer = LittleTrainer(t)

    batch_size = 2
    data = torch.zeros(batch_size, len(s0) + 1, 9, dtype=torch.long)
    data[:, :, 0] = 1
    data[0, 1:, 3] = torch.tensor(s0, dtype=torch.long)
    data[0, :, 7] = 1
    data[1, 1:, 3] = torch.tensor(s0, dtype=torch.long)
    data[1, len(s0), 3] = 99  # cruel
    data[1, :, 7] = 2

    t.cuda()

    batch = Batch(v, data, "cuda")

    for i in range(200):
        _, last_pred = trainer.step(batch)

    last_res = torch.argmax(last_pred, dim=-1)
    last_res[batch.tgt[:, :, 0] == 0] = 0

    last_res = last_res.cpu()
    target = batch.tgt_y.cpu()

    print(last_res)

    match_count = 0
    for i in range(batch_size):
        if torch.equal(last_res[i], target[i]):
            match_count += 1

    assert match_count == 2


@pytest.mark.parametrize("enable_flash", [True, False])
def test_cross_attention_identity(enable_flash):
    smallargs = ModelArgs(
        256,
        4,
        4,
        128,
        enable_flash=enable_flash,
        enable_cross_attention=True,
        cache=False,
        cross_attention_embedding_dim=256,
        dropout=0.1,
    )
    t = Transformer(smallargs)
    trainer = LittleTrainer(t)

    seq_len = 128
    batch_size = 32
    data = torch.randint(2, 3, (batch_size, seq_len), dtype=torch.long)

    encoder_output = torch.normal(0, 0.2, (batch_size, seq_len - 1, smallargs.dim))
    input = data[:, :-1]
    target = data[:, 1:]

    t.cuda()
    encoder_output = encoder_output.cuda()
    input = input.cuda()
    target = target.cuda()

    for i in range(300):
        trainer.step(input, target, encoder_out=encoder_output)

    t.eval()
    total = 0
    matches = 0
    with torch.no_grad():
        for i in range(2, seq_len):
            sampler_encoder_out = encoder_output[:, :i]
            sampler_in = input[:, :i]
            pred = torch.argmax(
                t.forward(sampler_in, start_pos=0, encoder_out=sampler_encoder_out)[
                    :, -1
                ],
                dim=-1,
            )
            matches += torch.count_nonzero(pred == target[:, i - 1]).item()
            total += batch_size

    print(matches / total)
    assert matches / total > 0.98


@pytest.mark.parametrize("enable_flash", [True, False])
def test_cross_id_one_shot(enable_flash):
    smallargs = ModelArgs(
        256,
        4,
        4,
        128,
        enable_flash=enable_flash,
        enable_cross_attention=True,
        cross_attention_embedding_dim=256,
        dropout=0.1,
    )
    t = Transformer(smallargs)
    trainer = LittleTrainer(t)

    seq_len = 2
    batch_size = 32
    data = torch.zeros(batch_size, seq_len, dtype=torch.long)
    data[:, 0] = 1  # start
    for i in range(batch_size):
        data[:, 1] = i + 2

    encoder_output = torch.normal(0, 0.2, (batch_size, seq_len - 1, smallargs.dim))
    input = data[:, :-1]
    target = data[:, 1:]

    t.cuda()
    encoder_output = encoder_output.cuda()
    input = input.cuda()
    target = target.cuda()

    for i in range(300):
        trainer.step(input, target, encoder_out=encoder_output)

    t.eval()
    total = 0
    matches = 0
    with torch.no_grad():
        for i in range(batch_size):
            sampler_encoder_out = encoder_output[i].unsqueeze(0)
            sampler_in = torch.ones(1, 1, dtype=torch.long).cuda()
            pred = torch.argmax(
                t.forward(sampler_in, start_pos=0, encoder_out=sampler_encoder_out)[
                    :, -1
                ],
                dim=-1,
            )
            matches += torch.count_nonzero(pred == target[i, 0]).item()
            total += 1

    print(matches / total)
    assert matches / total > 0.98


def build_cross_cartesian_ctx(enable_flash, enable_cross):
    smallargs = ModelArgs(
        256,
        4,
        4,
        128,
        enable_flash=enable_flash,
        enable_cross_attention=enable_cross,
        cross_attention_embedding_dim=256,
    )
    t = Transformer(smallargs)
    trainer = LittleTrainer(t)

    seq_len = 5
    batch_size = 8 * 8
    data = torch.zeros(batch_size, seq_len, dtype=torch.long)
    data[:, :] = 1
    for i in range(8):
        for j in range(8):
            data[i * 8 + j, 0] = i + 2
            data[i * 8 + j, -1] = i * 8 + j + 2

    normal_block = torch.normal(0, 1, (8, seq_len - 1, smallargs.dim))
    encoder_output = normal_block.repeat(8, 1, 1)
    print(encoder_output.shape)
    encoder_train_mask = torch.ones(batch_size, seq_len - 1, dtype=torch.bool)
    encoder_train_mask[:, -1] = False
    print(encoder_train_mask.shape)

    input = data[:, :-1]
    target = data[:, 1:]

    print(data)
    print(encoder_output)

    t.cuda()
    encoder_output = encoder_output.cuda()
    encoder_train_mask = encoder_train_mask.cuda()
    input = input.cuda()
    target = target.cuda()

    for i in range(300):
        trainer.step(
            input,
            target,
            encoder_out=encoder_output,
            encoder_out_mask=encoder_train_mask,
        )

    t.eval()

    return batch_size, seq_len, smallargs, input, target, encoder_output, t


@pytest.mark.parametrize("enable_flash", [True, False])
@pytest.mark.parametrize("enable_cross", [True, False])
def test_cross_cartesian_remember(enable_flash, enable_cross):
    (
        batch_size,
        seq_len,
        smallargs,
        input,
        target,
        encoder_output,
        t,
    ) = build_cross_cartesian_ctx(enable_flash, enable_cross)
    print("enable_flash", enable_flash, "enable_cross", enable_cross)

    total = 0
    matches = 0
    with torch.no_grad():
        for i in range(batch_size):
            pred = torch.argmax(
                t.forward(
                    input[i, :-1].unsqueeze(0),
                    start_pos=0,
                    encoder_out=encoder_output[i, :-1].unsqueeze(0),
                )[:, -1],
                dim=-1,
            )
            assert pred[0].item() == 1
            pred = torch.argmax(
                t.forward(
                    input[i, -1:].unsqueeze(0),
                    start_pos=seq_len - 2,
                    encoder_out=encoder_output[i, :-1].unsqueeze(0),
                )[:, -1],
                dim=-1,
            )
            print(i, pred, target[i, -1])
            matches += torch.count_nonzero(pred[0] == target[i, -1]).item()
            total += 1

    print(matches / total)
    assert matches / total > 0.90


def test_onnx():
    (
        batch_size,
        seq_len,
        smallargs,
        input,
        target,
        encoder_output,
        t,
    ) = build_cross_cartesian_ctx(False, True)
    t_onnx = TransformerONNX(smallargs)
    t_onnx.load_state_dict(t.state_dict(), strict=False)
    t_onnx.eval()
    t_onnx.cuda()
    t_onnx_one = TransformerOneStepONNX(smallargs)
    t_onnx_one.load_state_dict(t.state_dict(), strict=False)
    t_onnx_one.eval()
    t_onnx_one.cuda()
    total = 0
    matches = 0
    with torch.no_grad():
        for i in range(batch_size):
            pred = t.forward(
                input[i, :-2].unsqueeze(0),
                start_pos=0,
                encoder_out=encoder_output[i, :-1].unsqueeze(0),
            )
            pred_onnx, a_xks, a_xvs, *fwd_rest = t_onnx.forward(
                input[i, :-2].unsqueeze(0).repeat(2, 1),
                aux_embeddings=torch.zeros(
                    2,
                    seq_len - 3,
                    smallargs.dim,
                    dtype=torch.float,
                    device=next(t.parameters()).device,
                ),
                encoder_out=encoder_output[i, :-1].unsqueeze(0).repeat(2, 1, 1),
                encoder_valid=torch.ones(
                    2, encoder_output.shape[1] - 1, dtype=torch.bool, device="cuda"
                ),
            )

            c_xks, c_xvs, embeddings = fwd_rest

            assert torch.allclose(pred_onnx[0], pred_onnx[1])
            assert torch.allclose(embeddings[0], embeddings[1])
            pred_onnx = pred_onnx[0]
            embeddings = embeddings[0]

            assert torch.allclose(pred, pred_onnx, atol=1e-5)

            a_xks = a_xks.transpose(0, 1)
            a_xvs = a_xvs.transpose(0, 1)

            for j in range(2):
                pred = t.forward(
                    input[i, j - 2].view(1, 1),
                    start_pos=seq_len - 3 + j,
                    encoder_out=encoder_output[i, :-1].unsqueeze(0),
                )
                pred_onnx, a_xk_news, a_xv_news = t_onnx_one.forward(
                    input[i, j - 2].view(-1).unsqueeze(0).repeat(2, 1),
                    aux_embeddings=torch.zeros(
                        2,
                        1,
                        smallargs.dim,
                        dtype=torch.float,
                        device=next(t.parameters()).device,
                    ),
                    a_xks=a_xks,
                    a_xvs=a_xvs,
                    c_xks=c_xks,
                    c_xvs=c_xvs,
                    encoder_valid=torch.ones(
                        2, encoder_output.shape[1] - 1, dtype=torch.bool, device="cuda"
                    ),
                )

                a_xks = torch.cat([a_xks, a_xk_news], dim=0)
                a_xvs = torch.cat([a_xvs, a_xv_news], dim=0)

                assert torch.allclose(pred[0], pred_onnx[0], atol=1e-5)
                assert torch.allclose(pred[0], pred_onnx[1], atol=1e-5)

            matches += torch.count_nonzero(
                torch.argmax(pred_onnx[-1]) == target[i, -1]
            ).item()
            total += 1

    print(matches / total)
    assert matches / total > 0.90
