#!/usr/bin/env python3
"""
LIN ADC Measurement Verification Test

This test reads ADC measurement values from the ECU over LIN and verifies
they are within expected ranges across multiple LED states.

Test cases:
  1. All LEDs off
  2. Only Red on (color=255, intensity=255)
  3. Only Green on (color=255, intensity=255)
  4. Only Blue on (color=255, intensity=255)
  5. All LEDs on (color=255, intensity=255)

Verified signals:
- VF_Frame_VS: Supply voltage (expected ~12V = ~12000 mV)
- VF_Frame_VLED: DC-DC converter output voltage feeding LEDs (expected ~5V = ~5000 mV)
- VF_Frame_Red_VF: Red LED forward voltage (0 when off, ~1500-3500 mV when on)
- VF_Frame_Green_VF: Green LED forward voltage (0 when off, ~1500-3500 mV when on)
- VF_Frame_Blue1_VF: Blue LED forward voltage (0 when off, ~1500-3500 mV when on)

Frame structures:
  ALM_Req_A (ID=0x0A, master-to-slave, 8 bytes):
  - Byte 0: AmbLightColourRed   (0-255)
  - Byte 1: AmbLightColourGreen (0-255)
  - Byte 2: AmbLightColourBlue  (0-255)
  - Byte 3: AmbLightIntensity   (0-255)
  - Byte 4: AmbLightUpdate[1:0] | (AmbLightMode[5:0] << 2)
  - Byte 5: AmbLightDuration    (0-255)
  - Byte 6: AmbLightLIDFrom     (NAD range start — set equal to LIDTo to target one node)
  - Byte 7: AmbLightLIDTo       (NAD range end)

  PWM_wo_Comp (ID=0x15, slave-to-master, 8 bytes):
  - Byte 0-1: PWM_wo_Comp_Red (16-bit, little-endian)
  - Byte 2-3: PWM_wo_Comp_Green (16-bit, little-endian)
  - Byte 4-5: PWM_wo_Comp_Blue (16-bit, little-endian)
  - Byte 6-7: VF_Frame_VS (16-bit, little-endian, value in mV)

  VF_Frame (ID=0x13, slave-to-master, 8 bytes):
  - Byte 0-1: VF_Frame_Red_VF (16-bit, little-endian, value in mV)
  - Byte 2-3: VF_Frame_Green_VF (16-bit, little-endian, value in mV)
  - Byte 4-5: VF_Frame_Blue1_VF (16-bit, little-endian, value in mV)
  - Byte 6-7: VF_Frame_VLED (16-bit, little-endian, value in mV)
"""

import argparse
import logging
import time
import sys
from pylin import LinBusManager, LinDevice22
from pymumclient import MelexisUniversalMaster
from config import *

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)-15s %(levelname)-8s %(message)s'
)
logger = logging.getLogger(__name__)

# ADC measurement expected ranges (in mV)
VS_EXPECTED_MIN_MV = 10000   # 10.0V minimum
VS_EXPECTED_MAX_MV = 14000   # 14.0V maximum
VS_EXPECTED_NOMINAL_MV = 12000  # 12.0V nominal

VLED_EXPECTED_MIN_MV = 4000   # 4.0V minimum
VLED_EXPECTED_MAX_MV = 6000   # 6.0V maximum
VLED_EXPECTED_NOMINAL_MV = 5000  # 5.0V nominal

# LED forward voltage ranges when LEDs are off
LED_VF_OFF_MIN_MV = 0       # 0V minimum (off)
LED_VF_OFF_MAX_MV = 500     # 0.5V maximum (off, allowing some noise)

# LED forward voltage ranges when LEDs are on
LED_VF_ON_MIN_MV = 1500     # 1.5V minimum (on)
LED_VF_ON_MAX_MV = 3500     # 3.5V maximum (on)

# Settle time after changing LED state (seconds)
LED_SETTLE_TIME = 1.0


def read_alm_status(lin_dev):
    """Read ALM_Status frame and return (ALMNadNo, raw_bytes)."""
    try:
        response = lin_dev.send_message(
            master_to_slave=False,
            frame_id=ALM_STATUS_FRAME['frame_id'],
            data_length=ALM_STATUS_FRAME['length'],
            data=None,
        )
        if response and len(response) >= ALM_STATUS_FRAME['length']:
            parsed = unpack_frame(ALM_STATUS_FRAME, response)
            return parsed['ALMNadNo'], response
        return None, None
    except Exception as e:
        logger.error(f"Failed to read ALM_Status: {e}")
        return None, None


