#!/usr/bin/env python3
"""Generate realistic synthetic MME crash test fixture data.

Creates a proper ISO 13499 MME directory structure simulating a full
frontal crash test (56 km/h rigid barrier, Hybrid III 50th percentile
male driver). Produces ~30 channels covering all body regions needed
for injury criteria computation.

The signals use physically realistic waveforms:
- Half-sine acceleration pulses with appropriate timing and magnitude
- Neck force/moment with realistic tension-flexion patterns
- Chest deflection with viscous loading character
- Femur compressive force profiles
- Tibia axial and bending loads

All signals include pre-trigger baseline, realistic noise, and sensor
characteristics (sample rate, CFC class).

Usage:
    python generate_mme.py [output_dir]

If no output_dir is specified, generates into tests/fixtures/sample_mme/
"""

from __future__ import annotations

import sys
from pathlib import Path

import numpy as np


# ---------------------------------------------------------------------------
# Parameters
# ---------------------------------------------------------------------------

SAMPLE_RATE = 20000.0  # Hz
DT = 1.0 / SAMPLE_RATE
PRE_TRIGGER_S = 0.010  # 10 ms pre-trigger
EVENT_DURATION_S = 0.200  # 200 ms event
TOTAL_DURATION_S = PRE_TRIGGER_S + EVENT_DURATION_S
NUM_SAMPLES = int(TOTAL_DURATION_S * SAMPLE_RATE)
PRE_TRIGGER_SAMPLES = int(PRE_TRIGGER_S * SAMPLE_RATE)

RNG = np.random.default_rng(2024)

# Time array with t=0 at trigger
TIME = np.arange(NUM_SAMPLES, dtype=np.float64) * DT - PRE_TRIGGER_S


def noise(scale: float = 0.3) -> np.ndarray:
    """Add realistic sensor noise."""
    return RNG.normal(0, scale, NUM_SAMPLES)


def half_sine(
    t_start: float,
    t_duration: float,
    amplitude: float,
    phase_shift: float = 0.0,
) -> np.ndarray:
    """Generate a half-sine pulse."""
    data = np.zeros(NUM_SAMPLES)
    mask = (TIME >= t_start) & (TIME <= t_start + t_duration)
    t_local = TIME[mask] - t_start
    data[mask] = amplitude * np.sin(np.pi * t_local / t_duration + phase_shift)
    return data


def crash_pulse(t_start: float, t_rise: float, t_fall: float, amplitude: float) -> np.ndarray:
    """Generate a more realistic crash pulse with fast rise and slower decay."""
    data = np.zeros(NUM_SAMPLES)
    # Rise phase
    mask_rise = (TIME >= t_start) & (TIME < t_start + t_rise)
    t_local = TIME[mask_rise] - t_start
    data[mask_rise] = amplitude * np.sin(np.pi * t_local / (2 * t_rise))
    # Fall phase
    mask_fall = (TIME >= t_start + t_rise) & (TIME <= t_start + t_rise + t_fall)
    t_local = TIME[mask_fall] - (t_start + t_rise)
    data[mask_fall] = amplitude * np.cos(np.pi * t_local / (2 * t_fall))
    return data


# ---------------------------------------------------------------------------
# Channel definitions
# ---------------------------------------------------------------------------

# Each channel: (code, description, unit, cfc_class, data_generator)


