"""Reproduce an illustrative 24-bit CRC study. Python 3; plotting requires matplotlib.
Not an acoustic receiver, hardware trial, authentication or safety validation.
Run in place: python generate.py
"""
import json, random, csv, math
from pathlib import Path
SEED = 41002
TRIALS = 100_000
BIT_ERROR_RATES = (0.0001, 0.001, 0.01, 0.05)
FRAME_BITS = 24


def crc8(data: bytes) -> int:
    crc = 0
    for byte in data:
        crc ^= byte
        for _ in range(8):
            crc = ((crc << 1) ^ 0x07) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF
    return crc


def make_frame(payload: int) -> bytes:
    body = payload.to_bytes(2, "big")  # illustrative 8-bit address + 8-bit command
    return body + bytes((crc8(body),))


def valid(frame: bytes) -> bool:
    return len(frame) == 3 and frame[2] == crc8(frame[:2])


def flip_mask(frame: bytes, bits: list[int]) -> bytes:
    changed = bytearray(frame)
    for bit in bits:
        changed[bit // 8] ^= 1 << (7 - bit % 8)
    return bytes(changed)


def random_ber_trials(rng: random.Random, ber: float) -> dict:
    rejected = false_accepts = corrupted = 0
    for _ in range(TRIALS):
        payload = rng.randrange(1 << 16)
        frame = make_frame(payload)
        errors = [bit for bit in range(FRAME_BITS) if rng.random() < ber]
        corrupted += bool(errors)
        received = flip_mask(frame, errors)
        if valid(received):
            if int.from_bytes(received[:2], "big") != payload:
                false_accepts += 1
        else:
            rejected += 1
    return {
        "bit_error_probability": ber,
        "trials": TRIALS,
        "frames_rejected_by_crc": rejected,
        "rejection_rate": rejected / TRIALS,
        "corrupted_frames_accepted_as_wrong_payload": false_accepts,
        "false_accept_rate_per_transmitted_frame": false_accepts / TRIALS,
        "corrupted_frames": corrupted,
        "undetected_corruption_fraction": false_accepts / corrupted if corrupted else 0.0,
    }


def burst_trials() -> list[dict]:
    rows = []
    for length in range(1, 17):
        positions = list(range(FRAME_BITS - length + 1))
        interior_count = 1 if length <= 2 else 1 << (length - 2)
        undetected = 0
        # CRC syndrome is payload-independent for a fixed error mask.
        frame = make_frame(0)
        for start in positions:
            for interior in range(interior_count):
                bits = [start] if length == 1 else [start, start + length - 1]
                for offset in range(1, length - 1):
                    if interior & (1 << (offset - 1)):
                        bits.append(start + offset)
                received = flip_mask(frame, bits)
                if valid(received) and received != frame:
                    undetected += 1
        opportunities = (1 << 16) * len(positions) * interior_count
        undetected *= 1 << 16
        rows.append({
            "burst_length_bits": length,
            "tested_burst_positions_per_payload": len(positions),
            "error_patterns_per_position": interior_count,
            "opportunities": opportunities,
            "undetected_corrupt_frames": undetected,
            "undetected_fraction": undetected / opportunities,
        })
    return rows


def noise_only_acceptance(rng: random.Random) -> dict:
    trials = TRIALS
    accepted = 0
    for _ in range(trials):
        candidate = bytes((rng.randrange(256), rng.randrange(256), rng.randrange(256)))
        accepted += valid(candidate)
    return {
        "uniform_random_24_bit_candidates": trials,
        "crc_valid_candidates_accepted": accepted,
        "observed_acceptance_rate": accepted / trials,
        "ideal_crc8_acceptance_probability": 1 / 256,
        "scope": "Uniform random bits only; not an acoustic false-trigger probability or an authenticated command guarantee.",
    }


def run():
    rng=random.Random(SEED)
    result={"seed":SEED,"model":"Synthetic 24-bit frame: address byte, command byte, CRC byte", "crc":{"width":8,"polynomial_hex":"07","initial_hex":"00","reflect_input":False,"reflect_output":False,"xor_output_hex":"00","byte_order":"address then command; most-significant bit first"},"random_bit_error_results":[random_ber_trials(rng,p) for p in BIT_ERROR_RATES],"burst_error_results":burst_trials(),"noise_only_result":noise_only_acceptance(rng),"limits":["Bit errors are synthetic independent flips, not an acoustic channel.","CRC checks accidental corruption, not authentication, replay prevention or permission to act.","Burst counts cover every start position and endpoint-defined burst mask of lengths 1 through 16; linearity covers all 65536 payloads without separate payload simulations.","Zero observed errors do not prove zero probability."]}
    vectors=[(b"",0),(bytes([0]),0),(bytes([1]),7),(b"123456789",0xf4)]
    assert all(crc8(v)==expected for v,expected in vectors)
    original=make_frame((42<<8)|1)
    altered=(int.from_bytes(original,"big")^0x107).to_bytes(3,"big")
    assert valid(original) and valid(altered) and original[:2]!=altered[:2]
    result["algorithm_checks"]={"known_vectors":[{"bytes_hex":v.hex(),"expected_crc_hex":f"{e:02x}"} for v,e in vectors],"sample":{"address":42,"command":1,"frame_hex":original.hex(),"error_mask_hex":"000107","altered_frame_hex":altered.hex(),"altered_address":altered[0],"altered_command":altered[1],"both_pass_crc":True,"explanation":"XOR with generator polynomial 0x107 gives a nonzero undetected error. Its first/last flipped bits span nine positions. A valid check is not proof that the payload is unchanged."}}
    return result

def write_csv(path,rows):
    with path.open("w",newline="",encoding="utf-8") as f:
        w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)

