#!/usr/bin/env python3
"""Compara paridad, checksum de Internet y CRC-32 frente a distintos tipos de errores.

Toma frames aleatorios de 64 bytes, los daña de una forma conocida y cuenta cuántos
daños NO detecta cada método. La semilla es fija: el resultado es reproducible.

    python3 detect.py [intentos]
"""
import random
import struct
import sys
import zlib

FRAME_BYTES = 64


def parity(data: bytes) -> int:
    """Bit de paridad par de todo el frame."""
    return bin(int.from_bytes(data, "big")).count("1") % 2


def internet_checksum(data: bytes) -> int:
    """Suma de palabras de 16 bits con acarreo circular, complementada (RFC 1071)."""
    words = struct.unpack(f"!{len(data) // 2}H", data)
    total = sum(words)
    while total >> 16:
        total = (total & 0xFFFF) + (total >> 16)
    return ~total & 0xFFFF


METHODS = {"paridad": parity, "checksum": internet_checksum, "crc32": zlib.crc32}


def flip_bits(data: bytes, positions) -> bytes:
    out = bytearray(data)
    for pos in positions:
        out[pos // 8] ^= 0x80 >> (pos % 8)
    return bytes(out)


def one_bit(data, rng):
    return flip_bits(data, [rng.randrange(len(data) * 8)])


def two_bits(data, rng):
    return flip_bits(data, rng.sample(range(len(data) * 8), 2))


def burst(length):
    """Ráfaga: el primer y el último bit cambian; los de en medio, al azar."""

    def damage(data, rng):
        start = rng.randrange(len(data) * 8 - length)
        inner = [start + i for i in range(1, length - 1) if rng.random() < 0.5]
        return flip_bits(data, [start, start + length - 1] + inner)

    return damage


def swap_words(data, rng):
    """Intercambia dos palabras de 16 bits distintas: el orden cambia, la suma no."""
    words = list(struct.unpack(f"!{len(data) // 2}H", data))
    while True:
        i, j = rng.sample(range(len(words)), 2)
        if words[i] != words[j]:
            break
    words[i], words[j] = words[j], words[i]
    return struct.pack(f"!{len(words)}H", *words)


DAMAGES = [
    ("1 bit", one_bit),
    ("2 bits", two_bits),
    ("ráfaga de 16 bits", burst(16)),
    ("ráfaga de 32 bits", burst(32)),
    ("ráfaga de 100 bits", burst(100)),
    ("intercambio de palabras", swap_words),
]


def undetected(damage, trials, rng):
    """Cuenta, por método, los daños que dejan el mismo código de detección."""
    misses = dict.fromkeys(METHODS, 0)
    for _ in range(trials):
        data = bytes(rng.getrandbits(8) for _ in range(FRAME_BYTES))
        bad = damage(data, rng)
        for name, method in METHODS.items():
            if method(data) == method(bad):
                misses[name] += 1
    return misses


def main():
    trials = int(sys.argv[1]) if len(sys.argv) > 1 else 20000
    rng = random.Random(20260303)
    print(f"{trials} intentos por fila, frames de {FRAME_BYTES} bytes. Daños NO detectados:")
    print(f"{'daño':<25}{'paridad':>10}{'checksum':>10}{'crc32':>8}")
    for label, damage in DAMAGES:
        m = undetected(damage, trials, rng)
        print(f"{label:<25}{m['paridad']:>10}{m['checksum']:>10}{m['crc32']:>8}")


if __name__ == "__main__":
    main()