def gen_channels() -> list[tuple[str, str, str, int, np.ndarray]]:
    channels = []

    # === HEAD ===
    # Head X acceleration: main frontal pulse, ~45g peak
    head_ax = crash_pulse(0.0, 0.020, 0.060, 45.0) + noise(0.8)
    channels.append(("11HEAD0000ACXA", "Head CG X Acceleration", "g", 1000, head_ax))

    # Head Y acceleration: small lateral component
    head_ay = half_sine(0.005, 0.070, 8.0) * np.cos(5 * np.pi * (TIME - 0.005) / 0.070) + noise(0.4)
    head_ay[TIME < 0.005] = noise(0.4)[TIME < 0.005]
    channels.append(("11HEAD0000ACYA", "Head CG Y Acceleration", "g", 1000, head_ay))

    # Head Z acceleration: downward component
    head_az = crash_pulse(0.003, 0.018, 0.055, 22.0) + noise(0.5)
    channels.append(("11HEAD0000ACZA", "Head CG Z Acceleration", "g", 1000, head_az))

    # Head angular velocity (for future BrIC)
    head_avx = half_sine(0.005, 0.060, 15.0) + noise(0.3)
    channels.append(("11HEAD0000AVXA", "Head Angular Velocity X", "rad/s", 600, head_avx))
    head_avy = half_sine(0.008, 0.055, 25.0) + noise(0.3)
    channels.append(("11HEAD0000AVYA", "Head Angular Velocity Y", "rad/s", 600, head_avy))
    head_avz = half_sine(0.006, 0.050, 10.0) + noise(0.2)
    channels.append(("11HEAD0000AVZA", "Head Angular Velocity Z", "rad/s", 600, head_avz))

    # === NECK (upper) ===
    # Neck Fz: tension pulse ~3200 N
    neck_fz = crash_pulse(0.005, 0.015, 0.045, 3200.0) + noise(15.0)
    channels.append(("11NECKUP00FOZA", "Upper Neck Axial Force", "N", 600, neck_fz))

    # Neck Fx: shear force ~800 N
    neck_fx = crash_pulse(0.008, 0.012, 0.040, 800.0) + noise(8.0)
    channels.append(("11NECKUP00FOXA", "Upper Neck Shear Force X", "N", 600, neck_fx))

    # Neck My: flexion moment ~85 Nm
    neck_my = crash_pulse(0.010, 0.015, 0.050, 85.0) + noise(1.5)
    channels.append(("11NECKUP00MOYA", "Upper Neck Moment Y (Flexion)", "N·m", 600, neck_my))

    # Neck Mx: lateral moment ~25 Nm
    neck_mx = half_sine(0.012, 0.045, 25.0) + noise(0.8)
    channels.append(("11NECKUP00MOXA", "Upper Neck Moment X", "N·m", 600, neck_mx))

    # === CHEST ===
    # Chest X acceleration
    chest_ax = crash_pulse(0.008, 0.020, 0.055, 38.0) + noise(0.6)
    channels.append(("11CHST0000ACXA", "Chest T12 X Acceleration", "g", 180, chest_ax))

    # Chest Y acceleration
    chest_ay = half_sine(0.010, 0.060, 6.0) + noise(0.3)
    channels.append(("11CHST0000ACYA", "Chest T12 Y Acceleration", "g", 180, chest_ay))

    # Chest Z acceleration
    chest_az = crash_pulse(0.009, 0.018, 0.050, 18.0) + noise(0.4)
    channels.append(("11CHST0000ACZA", "Chest T12 Z Acceleration", "g", 180, chest_az))

    # Chest deflection: ramps up to ~36mm, then recovers
    chest_dc = np.zeros(NUM_SAMPLES)
    mask_load = (TIME >= 0.010) & (TIME < 0.055)
    t_load = TIME[mask_load] - 0.010
    chest_dc[mask_load] = 36.0 * np.sin(np.pi * t_load / 0.090)
    mask_unload = (TIME >= 0.055) & (TIME < 0.130)
    t_unload = TIME[mask_unload] - 0.055
    chest_dc[mask_unload] = 36.0 * np.sin(np.pi * 0.045 / 0.090) * np.exp(-t_unload / 0.030)
    chest_dc += noise(0.2)
    channels.append(("11CHST0000DCXA", "Chest Deflection", "mm", 180, chest_dc))

    # === PELVIS ===
    pelv_ax = crash_pulse(0.012, 0.022, 0.060, 35.0) + noise(0.5)
    channels.append(("11PELV0000ACXA", "Pelvis X Acceleration", "g", 180, pelv_ax))
    pelv_ay = half_sine(0.015, 0.055, 5.0) + noise(0.3)
    channels.append(("11PELV0000ACYA", "Pelvis Y Acceleration", "g", 180, pelv_ay))
    pelv_az = crash_pulse(0.014, 0.018, 0.050, 15.0) + noise(0.4)
    channels.append(("11PELV0000ACZA", "Pelvis Z Acceleration", "g", 180, pelv_az))

    # === FEMUR ===
    # Left femur: compressive pulse ~4800 N (negative = compression in SAE)
    femur_l = -crash_pulse(0.020, 0.015, 0.045, 4800.0) + noise(20.0)
    channels.append(("11FEMRLE00FOZA", "Left Femur Axial Force", "N", 600, femur_l))

    # Right femur: slightly lower
    femur_r = -crash_pulse(0.022, 0.014, 0.042, 4200.0) + noise(18.0)
    channels.append(("11FEMRRI00FOZA", "Right Femur Axial Force", "N", 600, femur_r))

    # === TIBIA (left upper) ===
    tibia_fz = -crash_pulse(0.025, 0.012, 0.040, 5500.0) + noise(25.0)
    channels.append(("11TIBILEUOFOZA", "Left Upper Tibia Axial Force", "N", 600, tibia_fz))

    tibia_mx = half_sine(0.028, 0.040, 80.0) + noise(2.0)
    channels.append(("11TIBILEUOMOXA", "Left Upper Tibia Moment X", "N·m", 600, tibia_mx))

    tibia_my = half_sine(0.027, 0.038, 120.0) + noise(2.5)
    channels.append(("11TIBILEUOMOYA", "Left Upper Tibia Moment Y", "N·m", 600, tibia_my))

    # === SEAT BELT ===
    belt_fo = crash_pulse(0.005, 0.025, 0.070, 6500.0) + noise(30.0)
    channels.append(("11BELT0000FOXA", "Lap Belt Force", "N", 60, belt_fo))

    # Shoulder belt force
    sbelt_fo = crash_pulse(0.003, 0.020, 0.065, 5800.0) + noise(25.0)
    channels.append(("11BELTUPOOFOZA", "Shoulder Belt Force", "N", 60, sbelt_fo))

    # === VEHICLE / STRUCTURAL ===
    # B-pillar acceleration
    bpil_ax = crash_pulse(0.001, 0.015, 0.040, 55.0) + noise(1.0)
    channels.append(("10BPILLE00ACXA", "Left B-Pillar X Acceleration", "g", 60, bpil_ax))

    # Steering column displacement
    stcl_ds = np.zeros(NUM_SAMPLES)
    mask = (TIME >= 0.015) & (TIME < 0.100)
    t_local = TIME[mask] - 0.015
    stcl_ds[mask] = 45.0 * (1 - np.exp(-t_local / 0.025))
    stcl_ds[TIME >= 0.100] = stcl_ds[mask][-1] if np.any(mask) else 0
    stcl_ds += noise(0.3)
    channels.append(("10STCL0000DSXA", "Steering Column Displacement", "mm", 60, stcl_ds))

    return channels


