#!/usr/bin/env python3
"""gen_vectors.py - Generate test vectors for comp_decomp_sync XSIM testbench.

Produces sync_rtl/comp_decomp/TB/vectors/comp_decomp_input.hex
Each line is a 32-bit hex value (8 hex chars, no spaces):
  bits[31:26] = padding (0)
  bits[25:14] = expected[11:0]
  bits[13:2]  = coeff_in[11:0]
  bit[1]      = d[0]   (d[4:0] split across bits [6:2] and bit[1])
  bit[0]      = mode (0=compress, 1=decompress)

Revised layout (cleaner, 32 bits):
  bits[31:20] = expected[11:0]
  bits[19:8]  = coeff_in[11:0]
  bits[7:3]   = d[4:0]
  bit[2]      = mode (0=compress, 1=decompress)
  bits[1:0]   = padding (0)

FIPS 203 formulas:
  Compress_q(x, d) = round((2^d / Q) * x) mod 2^d
                   = ((x * 2^d + Q//2) // Q) & ((1 << d) - 1)
  Decompress_q(y, d) = round((Q / 2^d) * y)
                     = (y * Q + (1 << (d-1))) // (1 << d)

Tests cover d in {4, 5, 10, 11} (ML-KEM standard values) plus edge d=1.
"""

import os
import sys

Q = 3329

OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vectors")
OUTPUT_FILE = os.path.join(OUTPUT_DIR, "comp_decomp_input.hex")


def compress_q(x: int, d: int) -> int:
    """Compress_q(x, d) per FIPS 203."""
    if d == 0:
        return 0
    two_d = 1 << d
    # round((2^d / Q) * x) = floor((x * 2^d + Q/2) / Q)
    val = (x * two_d + Q // 2) // Q
    return val & (two_d - 1)


def decompress_q(y: int, d: int) -> int:
    """Decompress_q(y, d) per FIPS 203."""
    if d == 0:
        return 0
    two_d = 1 << d
    # round((Q / 2^d) * y) = floor((y * Q + 2^(d-1)) / 2^d)
    val = (y * Q + (two_d >> 1)) // two_d
    return val % Q


def pack_vector(expected: int, coeff_in: int, d: int, mode: int) -> int:
    """Pack a single test vector into a 32-bit value."""
    val = 0
    val |= (expected & 0xFFF) << 20   # bits 31:20
    val |= (coeff_in & 0xFFF) << 8    # bits 19:8
    val |= (d       & 0x1F)  << 3    # bits 7:3
    val |= (mode    & 0x1)   << 2    # bit  2
    # bits 1:0 are padding = 0
    return val


def generate() -> list[int]:
    """Generate all test vectors. Returns list of packed 32-bit values."""
    vectors: list[int] = []

    def add_vector(x: int, d: int, mode: int) -> None:
        if mode == 0:
            exp = compress_q(x, d)
        else:
            exp = decompress_q(x, d)
        vectors.append(pack_vector(exp, x, d, mode))

    # Standard ML-KEM d values
    d_values = [4, 5, 10, 11]

    # Test coefficient values: edges and mid-range
    coeffs = [
        0,            # zero
        1,            # minimal
        3328,         # max (Q-1)
        1000,         # mid-range
        2000,         # mid-range
        1664,         # Q/2
        42,           # small
    ]

    # ---- COMPRESS (mode=0) ----
    for d in d_values:
        for c in coeffs:
            add_vector(c, d, 0)

        # Compress edge: max input
        add_vector(3328, d, 0)

        # Compress: some systematic sweep
        for c in [0, 500, 1000, 1500, 2000, 2500, 3000, 3328]:
            add_vector(c, d, 0)

    # ---- DECOMPRESS (mode=1) ----
    # For decompress, input is in [0, 2^d-1]
    for d in d_values:
        two_d_mask = (1 << d) - 1

        # Zero
        add_vector(0, d, 1)

        # Max in range
        add_vector(two_d_mask, d, 1)

        # Mid-range
        mid = two_d_mask // 2
        if mid > 0:
            add_vector(mid, d, 1)
            add_vector(mid - 1, d, 1) if mid > 1 else None
            add_vector(mid + 1, d, 1) if mid + 1 <= two_d_mask else None

        # Systematic sweep through valid range
        step = max(1, two_d_mask // 8)
        for y in range(0, two_d_mask + 1, step):
            add_vector(y, d, 1)

    # ---- Edge case: d=1 (minimum non-zero) ----
    add_vector(0, 1, 0)
    add_vector(3328, 1, 0)
    add_vector(0, 1, 1)
    add_vector(1, 1, 1)

    return vectors


def write_vectors(vectors: list[int]) -> None:
    """Write vectors to hex file."""
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    with open(OUTPUT_FILE, "w") as f:
        for v in vectors:
            # 32 bits = 8 hex digits
            f.write(f"{v:08X}\n")

    print(f"Generated {len(vectors)} test vectors -> {OUTPUT_FILE}")


def main() -> int:
    vectors = generate()
    write_vectors(vectors)

    # Print statistics and samples
    print(f"\nTotal vectors: {len(vectors)}")
    compress_count = sum(1 for v in vectors if ((v >> 2) & 1) == 0)
    decompress_count = sum(1 for v in vectors if ((v >> 2) & 1) == 1)
    print(f"  Compress:   {compress_count}")
    print(f"  Decompress: {decompress_count}")

    print("\nSample vectors (first 5):")
    for i, v in enumerate(vectors[:5]):
        exp     = (v >> 20) & 0xFFF
        coeff   = (v >> 8)  & 0xFFF
        d       = (v >> 3)  & 0x1F
        mode    = (v >> 2)  & 0x1
        op = "COMPRESS" if mode == 0 else "DECOMPRESS"
        if mode == 0:
            print(f"  [{i}] {op} x={coeff:04d} d={d:02d} expected={exp:04d}")
        else:
            print(f"  [{i}] {op} y={coeff:04d} d={d:02d} expected={exp:04d}")

    return 0


if __name__ == "__main__":
    sys.exit(main())
