"""gen_vectors.py - Test vector generator for sha3 module.

Generates vectors for G (SHA3-512), H (SHA3-256), J (SHAKE-256) modes
and computes expected outputs using the Python reference implementation.
"""

import os
import random
import sys

# Add test_framework/lib to path for VectorGenerator base class
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'lib'))

# Add Python reference path
_REF_PATH = os.path.expanduser(
    "~/Dev/server_code/python_project/PQC_2025/A_ML_KEM_v0")
_REF_PATH = os.path.expanduser(_REF_PATH)
sys.path.insert(0, _REF_PATH)

import SHA_3

from vector_gen import VectorGenerator


def _bits_to_hex(bits_str, num_bits):
    """Convert binary string (LSB-first: bits_str[0] = bit 0) to hex.

    Returns hex with MSB-nibble first (matching Verilog %%X format).
    Each group of 4 bits forms a nibble: bit[i+3:i] → two hex chars.
    """
    result = []
    for i in range(0, num_bits, 4):
        nib = 0
        for j in range(4):
            pos = i + j
            if pos < len(bits_str) and bits_str[pos] == '1':
                nib |= (1 << j)
        result.insert(0, '0123456789ABCDEF'[nib])
    return ''.join(result)


def _hex_to_bits(hex_str, num_bits):
    """Convert hex string (MSB-first) to binary string (LSB-first).

    Returns a binary string where result[0] = bit 0 (LSB).
    """
    hex_val = int(hex_str, 16)
    bits = ''
    for i in range(num_bits):
        bits += '1' if (hex_val & (1 << i)) else '0'
    return bits


def _random_bits(length):
    """Generate a random binary string of given length."""
    val = random.getrandbits(length)
    bits = ''
    for i in range(length):
        bits += '1' if (val & (1 << i)) else '0'
    return bits


class Sha3VectorGenerator(VectorGenerator):
    """Generates test vectors for the sha3_top module."""

    def generate_one(self, params: dict) -> dict:
        """Generate a single test vector.

        For G mode:
          - d: 256-bit random binary string
          - k: 8-bit random binary string
          - Input to RTL: d||k packed into 512-bit value (data_i[263:0])
          - Expected: G(d, k) = 512-bit binary string

        For H mode:
          - ek: 256-bit random binary string (single-block)
          - Input to RTL: ek in data_i[255:0]
          - Expected: H(ek) = 256-bit binary string

        For J mode:
          - z: 256-bit random binary string
          - c: 256-bit random binary string
          - Input to RTL: z||c in data_i[511:0]
          - Expected: J(z, c) = 256-bit binary string

        Args:
            params: dict with 'mode' key ('G', 'H', 'J').

        Returns:
            dict with 'input' and 'expected' keys.
        """
        mode = params.get('mode', 'G')

        if mode == 'G':
            # d: 256 bits, k: 8 bits
            d = _random_bits(256)
            k = _random_bits(8)
            msg = d + k  # 264 bits, LSB first

            # Pack into 512-bit hex value: data_i[511:0]
            # pad msg to 512 bits with zeros (upper bits = 0)
            msg_512 = msg + '0' * (512 - len(msg))
            input_hex = _bits_to_hex(msg_512, 512)

            # Expected: G(d, k) returns binary string
            expected_bits = SHA_3.G(d, k)
            expected_hex = _bits_to_hex(expected_bits, 512)

            return {
                'input': {
                    'mode': 0,
                    'data': input_hex
                },
                'expected': {
                    'hash': expected_hex
                }
            }

        elif mode == 'H':
            # ek: 256-bit random message (single-block test)
            ek = _random_bits(256)

            # Pack into 512-bit hex: data_i[511:0]
            msg_512 = ek + '0' * (512 - len(ek))
            input_hex = _bits_to_hex(msg_512, 512)

            # Expected: H(ek) returns 256-bit binary string
            expected_bits = SHA_3.H(ek)
            expected_hex = _bits_to_hex(expected_bits, 256)

            return {
                'input': {
                    'mode': 1,
                    'data': input_hex
                },
                'expected': {
                    'hash': expected_hex
                }
            }

        elif mode == 'J':
            # z: 256 bits, c: 256 bits
            z = _random_bits(256)
            c = _random_bits(256)
            msg = z + c  # 512 bits

            # Pack into 512-bit hex
            input_hex = _bits_to_hex(msg, 512)

            # Expected: J(z, c) returns 256-bit binary string
            expected_bits = SHA_3.J(z, c)
            expected_hex = _bits_to_hex(expected_bits, 256)

            return {
                'input': {
                    'mode': 2,
                    'data': input_hex
                },
                'expected': {
                    'hash': expected_hex
                }
            }

        else:
            raise ValueError(f"Unknown mode: {mode}")

    def write_hex_file(self, vectors: list[dict], filepath: str) -> None:
        """Write input vectors as "MM DDDD..." hex format (mode, data).

        Each line: "MM DDDD..." where MM is hex mode (00/01/10) and DDDD...
        is the hex-encoded 512-bit message.

        Args:
            vectors: List of vector dicts from generate_one().
            filepath: Path to write the hex file.
        """
        os.makedirs(os.path.dirname(filepath), exist_ok=True)
        with open(filepath, 'w') as f:
            for v in vectors:
                mode_val = v['input']['mode']
                data_hex = v['input']['data']
                f.write(f'{mode_val:02X} {data_hex}\n')

    def write_expected_file(self, vectors: list[dict], filepath: str) -> None:
        """Write expected output as hex strings, one per line.

        Args:
            vectors: List of vector dicts from generate_one().
            filepath: Path to write the expected hex file.
        """
        os.makedirs(os.path.dirname(filepath), exist_ok=True)
        with open(filepath, 'w') as f:
            for v in vectors:
                expected_hex = v['expected']['hash']
                f.write(f'{expected_hex}\n')
