from __future__ import annotations

import argparse
import gzip
import json
from collections import Counter
from pathlib import Path
import xml.etree.ElementTree as ET


def _node_name(node: ET.Element | None) -> str:
    if node is None:
        return ""
    for tag in ("EffectiveName", "UserName", "Name"):
        child = node.find(tag)
        if child is not None:
            value = child.attrib.get("Value", "")
            if value:
                return value
    return node.attrib.get("Value", "")


def _device_name(device: ET.Element) -> str:
    if device.tag == "PluginDevice":
        info = device.find("PluginDesc/VstPluginInfo")
        if info is None:
            info = device.find("PluginDesc/AuPluginInfo")
        if info is not None:
            plug = info.find("PlugName")
            if plug is not None and plug.attrib.get("Value"):
                return plug.attrib["Value"]
    return device.tag


def _session_clip_count(track: ET.Element) -> int:
    count = 0
    for slot in track.findall("./DeviceChain/MainSequencer/ClipSlotList/ClipSlot"):
        if slot.find("Value/MidiClip") is not None or slot.find("Value/AudioClip") is not None:
            count += 1
    return count


def _arrangement_clip_count(track: ET.Element) -> int:
    return len(track.findall(".//MainSequencer//MidiClip")) + len(
        track.findall(".//MainSequencer//AudioClip")
    )


def _tempo_value(live_set: ET.Element) -> float | None:
    node = live_set.find(".//Tempo/Manual")
    if node is None:
        return None
    try:
        return float(node.attrib.get("Value", "0"))
    except ValueError:
        return None


def _locator_summary(live_set: ET.Element) -> list[dict[str, float | str | None]]:
    locators: list[tuple[float, str]] = []
    for locator in live_set.findall(".//Locators/Locators/Locator"):
        try:
            time = float(locator.find("Time").attrib.get("Value", "0"))
        except (AttributeError, ValueError):
            time = 0.0
        name = _node_name(locator.find("Name"))
        locators.append((time, name))
    locators.sort(key=lambda item: item[0])
    summary: list[dict[str, float | str | None]] = []
    for index, (time, name) in enumerate(locators):
        next_time = locators[index + 1][0] if index + 1 < len(locators) else None
        summary.append(
            {
                "time_beats": time,
                "name": name,
                "section_length_beats": None if next_time is None else next_time - time,
            }
        )
    return summary


def _arrangement_length_beats(root: ET.Element) -> float:
    max_end = 0.0
    for clip in root.findall(".//MidiClip") + root.findall(".//AudioClip"):
        current_end = clip.find("CurrentEnd")
        start = clip.attrib.get("Time")
        if current_end is None or start is None:
            continue
        try:
            end = float(start) + float(current_end.attrib.get("Value", "0"))
        except ValueError:
            continue
        max_end = max(max_end, end)
    return max_end


def analyze_set(als_path: Path) -> dict:
    with gzip.open(als_path, "rb") as handle:
        root = ET.parse(handle).getroot()
    live_set = root.find("LiveSet")
    if live_set is None:
        raise ValueError(f"Invalid ALS file: {als_path}")

    tracks = list(live_set.find("Tracks") or [])
    track_summaries = []
    device_counter: Counter[str] = Counter()

    for track in tracks:
        devices = track.findall("./DeviceChain/DeviceChain/Devices/*")
        device_names = [_device_name(device) for device in devices]
        device_counter.update(device_names)
        track_summaries.append(
            {
                "type": track.tag,
                "name": _node_name(track.find("Name")),
                "group_id": track.find("TrackGroupId").attrib.get("Value", "")
                if track.find("TrackGroupId") is not None
                else "",
                "session_clip_count": _session_clip_count(track),
                "arrangement_clip_count": _arrangement_clip_count(track),
                "devices": device_names,
            }
        )

    automation_events = 0
    for automation in root.findall(".//ArrangerAutomation"):
        automation_events += len(automation.findall(".//FloatEvent"))
        automation_events += len(automation.findall(".//EnumEvent"))
        automation_events += len(automation.findall(".//BoolEvent"))

    return {
        "file": str(als_path),
        "tempo": _tempo_value(live_set),
        "track_type_counts": dict(Counter(track.tag for track in tracks)),
        "scene_count": len(live_set.findall("./SceneNames/Scene")),
        "locators": _locator_summary(live_set),
        "arrangement_length_beats": _arrangement_length_beats(root),
        "automation_event_count": automation_events,
        "top_devices": dict(device_counter.most_common(16)),
        "tracks": track_summaries,
    }


def main() -> None:
    parser = argparse.ArgumentParser(description="Analyze Ableton .als templates.")
    parser.add_argument("path", nargs="?", default=".", help="Folder containing .als files")
    parser.add_argument("--json", action="store_true", help="Emit JSON")
    args = parser.parse_args()

    base = Path(args.path).resolve()
    results = [analyze_set(path) for path in sorted(base.rglob("*.als"))]

    if args.json:
        print(json.dumps(results, indent=2))
        return

    for result in results:
        print(f"=== {Path(result['file']).name} ===")
        print(f"tempo: {result['tempo']}")
        print(f"tracks: {result['track_type_counts']}")
        print(f"scenes: {result['scene_count']}")
        print(f"arrangement_length_beats: {result['arrangement_length_beats']}")
        print(f"automation_event_count: {result['automation_event_count']}")
        print("locators:")
        for locator in result["locators"]:
            print(
                f"  - {locator['time_beats']:>6}  {locator['name']}"
                f"  len={locator['section_length_beats']}"
            )
        print("top_devices:")
        for name, count in result["top_devices"].items():
            print(f"  - {name}: {count}")
        print()


if __name__ == "__main__":
    main()
