"""
DAQ Streaming Acquisition for Fringe Counting
===============================================

Acquire continuous photodetector data using the NI USB-6009 DAQ in
streaming mode. Use this script to verify your DAQ setup before adding
motor control.

In the Gaussian Beams lab, you read single voltage samples with
task.read(). For fringe counting, you need the DAQ to sample at a
precise rate for a fixed duration — this is called "timed" or "finite"
acquisition. The key difference is cfg_samp_clk_timing(), which tells
the DAQ hardware to use its internal clock.

Quick Test:
    Run this script with the interferometer aligned but the motor off.
    Gently tap the optical table — you should see fringes appear in
    the time-domain trace.

Key Concepts:
    - FINITE acquisition: collect a fixed number of samples at a precise rate
    - cfg_samp_clk_timing: configures the DAQ's internal sample clock
    - Differential mode: measures voltage between AI+ and AI- pins,
      rejecting common-mode noise (important for small fringe signals)

For fringe counting, the fringe frequency depends on mirror velocity:

    f_fringe = 2v / λ

For HeNe (λ = 632.8 nm) at v = 0.5 mm/s:  f_fringe ≈ 1581 Hz
Your sample rate must satisfy Nyquist:  f_sample > 2 × f_fringe

Hardware:
    - NI USB-6009 DAQ
    - Photodetector connected in differential mode
      (signal to AI0+ on pin 2, ground to AI0- on pin 3)

Software:
    pip install nidaqmx numpy matplotlib

Usage:
    python 02_daq_streaming.py

Author: PHYS 4430
"""

import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime

try:
    import nidaqmx
    from nidaqmx.constants import AcquisitionType, TerminalConfiguration
    NIDAQMX_AVAILABLE = True
except ImportError:
    print("nidaqmx not available — install with: pip install nidaqmx")
    NIDAQMX_AVAILABLE = False


def list_daq_devices():
    """
    List all connected NI DAQ devices.

    Returns:
        List of device name strings (e.g., ["Dev1"])
    """
    if not NIDAQMX_AVAILABLE:
        print("NI-DAQmx not available")
        return []

    system = nidaqmx.system.System.local()
    devices = []
    for device in system.devices:
        print(f"  {device.name}: {device.product_type}")
        devices.append(device.name)
    return devices


def acquire_finite(device="Dev1", channel="ai0", sample_rate=10000,
                   duration_s=2.0, voltage_range=10.0):
    """
    Acquire a fixed number of samples at a precise rate (FINITE mode).

    This is the streaming pattern you need for fringe counting. Compare
    to the Gaussian Beams single-read pattern (task.read() with no timing
    configuration) — here, cfg_samp_clk_timing tells the DAQ hardware to
    sample at an exact rate using its internal clock.

    Uses differential mode to reduce ground loop noise.

    Parameters:
        device: DAQ device name (e.g., "Dev1")
        channel: Analog input channel (e.g., "ai0")
        sample_rate: Samples per second (Hz)
        duration_s: Total acquisition time (s)
        voltage_range: Expected voltage range (±V)

    Returns:
        time_array: Time values (s)
        voltage_array: Voltage values (V)
    """
    if not NIDAQMX_AVAILABLE:
        raise RuntimeError("NI-DAQmx not available")

    num_samples = int(sample_rate * duration_s)
    print(f"Acquiring {num_samples} samples at {sample_rate} Hz "
          f"({duration_s} s)...")

    with nidaqmx.Task() as task:
        task.ai_channels.add_ai_voltage_chan(
            f"{device}/{channel}",
            terminal_config=TerminalConfiguration.DIFF,
            min_val=-voltage_range,
            max_val=voltage_range
        )
        task.timing.cfg_samp_clk_timing(
            rate=sample_rate,
            sample_mode=AcquisitionType.FINITE,
            samps_per_chan=num_samples
        )
        # Read back the actual rate — the USB-6009 may coerce to the
        # nearest rate its clock can achieve
        actual_rate = task.timing.samp_clk_rate
        if abs(actual_rate - sample_rate) / sample_rate > 0.01:
            print(f"  WARNING: DAQ coerced rate from {sample_rate} to "
                  f"{actual_rate:.1f} Hz "
                  f"({(actual_rate - sample_rate)/sample_rate*100:+.1f}%)")
        # This blocks until all samples are collected
        data = task.read(
            number_of_samples_per_channel=num_samples,
            timeout=duration_s + 5.0
        )

    time_array = np.arange(num_samples) / actual_rate
    voltage_array = np.array(data)

    print(f"  Voltage range: {voltage_array.min():.4f} to "
          f"{voltage_array.max():.4f} V")
    print(f"  Peak-to-peak: {voltage_array.max() - voltage_array.min():.4f} V")

    return time_array, voltage_array


def plot_time_domain(time_array, voltage_array, save_path=None):
    """
    Plot the acquired time-domain signal.

    Shows the full trace and a zoomed-in view so you can see
    individual fringes.

    Parameters:
        time_array: Time values (s)
        voltage_array: Voltage values (V)
        save_path: If provided, save figure to this path
    """
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))

    t_ms = time_array * 1000

    # Full trace
    ax1.plot(t_ms, voltage_array, 'b-', linewidth=0.5)
    ax1.set_title('Full Signal')
    ax1.set_xlabel('Time (ms)')
    ax1.set_ylabel('Voltage (V)')
    ax1.grid(True, alpha=0.3)

    # Zoomed view (first 50 ms or full signal if shorter)
    if t_ms[-1] > 100:
        mask = t_ms <= 50
        ax2.plot(t_ms[mask], voltage_array[mask], 'b-', linewidth=0.5)
        ax2.set_title('Zoomed View (first 50 ms)')
    else:
        ax2.plot(t_ms, voltage_array, 'b-', linewidth=0.5)
        ax2.set_title('Zoomed View')
    ax2.set_xlabel('Time (ms)')
    ax2.set_ylabel('Voltage (V)')
    ax2.grid(True, alpha=0.3)

    plt.tight_layout()

    if save_path:
        plt.savefig(save_path, dpi=150, bbox_inches='tight')
        print(f"Plot saved to: {save_path}")

    plt.show()


def main():
    """Acquire and analyze streaming DAQ data."""
    print("=" * 55)
    print("  DAQ STREAMING ACQUISITION FOR FRINGE COUNTING")
    print("=" * 55)

    if not NIDAQMX_AVAILABLE:
        print("\nNI-DAQmx is not installed. Install with: pip install nidaqmx")
        return

    # List available devices
    print("\nSearching for DAQ devices...")
    devices = list_daq_devices()
    if not devices:
        print("No DAQ devices found. Check USB connection.")
        return

    # Get user input
    device = input(f"\nDAQ device name (default {devices[0]}): ").strip()
    device = device if device else devices[0]

    rate = input("Sample rate in Hz (default 10000): ").strip()
    sample_rate = int(rate) if rate else 10000

    dur = input("Duration in seconds (default 2.0): ").strip()
    duration = float(dur) if dur else 2.0

    # Acquire data
    time_array, voltage_array = acquire_finite(
        device=device, sample_rate=sample_rate, duration_s=duration
    )

    # Save data
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    data_file = f"daq_stream_{timestamp}.csv"
    np.savetxt(data_file,
               np.column_stack([time_array, voltage_array]),
               delimiter=',',
               header='Time (s),Voltage (V)',
               comments='')
    print(f"Data saved to: {data_file}")

    # Plot
    plot_time_domain(
        time_array, voltage_array,
        save_path=f"daq_stream_{timestamp}.png"
    )


if __name__ == "__main__":
    main()