def send_config_frame(lin_dev, calibration=0, enable_derating=1,
                      enable_compensation=1, max_lm=3840):
    """Send ConfigFrame to configure calibration, derating and compensation."""
    data = pack_frame(CONFIG_FRAME,
        ConfigFrame_Calibration=calibration,
        ConfigFrame_EnableDerating=enable_derating,
        ConfigFrame_EnableCompensation=enable_compensation,
        ConfigFrame_MaxLM=max_lm,
    )
    lin_dev.send_message(
        master_to_slave=True,
        frame_id=CONFIG_FRAME['frame_id'],
        data_length=CONFIG_FRAME['length'],
        data=data,
    )


def set_led_color(lin_dev, nad, red, green, blue, intensity):
    """Set LED color and intensity via ALM_Req_A frame."""
    data = pack_frame(ALM_REQ_A_FRAME,
        AmbLightColourRed=red,
        AmbLightColourGreen=green,
        AmbLightColourBlue=blue,
        AmbLightIntensity=intensity,
        AmbLightLIDFrom=nad,
        AmbLightLIDTo=nad,
    )
    lin_dev.send_message(
        master_to_slave=True,
        frame_id=ALM_REQ_A_FRAME['frame_id'],
        data_length=ALM_REQ_A_FRAME['length'],
        data=data,
    )


def read_pwm_wo_comp_frame(lin_dev):
    """
    Read PWM_wo_Comp frame from slave.

    Returns:
        tuple: (raw_bytes, parsed_dict) or (None, None) on failure.
               parsed_dict keys: pwm_red, pwm_green, pwm_blue, vs_mv
    """
    try:
        response = lin_dev.send_message(
            master_to_slave=False,
            frame_id=PWM_WO_COMP_FRAME['frame_id'],
            data_length=PWM_WO_COMP_FRAME['length'],
            data=None,
        )
        if response and len(response) >= PWM_WO_COMP_FRAME['length']:
            s = unpack_frame(PWM_WO_COMP_FRAME, response)
            return response, {
                'pwm_red':   s['PWM_wo_Comp_Red'],
                'pwm_green': s['PWM_wo_Comp_Green'],
                'pwm_blue':  s['PWM_wo_Comp_Blue'],
                'vs_mv':     s['VF_Frame_VS'],
            }
        return None, None
    except Exception as e:
        logger.error(f"Failed to read PWM_wo_Comp frame: {e}")
        return None, None


def read_vf_frame(lin_dev):
    """
    Read VF_Frame from slave.

    Returns:
        tuple: (raw_bytes, parsed_dict) or (None, None) on failure.
               parsed_dict keys: red_vf_mv, green_vf_mv, blue_vf_mv, vled_mv
    """
    try:
        response = lin_dev.send_message(
            master_to_slave=False,
            frame_id=VF_FRAME['frame_id'],
            data_length=VF_FRAME['length'],
            data=None,
        )
        if response and len(response) >= VF_FRAME['length']:
            s = unpack_frame(VF_FRAME, response)
            return response, {
                'red_vf_mv':   s['VF_Frame_Red_VF'],
                'green_vf_mv': s['VF_Frame_Green_VF'],
                'blue_vf_mv':  s['VF_Frame_Blue1_VF'],
                'vled_mv':     s['VF_Frame_VLED'],
            }
        return None, None
    except Exception as e:
        logger.error(f"Failed to read VF_Frame: {e}")
        return None, None


def sample_signal(lin_dev, signal_name, read_func, signal_key,
                  expected_min, expected_max, num_samples=5, sample_interval=0.1):
    """
    Read a signal multiple times and verify it is within expected range.

    Args:
        lin_dev: LinDevice22 instance
        signal_name: Display name for the signal
        read_func: Function to call to read the frame (returns raw, parsed)
        signal_key: Key in parsed dict to extract the signal value
        expected_min: Minimum expected value in mV
        expected_max: Maximum expected value in mV
        num_samples: Number of samples to read
        sample_interval: Delay between samples in seconds

    Returns:
        tuple: (passed, avg_voltage_mv, samples)
    """
    samples = []
    passed = True

    for i in range(num_samples):
        raw, parsed = read_func(lin_dev)

        if parsed is None:
            logger.warning(f"    Sample {i+1}/{num_samples}: No response")
            continue

        value_mv = parsed[signal_key]
        samples.append(value_mv)

        in_range = expected_min <= value_mv <= expected_max
        status = "OK" if in_range else "FAIL"
        logger.info(f"    Sample {i+1}/{num_samples}: {signal_name} = {value_mv} mV ({value_mv/1000:.2f} V) [{status}]")

        if not in_range:
            passed = False

        if i < num_samples - 1:
            time.sleep(sample_interval)

    if len(samples) == 0:
        logger.error(f"    No valid samples received for {signal_name}")
        return False, 0, samples

    avg_mv = sum(samples) / len(samples)
    return passed, avg_mv, samples


