#!/usr/bin/env python3
"""CC0: bounded LZ4 frame experiment, not a general frame parser or benchmark."""
import ctypes as C
import ctypes.util
import datetime
import hashlib
import json
import pathlib
import platform
import struct

class FrameInfo(C.Structure):
    _fields_ = [("blockSizeID", C.c_int), ("blockMode", C.c_int),
                ("contentChecksumFlag", C.c_int), ("frameType", C.c_int),
                ("contentSize", C.c_ulonglong), ("dictID", C.c_uint),
                ("blockChecksumFlag", C.c_int)]

class Preferences(C.Structure):
    _fields_ = [("frameInfo", FrameInfo), ("compressionLevel", C.c_int),
                ("autoFlush", C.c_uint), ("favorDecSpeed", C.c_uint),
                ("reserved", C.c_uint * 3)]

lib = C.CDLL(ctypes.util.find_library("lz4") or "liblz4.so.1")
for name, args, result in [
    ("LZ4_versionString", [], C.c_char_p),
    ("LZ4F_compressFrameBound", [C.c_size_t, C.POINTER(Preferences)], C.c_size_t),
    ("LZ4F_compressFrame", [C.c_void_p, C.c_size_t, C.c_void_p, C.c_size_t, C.POINTER(Preferences)], C.c_size_t),
    ("LZ4F_createDecompressionContext", [C.POINTER(C.c_void_p), C.c_uint], C.c_size_t),
    ("LZ4F_freeDecompressionContext", [C.c_void_p], C.c_size_t),
    ("LZ4F_decompress", [C.c_void_p, C.c_void_p, C.POINTER(C.c_size_t), C.c_void_p, C.POINTER(C.c_size_t), C.c_void_p], C.c_size_t),
    ("LZ4F_isError", [C.c_size_t], C.c_uint),
    ("LZ4F_getErrorName", [C.c_size_t], C.c_char_p),
]:
    fn = getattr(lib, name)
    fn.argtypes, fn.restype = args, result

PAYLOAD = b"A" * 65536 + b"B" * 65536
sha = lambda value: hashlib.sha256(value).hexdigest()

def checked(code):
    if lib.LZ4F_isError(code):
        raise RuntimeError(lib.LZ4F_getErrorName(code).decode())
    return code

def frame(content_checksum=False, content_size=False):
    prefs = Preferences()
    prefs.frameInfo.blockSizeID = 4
    prefs.frameInfo.blockMode = 1
    prefs.frameInfo.blockChecksumFlag = 1
    prefs.frameInfo.contentChecksumFlag = int(content_checksum)
    prefs.frameInfo.contentSize = len(PAYLOAD) if content_size else 0
    size = checked(lib.LZ4F_compressFrameBound(len(PAYLOAD), C.byref(prefs)))
    out = C.create_string_buffer(size)
    length = checked(lib.LZ4F_compressFrame(out, size, PAYLOAD, len(PAYLOAD), C.byref(prefs)))
    return out.raw[:length]

def split_fixture(data):
    assert data[:4] == bytes.fromhex("04224d18")
    flags = data[4]
    assert flags & 0x20 and flags & 0x10 and not flags & 1
    pos = 7 + (8 if flags & 8 else 0)
    header, blocks = data[:pos], []
    while True:
        size = struct.unpack_from("<I", data, pos)[0]
        if size == 0:
            break
        assert not size & 0x80000000, "Fixture must contain compressed blocks"
        end = pos + 4 + size + 4
        assert end <= len(data)
        blocks.append(data[pos:end])
        pos = end
    assert len(blocks) == 2
    assert len(data[pos:]) == 4 + (4 if flags & 4 else 0)
    return header, blocks, data[pos:]

