#!/usr/bin/env python3
"""
feature_finder.py — locate a feature/config byte in ECU firmwares two ways.

Why landmark-based, not fixed-offset: the same calibration block sits at DIFFERENT
absolute offsets across models/sub-calibrations (preceding tables vary in length).
So we anchor on a byte pattern that IS constant (a curve/axis next to the value) and
read the value relative to it. This is how the Cerato cruise byte was found and then
matched across 176 ME17.9.11 siblings despite per-model offset shifts.

Modes:
  landmark  — given a hex landmark + relative offset, print the value at that spot
              for every .bin under a directory (and for a reference file). Great for
              "which firmwares have feature X enabled, and with what value".
  diff      — align two firmwares by a shared landmark (or raw) and report ONLY the
              bytes that differ in a window — strips out model/tune map divergence so
              the feature-relevant delta stands out (donor-diff).

Examples:
  # cruise byte = 2 bytes immediately BEFORE the speed-axis curve
  python feature_finder.py landmark --dir ~/fw --ref mycar.bin \
      --landmark 0808191f25324b647da3 --rel -2 --len 2

  # what differs between my OFF firmware and a donor with the feature ON, near the curve
  python feature_finder.py diff --a mycar_off.bin --b donor_on.bin \
      --landmark 0808191f25324b647da3 --before 16 --after 24
"""
import argparse, os, glob


def find_lm(d, lm):
    i = d.find(lm)
    return i if i >= 0 else None


def cmd_landmark(a):
    lm = bytes.fromhex(a.landmark)
    def val(d):
        i = find_lm(d, lm)
        if i is None:
            return None, None
        pos = i + a.rel
        return d[pos:pos + a.len].hex(), pos
    if a.ref:
        d = open(os.path.expanduser(a.ref), 'rb').read()
        v, p = val(d)
        print(f"REF {os.path.basename(a.ref)}: value={v} @ {hex(p) if p is not None else '-'}")
    files = sorted(glob.glob(os.path.join(os.path.expanduser(a.dir), '**', '*.bin'), recursive=True)) if a.dir else []
    from collections import Counter
    dist = Counter()
    for f in files:
        try:
            d = open(f, 'rb').read()
        except OSError:
            continue
        v, p = val(d)
        if v is None:
            continue
        dist[v] += 1
        if a.verbose:
            print(f"  {os.path.basename(f)[:60]:60} {v} @ {hex(p)}")
    if dist:
        print("\nvalue distribution:")
        for v, n in dist.most_common():
            print(f"  {v}  ({n} files)")


def cmd_diff(a):
    A = open(os.path.expanduser(a.a), 'rb').read()
    B = open(os.path.expanduser(a.b), 'rb').read()
    if a.landmark:
        lm = bytes.fromhex(a.landmark)
        ia, ib = find_lm(A, lm), find_lm(B, lm)
        if ia is None or ib is None:
            print("landmark not found in one of the files"); return
        print(f"landmark @ A={hex(ia)} B={hex(ib)}")
        wa = A[ia - a.before:ia + a.after]
        wb = B[ib - a.before:ib + a.after]
        diffs = [k for k in range(min(len(wa), len(wb))) if wa[k] != wb[k]]
        print(f"A window: {wa.hex()}")
        print(f"B window: {wb.hex()}")
        print(f"differing positions (rel to landmark): {[k - a.before for k in diffs]}")
    else:
        n = min(len(A), len(B))
        diffs = [k for k in range(n) if A[k] != B[k]]
        print(f"total differing bytes: {len(diffs)}")
        print("offsets:", [hex(x) for x in diffs[:200]])


def main():
    p = argparse.ArgumentParser(description="Locate feature/config bytes in ECU firmwares")
    sub = p.add_subparsers(dest='cmd', required=True)
    l = sub.add_parser('landmark'); l.set_defaults(fn=cmd_landmark)
    l.add_argument('--dir'); l.add_argument('--ref')
    l.add_argument('--landmark', required=True, help='hex pattern that is constant across models')
    l.add_argument('--rel', type=int, default=-2, help='byte offset of value relative to landmark start')
    l.add_argument('--len', type=int, default=2)
    l.add_argument('--verbose', action='store_true')
    d = sub.add_parser('diff'); d.set_defaults(fn=cmd_diff)
    d.add_argument('--a', required=True); d.add_argument('--b', required=True)
    d.add_argument('--landmark', help='align by this hex pattern (else raw whole-file diff)')
    d.add_argument('--before', type=int, default=16); d.add_argument('--after', type=int, default=24)
    a = p.parse_args()
    a.fn(a)


if __name__ == '__main__':
    main()
