#!/usr/bin/env python3
"""
Generate piecewise-linear LUT arrays for IndSensorLUT.cpp from calibrated xlsx.

Reads A0Calibration/data/Sensor{0,1,2,5}.xlsx (columns: mm_val, avg_adc),
resamples onto a uniform mm grid over the active region, enforces strict
monotonicity, and prints C++ arrays ready to paste into IndSensorLUT.cpp.

Usage:  python generate_lut.py  [--mm-min 4.0] [--mm-max 16.0] [--step 0.1]
                                [--sensors 0,1,2,5]
"""

import argparse
from pathlib import Path
import numpy as np
import pandas as pd

DATA_DIR = Path(__file__).parent / 'data'


def build_lut(mm, adc, grid):
    order = np.argsort(mm)
    mm, adc = mm[order], adc[order]
    # Drop duplicate mm (keep mean) so np.interp is well-defined
    uniq, inv = np.unique(mm, return_inverse=True)
    if len(uniq) != len(mm):
        adc = np.array([adc[inv == i].mean() for i in range(len(uniq))])
        mm = uniq
    lut = np.interp(grid, mm, adc)
    lut_i = np.clip(np.round(lut).astype(int), 0, 65535)
    # Enforce strict monotonic increasing
    for i in range(1, len(lut_i)):
        if lut_i[i] <= lut_i[i - 1]:
            lut_i[i] = lut_i[i - 1] + 1
    return lut_i


def format_array(name, values, per_row=10):
    out = [f'static const uint16_t {name}[{len(values)}] = {{']
    for i in range(0, len(values), per_row):
        row = ', '.join(f'{v:4d}' for v in values[i:i + per_row])
        sep = '' if i + per_row >= len(values) else ','
        out.append('  ' + row + sep)
    out.append('};')
    return '\n'.join(out)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--mm-min', type=float, default=4.0)
    ap.add_argument('--mm-max', type=float, default=16.0)
    ap.add_argument('--step',   type=float, default=0.1)
    ap.add_argument('--sensors', type=str, default='0,1,2,5')
    args = ap.parse_args()

    sensors = [int(s) for s in args.sensors.split(',')]
    n = int(round((args.mm_max - args.mm_min) / args.step)) + 1
    grid = np.round(args.mm_min + np.arange(n) * args.step, 4)

    print(f'// Active range: {args.mm_min}..{args.mm_max} mm @ {args.step} mm '
          f'({n} entries per sensor)')
    print()

    for sn in sensors:
        path = DATA_DIR / f'Sensor{sn}.xlsx'
        if not path.exists():
            print(f'// [skip] {path} not found')
            continue
        df = pd.read_excel(path)
        df = df.dropna(subset=['mm_val', 'avg_adc'])
        in_range = (df['mm_val'] >= args.mm_min - args.step) & \
                   (df['mm_val'] <= args.mm_max + args.step)
        if in_range.sum() < 2:
            print(f'// [skip] Sensor{sn}: insufficient points in active range')
            continue
        lut_i = build_lut(df['mm_val'].to_numpy(),
                          df['avg_adc'].to_numpy(), grid)
        print(f'// Sensor{sn} — {len(df)} raw points, '
              f'ADC {lut_i[0]}..{lut_i[-1]}')
        print(format_array(f'ind{sn}LUT', lut_i))
        print()

    print('// IndSensorLUT struct initializers:')
    for sn in sensors:
        print(f'const IndSensorLUT ind{sn}LUTCal = '
              f'{{ ind{sn}LUT, {n}, {args.mm_min}f, {args.step}f }};')


if __name__ == '__main__':
    main()