def decode(data):
    context = C.c_void_p()
    checked(lib.LZ4F_createDecompressionContext(C.byref(context), 100))
    offset, chunks, hint, error = 0, [], 1, None
    try:
        for _ in range(1000):
            src = C.create_string_buffer(data[offset:])
            src_size = C.c_size_t(len(data) - offset)
            dst = C.create_string_buffer(8192)
            dst_size = C.c_size_t(len(dst))
            hint = lib.LZ4F_decompress(context, dst, C.byref(dst_size), src, C.byref(src_size), None)
            if lib.LZ4F_isError(hint):
                error = lib.LZ4F_getErrorName(hint).decode()
                break
            offset += src_size.value
            chunks.append(dst.raw[:dst_size.value])
            if hint == 0 or (src_size.value == 0 and dst_size.value == 0):
                break
        else:
            raise AssertionError("Decoder made too many calls")
    finally:
        checked(lib.LZ4F_freeDecompressionContext(context))
    output = b"".join(chunks)
    complete = error is None and hint == 0
    return {"error": error, "frameComplete": complete,
            "inputFullyConsumed": offset == len(data), "consumedBytes": offset,
            "needsMoreInput": error is None and hint != 0,
            "successfulCallOutputBytes": len(output),
            "outputSha256": sha(output), "exactPayload": output == PAYLOAD,
            "accepted": complete and offset == len(data) and output == PAYLOAD}

def run():
    rows = []
    for policy, checksum, size in [
        ("block-only", False, False), ("block-and-content", True, False),
        ("block-and-size", False, True),
    ]:
        original = frame(checksum, size)
        header, blocks, footer = split_fixture(original)
        variants = {
            "clean": original,
            "reordered": header + blocks[1] + blocks[0] + footer,
            "second-block-removed": header + blocks[0] + footer,
        }
        if policy == "block-only":
            damaged = bytearray(original)
            damaged[len(header) + 5] ^= 1
            variants["payload-bit-flipped"] = bytes(damaged)
            variants["endmark-truncated"] = original[:-1]
            variants["additional-frame"] = original + original
        for change, data in variants.items():
            row = {"id": policy + "/" + change, "policy": policy, "change": change,
                   "frameBytes": len(data), "frameSha256": sha(data), **decode(data)}
            rows.append(row)
    by_id = {row["id"]: row for row in rows}
    for policy in ("block-only", "block-and-content", "block-and-size"):
        assert by_id[policy + "/clean"]["accepted"]
    for case in ("reordered", "second-block-removed"):
        assert by_id["block-only/" + case]["frameComplete"]
        assert not by_id["block-only/" + case]["exactPayload"]
        assert by_id["block-and-content/" + case]["error"] == "ERROR_contentChecksum_invalid"
    assert by_id["block-and-size/reordered"]["frameComplete"]
    assert not by_id["block-and-size/reordered"]["exactPayload"]
    assert by_id["block-and-size/second-block-removed"]["error"] == "ERROR_frameSize_wrong"
    assert by_id["block-only/payload-bit-flipped"]["error"] == "ERROR_blockChecksum_invalid"
    assert by_id["block-only/endmark-truncated"]["needsMoreInput"]
    assert by_id["block-only/endmark-truncated"]["exactPayload"]
    assert by_id["block-only/additional-frame"]["frameComplete"]
    assert not by_id["block-only/additional-frame"]["inputFullyConsumed"]
    assert not by_id["block-only/additional-frame"]["accepted"]
    return {"schemaVersion": 1, "generatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
            "classification": "Deterministic synthetic API experiment; no benchmark or corruption-rate estimate.",
            "environment": {"python": platform.python_version(), "liblz4": lib.LZ4_versionString().decode(),
                            "platform": platform.platform()},
            "method": {"payload": "65536 ASCII A bytes followed by 65536 ASCII B bytes",
                       "expectedBytes": len(PAYLOAD), "expectedSha256": sha(PAYLOAD),
                       "independentBlocks": True, "blockMaximumBytes": 65536,
                       "dictionary": None, "blockChecksums": True,
                       "decoderOptions": "NULL: checksums verified",
                       "outputBufferBytes": 8192,
                       "errorOutput": "Only bytes from successful calls counted; error-call sizes not used.",
                       "seed": None, "harnessSha256": sha(pathlib.Path(__file__).read_bytes())},
            "observations": rows}

if __name__ == "__main__":
    print(json.dumps(run(), indent=2))