def log_signal_summary(signal_name, passed, avg_mv, samples):
    """Log summary statistics for a verified signal."""
    if len(samples) > 0:
        s_min = min(samples)
        s_max = max(samples)
        logger.info(f"    Average: {avg_mv:.0f} mV ({avg_mv/1000:.2f} V)")
        logger.info(f"    Min:     {s_min} mV ({s_min/1000:.2f} V)")
        logger.info(f"    Max:     {s_max} mV ({s_max/1000:.2f} V)")
        logger.info(f"    Result:  {'PASS' if passed else 'FAIL'}")
    else:
        logger.error(f"    Result:  FAIL (no data)")


def verify_adc_signals(lin_dev, num_samples, sample_interval,
                       expected_red_vf, expected_green_vf, expected_blue_vf):
    """
    Verify all ADC signals (VS, VLED, Red_VF, Green_VF, Blue_VF) for the current LED state.

    Args:
        lin_dev: LinDevice22 instance
        num_samples: Number of samples per signal
        sample_interval: Delay between samples in seconds
        expected_red_vf: Tuple (min_mv, max_mv) for Red forward voltage
        expected_green_vf: Tuple (min_mv, max_mv) for Green forward voltage
        expected_blue_vf: Tuple (min_mv, max_mv) for Blue forward voltage

    Returns:
        bool: True if all signals pass, False otherwise
    """
    all_passed = True

    logger.info(f"  --- VS (Supply Voltage) ---")
    vs_passed, vs_avg, vs_samples = sample_signal(
        lin_dev, "VS", read_pwm_wo_comp_frame, 'vs_mv',
        VS_EXPECTED_MIN_MV, VS_EXPECTED_MAX_MV,
        num_samples=num_samples, sample_interval=sample_interval
    )
    log_signal_summary("VS", vs_passed, vs_avg, vs_samples)
    if not vs_passed:
        all_passed = False

    logger.info(f"  --- VLED (DC-DC Voltage) ---")
    vled_passed, vled_avg, vled_samples = sample_signal(
        lin_dev, "VLED", read_vf_frame, 'vled_mv',
        VLED_EXPECTED_MIN_MV, VLED_EXPECTED_MAX_MV,
        num_samples=num_samples, sample_interval=sample_interval
    )
    log_signal_summary("VLED", vled_passed, vled_avg, vled_samples)
    if not vled_passed:
        all_passed = False

    led_checks = [
        ("Red_VF",   'red_vf_mv',   expected_red_vf),
        ("Green_VF", 'green_vf_mv', expected_green_vf),
        ("Blue_VF",  'blue_vf_mv',  expected_blue_vf),
    ]

    for signal_name, signal_key, (exp_min, exp_max) in led_checks:
        logger.info(f"  --- {signal_name} (expected {exp_min}-{exp_max} mV) ---")
        led_passed, led_avg, led_samples = sample_signal(
            lin_dev, signal_name, read_vf_frame, signal_key,
            exp_min, exp_max,
            num_samples=num_samples, sample_interval=sample_interval
        )
        log_signal_summary(signal_name, led_passed, led_avg, led_samples)
        if not led_passed:
            all_passed = False

    return all_passed


