#!/usr/bin/env python3
"""Compare two pin captures after aligning their first falling data edge.

Usage: python3 compare.py hardware.json simulation.json [--max-offset-us 200]
Timestamps are nanoseconds; 1 = low, 2 = high. No third-party packages needed.
Without a tolerance, exit status checks level sequences only, not timing accuracy.
"""
import argparse
import hashlib
import json
import math
import statistics
from pathlib import Path


def load(path):
    raw = Path(path).read_bytes()
    record = json.loads(raw)
    for channel in ('w0', 'w1'):
        edges = record[channel]['edges']
        if not edges:
            raise ValueError(f'{path}: empty {channel}')
        previous = -1
        for t, level in edges:
            if not isinstance(t, (int, float)) or not math.isfinite(t) or t < 0 or t < previous:
                raise ValueError(f'{path}: invalid or unordered timestamp')
            if level not in (1, 2):
                raise ValueError(f'{path}: unsupported level {level}')
            previous = t
    try:
        origin = next(t for t, level in record['w0']['edges'] if level == 1)
    except StopIteration:
        raise ValueError(f'{path}: no falling data edge') from None
    aligned = {c: [(t - origin, v) for t, v in record[c]['edges'] if t >= origin]
               for c in ('w0', 'w1')}
    if any(not edges for edges in aligned.values()):
        raise ValueError(f'{path}: empty channel after alignment')
    return aligned, hashlib.sha256(raw).hexdigest()


def compare(hardware, simulation, tolerance=None):
    a, hardware_hash = load(hardware)
    b, simulation_hash = load(simulation)
    report = {'alignment': 'first falling edge of w0; timestamps in ns',
              'hardware_sha256': hardware_hash, 'simulation_sha256': simulation_hash,
              'max_offset_us': tolerance, 'channels': {}}
    passed = True
    for c in ('w0', 'w1'):
        same = [v for _, v in a[c]] == [v for _, v in b[c]]
        result = {'hardware_edges': len(a[c]), 'simulation_edges': len(b[c]),
                  'levels_match': same}
        passed &= same
        if same:
            delta = [(y[0] - x[0]) / 1000 for x, y in zip(a[c], b[c])]
            result.update(min_offset_us=min(delta), median_offset_us=statistics.median(delta),
                          max_offset_us=max(delta), max_absolute_offset_us=max(map(abs, delta)))
            if tolerance is not None:
                result['within_tolerance'] = max(map(abs, delta)) <= tolerance
                passed &= result['within_tolerance']
        report['channels'][c] = result
    report['passed'] = passed
    report['timing_checked'] = tolerance is not None
    return report


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('hardware')
    parser.add_argument('simulation')
    parser.add_argument('--max-offset-us', type=float)
    args = parser.parse_args()
    if args.max_offset_us is not None and (not math.isfinite(args.max_offset_us) or args.max_offset_us < 0):
        parser.error('max-offset-us must be finite and nonnegative')
    try:
        result = compare(args.hardware, args.simulation, args.max_offset_us)
    except (OSError, ValueError, KeyError, TypeError) as error:
        parser.exit(2, f'Invalid capture: {error}\n')
    print(json.dumps(result, indent=2))
    raise SystemExit(0 if result['passed'] else 1)