# ---------------------------------------------------------------------------
# MME writer
# ---------------------------------------------------------------------------


def write_mme(output_dir: Path) -> None:
    """Write a complete MME directory structure."""
    output_dir.mkdir(parents=True, exist_ok=True)

    # --- MME.ini ---
    ini_content = """\
[Test]
test_number = IMPAKT_SYNTH_001
test_date = 2024-06-15
test_type = Full Frontal Rigid Barrier
test_speed = 56.3
test_facility = Impakt Synthetic Data Generator
test_standard = FMVSS 208
description = Synthetic frontal crash test for Impakt development and testing

[Vehicle]
vehicle_make = Synthetic
vehicle_model = TestCar
vehicle_year = 2024
vehicle_vin = IMPAKT0000000001
vehicle_mass = 1523.0
vehicle_type = Passenger Car

[Barrier]
barrier_type = Rigid
impact_angle = 0
overlap_percent = 100
impact_side = Front

[Dummy]
dummy_type = Hybrid III 50th Percentile Male
dummy_serial = H3-50M-SYNTH-001
dummy_position = Driver
dummy_mass = 78.0
restraint_type = 3-point belt + frontal airbag
seat_position = Mid-track, mid-height

[DataAcquisition]
sample_rate = 20000
pre_trigger_ms = 10
total_duration_ms = 210
num_channels = 28
"""

    (output_dir / "MME.ini").write_text(ini_content, encoding="utf-8")

    # --- Channel files ---
    ch_dir = output_dir / "channels"
    ch_dir.mkdir(exist_ok=True)

    channels = gen_channels()

    for code, description, unit, cfc, data in channels:
        # Write .chn header
        chn_content = f"""\
[Channel]
channel_code = {code}
description = {description}
unit = {unit}
sample_rate = {SAMPLE_RATE:.0f}
num_samples = {NUM_SAMPLES}
dt = {DT:.10f}
pre_trigger = {PRE_TRIGGER_SAMPLES}
cfc = {cfc}
data_format = ascii
"""
        (ch_dir / f"{code}.chn").write_text(chn_content, encoding="utf-8")

        # Write .dat data (ASCII, one value per line)
        np.savetxt(str(ch_dir / f"{code}.dat"), data, fmt="%.8f")

    print(f"Generated MME fixture: {output_dir}")
    print(f"  {len(channels)} channels, {NUM_SAMPLES} samples each")
    print(f"  Sample rate: {SAMPLE_RATE:.0f} Hz")
    print(
        f"  Duration: {PRE_TRIGGER_S * 1000:.0f} ms pre-trigger + {EVENT_DURATION_S * 1000:.0f} ms event"
    )
    print(
        f"  Total size: ~{sum(len(data) for _, _, _, _, data in channels) * 12 / 1024 / 1024:.1f} MB"
    )


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    if len(sys.argv) > 1:
        out = Path(sys.argv[1])
    else:
        out = Path(__file__).parent / "sample_mme"

    write_mme(out)
