#!/usr/bin/env python3
"""
bosch_checksum.py — reusable Bosch (TriCore ME17/MED17/EDC17) checksum core.

After you change ANY byte in a Bosch calibration/dataset block, the block's CRC32
and ADD16 stop matching and the ECU/PCMflash will reject (or run a backup). This
module recomputes them by adjusting a "dCSAdjust" dword so the targets hold again,
WITHOUT touching anything else.

Two non-obvious things this encodes (learned the hard way):
  1. ADD16 (medc17 SB_ADD16_ALGO_E): sum 16-bit LE words, but the LAST word is
     shifted << 16 before adding. So the adjust dword contribution is linear:
     needed = (target - baseline_with_adjust_zeroed) & 0xFFFFFFFF, written LE.
  2. CRC32 init = 0xFADECAFE (reflected poly 0xEDB88320). When the adjust dword is
     NOT the last thing in the region (e.g. a DEADBEEF epilog marker follows it),
     you can't just append a correction — you must SOLVE for the 4 adjust bytes via
     GF(2) linear algebra (32 single-bit CRC probes + Gaussian elimination). That's
     `solve_crc32_adjust` below. Never overwrite the DEADBEEF marker.

ALWAYS validate the result with medc17 (tools/medc17/main.py) before flashing —
this module computes, medc17 independently confirms. Two-tool agreement = safe.

Region constants below are ME17.9.11 (Kia/Hyundai GTD-B46*) DEFAULTS. They differ
per ECU family — confirm with medc17's block scan first (it prints each block's range).
"""

POLY = 0xEDB88320
_CRC_TBL = []
for _i in range(256):
    _c = _i
    for _ in range(8):
        _c = (_c >> 1) ^ POLY if _c & 1 else _c >> 1
    _CRC_TBL.append(_c)


def crc32_fade(data, init=0xFADECAFE):
    c = init
    for b in data:
        c = _CRC_TBL[(c ^ b) & 0xFF] ^ (c >> 8)
    return c & 0xFFFFFFFF


def add16(data, init=0xFADECAFE):
    """Sum 16-bit LE words; LAST word shifted << 16 (SB_ADD16_ALGO_E)."""
    s = init
    n = len(data)
    pos = 0
    while pos <= n - 4:
        s = (s + (data[pos] | (data[pos + 1] << 8))) & 0xFFFFFFFF
        pos += 2
    s = (s + ((data[pos] | (data[pos + 1] << 8)) << 16)) & 0xFFFFFFFF
    return s


def fix_add16(buf, a, b, adj, target=0xCAFEAFFE):
    """buf: bytearray. Region [a..b] inclusive, adjust dword at `adj`. In-place."""
    buf[adj:adj + 4] = b"\x00\x00\x00\x00"
    baseline = add16(bytes(buf[a:b + 1]))
    needed = (target - baseline) & 0xFFFFFFFF
    buf[adj:adj + 4] = needed.to_bytes(4, "little")
    return add16(bytes(buf[a:b + 1]))


def solve_crc32_adjust(buf, a, b, adj, target=0x35015001):
    """Solve the 4 adjust bytes at `adj` so CRC32 of [a..b] == target. Works even when
    `adj` is in the MIDDLE of the region (epilog after it). In-place. buf: bytearray."""
    buf[adj:adj + 4] = b"\x00\x00\x00\x00"
    baseline = crc32_fade(bytes(buf[a:b + 1]))
    delta = baseline ^ target
    A = []
    for bit in range(32):
        test = bytearray(buf)
        test[adj + bit // 8] ^= (1 << (bit % 8))
        A.append(baseline ^ crc32_fade(bytes(test[a:b + 1])))
    rows = [0] * 32
    for j in range(32):
        for i in range(32):
            if (A[j] >> i) & 1:
                rows[i] |= (1 << j)
    M = [(rows[i], (delta >> i) & 1) for i in range(32)]
    for col in range(32):
        piv = next((r for r in range(col, 32) if (M[r][0] >> col) & 1), -1)
        if piv < 0:
            continue
        M[col], M[piv] = M[piv], M[col]
        for r in range(32):
            if r != col and (M[r][0] >> col) & 1:
                M[r] = (M[r][0] ^ M[col][0], M[r][1] ^ M[col][1])
    x = 0
    for col in range(32):
        if M[col][1]:
            x |= (1 << col)
    buf[adj:adj + 4] = x.to_bytes(4, "little")
    return crc32_fade(bytes(buf[a:b + 1]))


# ME17.9.11 Kia/Hyundai GTD-B46* dataset block defaults (confirm per-ECU with medc17!)
ME17_9_11 = dict(
    add16=dict(a=0x20230, b=0x203B7, adj=0x203B4, target=0xCAFEAFFE),
    crc32=dict(a=0x20000, b=0x3FEFF, adj=0x3FEFC, target=0x35015001),
)

if __name__ == "__main__":
    import sys
    if len(sys.argv) < 4:
        print("usage: bosch_checksum.py <in.bin> <out.bin> <hexoffset> <hexbytes>")
        print("  edits bytes at offset, then fixes ME17.9.11 ADD16+CRC32. Validate with medc17 after.")
        sys.exit(1)
    inf, outf, off, hexb = sys.argv[1], sys.argv[2], int(sys.argv[3], 16), sys.argv[4]
    d = bytearray(open(inf, "rb").read())
    nb = bytes.fromhex(hexb)
    d[off:off + len(nb)] = nb
    print("ADD16:", hex(fix_add16(d, **ME17_9_11["add16"])))
    print("CRC32:", hex(solve_crc32_adjust(d, **ME17_9_11["crc32"])))
    open(outf, "wb").write(bytes(d))
    print("wrote", outf, "— now validate: python tools/medc17/main.py", outf)
