#!/usr/bin/env python3
"""
thorlabs_to_yml.py
 generated by claude May 10 2026
Scans a folder for Thorlabs Beam M² .txt files and writes a YAML file
containing 10 fields per file:

  From filename parsing (format: ZM4_<V>V_<sg>_ZM5_<V>V_<sg>.txt):
    ZM4 V PZT   – integer voltage after "ZM4_"
    ZM4 V sg    – signal value after first "V_" (n→-, p→.)
    ZM5 V PZT   – integer voltage after "_ZM5_"
    ZM5 V sg    – signal value after second "V_" (n→-, p→.)

  From "Original Waist Measurement Results" section of each file:
    M2 x        – M² X'
    M2 y        – M² Y'
    Rayleigh Length X
    Rayleigh Length Y
    Beam Waist Position X
    Beam Waist Position Y

Usage:
    python thorlabs_to_yml.py <folder_path> [output.yml]
"""

import os
import sys
import re
from datetime import datetime, timezone


# ---------------------------------------------------------------------------
# Filename parsing
# ---------------------------------------------------------------------------

def parse_sg_value(token: str) -> str:
    """
    Convert a signal-value token like '6p2', 'np4', '10', 'n3p5' into a
    numeric string:
      - Leading 'n' → '-'
      - 'p' → '.'
    Examples:
      '6p2'  → '6.2'
      'np4'  → '-0.4'   (n with nothing before p → -0.)
      'n4'   → '-4'
      '10'   → '10'
      'n3p5' → '-3.5'
    """
    s = token
    negative = s.startswith('n')
    if negative:
        s = s[1:]          # strip leading 'n'
    s = s.replace('p', '.')
    if negative:
        # if there was nothing between 'n' and 'p', s starts with '.'
        s = '-' + s if s else '-0'
    return float(s)


def parse_filename(stem: str) -> dict:
    """
    Parse a filename stem of the form:
        ZM4_<pzt4>V_<sg4>_ZM5_<pzt5>V_<sg5>
    Returns a dict with the four fields, or empty strings on failure.
    """
    pattern = re.compile(
        r'^ZM4_(?P<pzt4>\d+)V_(?P<sg4>[A-Za-z0-9]+)_ZM5_(?P<pzt5>\d+)V_(?P<sg5>[A-Za-z0-9]+)$',
        re.IGNORECASE
    )
    m = pattern.match(stem)
    if not m:
        return {
            'ZM4 V PZT': '',
            'ZM4 V sg':  '',
            'ZM5 V PZT': '',
            'ZM5 V sg':  '',
        }
    return {
        'ZM4 V PZT': int(m.group('pzt4')),
        'ZM4 V sg':  parse_sg_value(m.group('sg4')),
        'ZM5 V PZT': int(m.group('pzt5')),
        'ZM5 V sg':  parse_sg_value(m.group('sg5')),
    }


# ---------------------------------------------------------------------------
# File content parsing
# ---------------------------------------------------------------------------

# Map the exact label text in the file to our YAML key names.
WAIST_FIELDS = {
    "M\u00b2x'":                'M2 x',               # M²x'
    "M\u00b2y'":                'M2 y',               # M²y'
    "Rayleigh Length X'":       'Rayleigh Length X',
    "Rayleigh Length Y'":       'Rayleigh Length Y',
    "Beam Waist Position X'":   'Beam Waist Position X',
    "Beam Waist Position Y'":   'Beam Waist Position Y',
}


def parse_waist_results(content: str) -> dict:
    """
    Extract the 6 values from the 'Original Waist Measurement Results' section.

    The file structure around this section is:
        * * * * * * * * * *
        Original Waist Measurement Results
        * * * * * * * * * *        ← closing stars on the SAME block
        Parameter;Unit;Result;
        M²x'; ;1.44;
        ...
        (blank line or next * * * block starts a new section)

    So we locate the heading line, skip the very next '* * *' line, then
    collect data lines until we hit another '* * *' line or end of file.
    """
    results = {v: '' for v in WAIST_FIELDS.values()}

    lines = content.splitlines()
    in_section = False
    skip_next_stars = False

    for line in lines:
        stripped = line.strip()

        if not in_section:
            if re.search(r'Original Waist Measurement Results', stripped, re.IGNORECASE):
                in_section = True
                skip_next_stars = True   # the very next * * * line closes the heading
            continue

        # We are inside the section
        if stripped.startswith('* * *'):
            if skip_next_stars:
                skip_next_stars = False  # consume the closing header stars
                continue
            else:
                break                    # a new section starts — stop

        skip_next_stars = False          # clear flag once we've passed the stars line

        parts = [p.strip() for p in stripped.split(';')]
        if len(parts) < 3:
            continue
        label = parts[0]
        value = parts[2]
        if label in WAIST_FIELDS:
            try:
                results[WAIST_FIELDS[label]] = float(value)
            except ValueError:
                results[WAIST_FIELDS[label]] = value

    return results


def analyze_file(filepath: str, stem: str) -> dict:
    """Combine filename fields and waist-results fields into one ordered dict."""
    with open(filepath, 'r', encoding='latin-1') as f:
        content = f.read()

    entry = {}
    entry.update(parse_filename(stem))
    entry.update(parse_waist_results(content))
    return entry


# ---------------------------------------------------------------------------
# YAML writer (stdlib only)
# ---------------------------------------------------------------------------

def yaml_scalar(v) -> str:
    """Format a scalar value for YAML output."""
    if isinstance(v, (int, float)):
        return str(v)
    s = str(v)
    if not s:
        return "''"
    # Quote if the string contains YAML-special characters or looks numeric-ish
    needs_quote = any(c in s for c in (':', '#', '[', ']', '{', '}', ',',
                                        '&', '*', '?', '|', '-', '<', '>',
                                        '=', '!', '%', '@', '`', "'", '"', '\n'))
    if needs_quote:
        escaped = s.replace('"', '\\"')
        return f'"{escaped}"'
    return s


def write_yaml(data: dict, output_path: str) -> None:
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write('# Generated by thorlabs_to_yml.py\n')
        f.write(f'# Created: {datetime.now(tz=timezone.utc).isoformat()}\n\n')
        f.write('files:\n')
        for filename, fields in data.items():
            f.write(f'  {filename}:\n')
            for key, value in fields.items():
                f.write(f'    {key}: {yaml_scalar(value)}\n')
            f.write('\n')


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

def main():
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(1)

    folder = sys.argv[1]
    output_file = sys.argv[2] if len(sys.argv) >= 3 else 'output.yml'

    if not os.path.isdir(folder):
        print(f"Error: '{folder}' is not a valid directory.")
        sys.exit(1)

    txt_files = sorted(
        f for f in os.listdir(folder) if f.lower().endswith('.txt')
    )

    if not txt_files:
        print(f"No .txt files found in '{folder}'.")
        sys.exit(0)

    print(f"Found {len(txt_files)} .txt file(s). Parsing...")

    results = {}
    for filename in txt_files:
        filepath = os.path.join(folder, filename)
        stem = os.path.splitext(filename)[0]
        print(f"  → {filename}")
        results[filename] = analyze_file(filepath, stem)

    write_yaml(results, output_file)
    print(f"\nDone! YAML written to: {output_file}")


if __name__ == '__main__':
    main()