def main():
    parser = argparse.ArgumentParser(description='LIN ADC Measurement Verification Test')
    parser.add_argument('--host', default=MUM_HOST,
                       help=f'MUM IP address (default: {MUM_HOST})')
    parser.add_argument('--nad', type=lambda x: int(x, 0), default=LED_DEFAULT_NAD,
                       help=f'Node address (default: 0x{LED_DEFAULT_NAD:02X})')
    parser.add_argument('--samples', type=int, default=10,
                       help='Number of samples to read per signal (default: 10)')
    parser.add_argument('--interval', type=float, default=0.1,
                       help='Interval between samples in seconds (default: 0.1)')
    parser.add_argument('--settle-time', type=float, default=LED_SETTLE_TIME,
                       help=f'Settle time after LED state change (default: {LED_SETTLE_TIME}s)')

    args = parser.parse_args()

    # Define test cases: (name, red, green, blue, intensity,
    #                      expected_red_vf, expected_green_vf, expected_blue_vf)
    test_cases = [
        (
            "All LEDs OFF",
            0, 0, 0, 0,
            (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
            (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
            (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
        ),
        (
            "Red ON (255/255)",
            255, 0, 0, 255,
            (LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV),
            (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
            (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
        ),
        (
            "Green ON (255/255)",
            0, 255, 0, 255,
            (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
            (LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV),
            (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
        ),
        (
            "Blue ON (255/255)",
            0, 0, 255, 255,
            (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
            (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
            (LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV),
        ),
        (
            "All LEDs ON (255/255)",
            255, 255, 255, 255,
            (LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV),
            (LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV),
            (LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV),
        ),
    ]

    test_results = {}
    nad = args.nad  # may be updated below after reading ALM_Status

    try:
        logger.info(f"Connecting to MUM at {args.host}...")

        mum = MelexisUniversalMaster()
        mum.open_all(args.host)

        power_control = mum.get_device(MUM_POWER_DEVICE)
        linmaster = mum.get_device(MUM_LIN_DEVICE)

        linmaster.setup()

        lin_bus = LinBusManager(linmaster)
        lin_dev = LinDevice22(lin_bus)
        lin_dev.baudrate = LIN_BAUDRATE
        lin_dev.nad = args.nad

        power_control.power_up()
        time.sleep(0.5)

        logger.info("MUM connected and LIN bus ready")
        logger.info("=" * 70)
        logger.info("ADC MEASUREMENT VERIFICATION TEST")
        logger.info(f"Samples: {args.samples}, Interval: {args.interval}s, "
                    f"Settle: {args.settle_time}s")
        logger.info("=" * 70)

        # Wait for ADC to settle after power-up
        logger.info("Waiting for ADC to settle after power-up...")
        time.sleep(1.0)

        # Read the actual NAD from the node.  Using args.nad directly risks
        # a silent miss if the node was assigned a different NAD (e.g. via
        # auto-addressing), because AmbLightLIDFrom/LIDTo must equal ALMNadNo.
        logger.info("Reading node NAD from ALM_Status...")
        detected_nad, status_data = read_alm_status(lin_dev)
        if detected_nad is not None:
            nad = detected_nad
            data_hex = ' '.join(f'{b:02X}' for b in status_data)
            logger.info(f"Detected NAD: 0x{nad:02X}  (Status frame: {data_hex})")
        else:
            nad = args.nad
            logger.warning(f"Could not read NAD, falling back to 0x{nad:02X}")
        logger.info("=" * 70)

        # Configure: disable derating and compensation so PWM output directly
        # reflects the requested color/brightness.
        logger.info("Sending ConfigFrame: Calibration=1, Derating=0, Compensation=0")
        send_config_frame(lin_dev, calibration=1, enable_derating=0,
                          enable_compensation=0)
        time.sleep(0.1)

        # Ensure LEDs are off before starting
        set_led_color(lin_dev, nad, 0, 0, 0, 0)
        time.sleep(args.settle_time)

        for idx, (name, red, green, blue, intensity,
                  exp_red, exp_green, exp_blue) in enumerate(test_cases, 1):

            logger.info("")
            logger.info("-" * 70)
            logger.info(f"TEST {idx}/{len(test_cases)}: {name}")
            logger.info(f"  Command: R={red} G={green} B={blue} I={intensity}"
                        f"  -> NAD 0x{nad:02X}")
            logger.info("-" * 70)

            # Set LED state
            set_led_color(lin_dev, nad, red, green, blue, intensity)
            logger.info(f"  Waiting {args.settle_time}s for ADC to settle...")
            time.sleep(args.settle_time)

            # Verify all ADC signals
            passed = verify_adc_signals(
                lin_dev, args.samples, args.interval,
                exp_red, exp_green, exp_blue
            )

            test_results[name] = passed
            logger.info(f"  >> TEST {idx} {'PASS' if passed else 'FAIL'}")

        # Turn LEDs off at the end
        set_led_color(lin_dev, nad, 0, 0, 0, 0)

        # Summary
        logger.info("")
        logger.info("=" * 70)
        logger.info("TEST SUMMARY")
        logger.info("=" * 70)

        all_passed = True
        for name, passed in test_results.items():
            status = "PASS" if passed else "FAIL"
            logger.info(f"  {status} - {name}")
            if not passed:
                all_passed = False

        logger.info("-" * 70)
        if all_passed:
            logger.info("RESULT: ALL TESTS PASSED")
        else:
            logger.info("RESULT: SOME TESTS FAILED")
        logger.info("=" * 70)

        logger.info("Tearing down...")
        linmaster.teardown()
        logger.info("Done (ECU still powered)")

        sys.exit(0 if all_passed else 1)

    except KeyboardInterrupt:
        logger.info("")
        logger.info("Interrupted by user")
        try:
            set_led_color(lin_dev, nad, 0, 0, 0, 0)
            linmaster.teardown()
        except:
            pass
        sys.exit(130)
    except Exception as e:
        logger.error(f"Error: {e}", exc_info=True)
        sys.exit(1)


if __name__ == "__main__":
    main()