def plot(result,out):
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    import matplotlib.ticker as mt
    plt.rcParams.update({"font.family":"DejaVu Sans","font.size":10,"axes.spines.top":False,"axes.spines.right":False,"svg.fonttype":"none"})
    fig,ax=plt.subplots(1,3,figsize=(14,4.5),layout="constrained")
    rows=result["random_bit_error_results"];x=[r["bit_error_probability"] for r in rows]
    ax[0].plot(x,[100*r["rejection_rate"] for r in rows],"o-",color="#166e99")
    ax[0].set(xscale="log",xlabel="Independent bit-flip probability",ylabel="Rejected / transmitted frames (%)",title="a  Corruption detected")
    vals=[100*r["false_accept_rate_per_transmitted_frame"] for r in rows]
    ax[1].plot(x,vals,"o-",color="#9a376b")
    for xx,yy,r in zip(x,vals,rows):ax[1].annotate(str(r["corrupted_frames_accepted_as_wrong_payload"]),(xx,yy),xytext=(0,7),textcoords="offset points",ha="center")
    ax[1].set(xscale="log",xlabel="Independent bit-flip probability",ylabel="Wrong payload accepted / transmitted (%)",title="b  Corruption missed")
    ax[1].set_ylim(-0.002,max(vals)*1.35)
    b=result["burst_error_results"]
    ax[2].plot([r["burst_length_bits"] for r in b],[100*r["undetected_fraction"] for r in b],"s-",color="#53648c")
    ax[2].set(xlabel="Burst length (bits)",ylabel="Undetected / enumerated burst cases (%)",title="c  Exact burst enumeration",xticks=[1,4,8,9,12,16])
    for a in ax:a.grid(alpha=.2)
    fig.suptitle("CRC-8 detects many errors, but does not certify a message",fontsize=15)
    fig.supxlabel("Panels a–b: 100,000 synthetic frames per point; labels in b are event counts. Panel c: exact counts, not sampled trials.",fontsize=9)
    for ext in ["png","svg","pdf"]:fig.savefig(out/f"crc-study.{ext}",dpi=190)
    plt.close(fig)

if __name__=="__main__":
    out=Path(__file__).resolve().parent
    result=run()
    (out/"crc-study.json").write_text(json.dumps(result,indent=2)+"\n",encoding="utf-8")
    write_csv(out/"crc-random-errors.csv",result["random_bit_error_results"])
    write_csv(out/"crc-bursts.csv",result["burst_error_results"])
    write_csv(out/"crc-random-candidates.csv",[result["noise_only_result"]])
    plot(result,out)
    print(json.dumps(result["algorithm_checks"]))
