#!/usr/bin/env python3
"""Poll the original EXE selected-pointer global while driving key-buffer input."""
from __future__ import annotations

import argparse
import html
import json
import shutil
import subprocess
import threading
import time
from pathlib import Path
from typing import Any

from probe_runtime_input_path import (
    CURRENT_ROOT,
    DEFAULT_PREFIX,
    OUT,
    ROOT,
    env_for,
    find_free_display,
    find_hwanse_pid,
    hex32,
    load_selector_contexts,
    loaded_base,
    parse_windows,
    run,
    sample_process,
    selected_pointer_context,
    truncate,
    write_key_buffer,
)
from probe_runtime_key_sequences import EXPANDED_SEQUENCES, KEY_OFFSETS, run_input_path_prelude


TARGET_SELECTOR = "2:0"


def context_key(context: dict[str, Any] | None) -> str:
    return (context or {}).get("selector") or "-"


def compact_context(context: dict[str, Any] | None) -> dict[str, Any] | None:
    if not context:
        return None
    return {
        "selector": context.get("selector"),
        "rootHex": context.get("rootHex"),
        "offsetHex": context.get("offsetHex"),
        "fieldMaps": context.get("fieldMaps") or [],
        "equalsCurrentRouteRoot": context.get("equalsCurrentRouteRoot"),
    }


def sample_event(
    *,
    phase: str,
    sample_index: int,
    elapsed: float,
    sample: dict[str, Any],
    roots: list[int],
    contexts: dict[int, dict],
) -> dict[str, Any]:
    selected = sample.get("selectedPointerStaticHex")
    context = selected_pointer_context(selected, roots, contexts)
    return {
        "phase": phase,
        "sampleIndex": sample_index,
        "elapsedMs": round(elapsed * 1000, 1),
        "selectedPointerStaticHex": selected,
        "selectorContext": compact_context(context),
        "watchValues": sample.get("watchValues") or {},
        "pressedKeyOffsets": sample.get("pressedKeyOffsets") or [],
        "readOk": sample.get("readOk"),
    }


def compact_watch_values(sample: dict[str, Any]) -> dict[str, str | None]:
    return {
        name: row.get("valueHex")
        for name, row in (sample.get("watchValues") or {}).items()
        if isinstance(row, dict)
    }


def watch_values_to_rows(values: dict[str, dict[str, int]]) -> dict[str, list[dict[str, Any]]]:
    return {
        name: [
            {"valueHex": value_hex, "count": count}
            for value_hex, count in sorted(rows.items(), key=lambda item: (-item[1], item[0] or ""))
        ]
        for name, rows in sorted(values.items())
    }


def aggregate_watch_values(rows: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
    aggregate: dict[str, dict[str, int]] = {}
    for row in rows:
        for name, values in (row.get("uniqueWatchValues") or {}).items():
            target = aggregate.setdefault(name, {})
            for value_row in values or []:
                value_hex = value_row.get("valueHex")
                if value_hex is None:
                    continue
                target[value_hex] = target.get(value_hex, 0) + int(value_row.get("count") or 0)
    return watch_values_to_rows(aggregate)


def watch_summary(values: dict[str, list[dict[str, Any]]] | None) -> str:
    parts = []
    for name, rows in sorted((values or {}).items()):
        rendered = ",".join(f"{row.get('valueHex')}x{row.get('count')}" for row in rows or [])
        parts.append(f"{name}={rendered or '-'}")
    return "; ".join(parts) or "-"


def event_watch_summary(event: dict[str, Any]) -> str:
    return watch_summary({
        name: [{"valueHex": value.get("valueHex"), "count": 1}]
        for name, value in (event.get("watchValues") or {}).items()
        if isinstance(value, dict)
    })


def poll_until(
    *,
    pid: int,
    base: int,
    deadline: float,
    interval: float,
    phase: str,
    roots: list[int],
    contexts: dict[int, dict],
    state: dict[str, Any],
) -> None:
    last_selected = state.get("lastSelectedPointerStaticHex")
    last_pressed = tuple(state.get("lastPressedKeyOffsets") or [])
    last_watch_values = state.get("lastWatchValues") or {}
    while time.time() < deadline:
        now = time.time()
        sample = sample_process(pid, base)
        state["sampleCount"] += 1
        selected = sample.get("selectedPointerStaticHex")
        pressed = tuple(sample.get("pressedKeyOffsets") or [])
        watch_values = compact_watch_values(sample)
        for name, value_hex in watch_values.items():
            rows = state["uniqueWatchValues"].setdefault(name, {})
            rows[value_hex] = rows.get(value_hex, 0) + 1
        if selected:
            pointer_row = state["uniqueSelectedPointers"].setdefault(
                selected,
                {
                    "selectedPointerStaticHex": selected,
                    "count": 0,
                    "selectorContext": compact_context(selected_pointer_context(selected, roots, contexts)),
                },
            )
            pointer_row["count"] += 1
            context = selected_pointer_context(selected, roots, contexts)
            selector = context_key(context)
            selector_row = state["uniqueSelectorContexts"].setdefault(
                selector,
                {
                    "selector": selector,
                    "count": 0,
                    "rootHex": (context or {}).get("rootHex"),
                    "fieldMaps": (context or {}).get("fieldMaps") or [],
                },
            )
            selector_row["count"] += 1
            if selected == hex32(CURRENT_ROOT):
                state["currentRootHitCount"] += 1
            if selector == TARGET_SELECTOR:
                state["routeSelectorHitCount"] += 1
        changed = selected != last_selected or pressed != last_pressed or watch_values != last_watch_values
        target_hit = selected == hex32(CURRENT_ROOT) or context_key(selected_pointer_context(selected, roots, contexts)) == TARGET_SELECTOR
        if changed or target_hit or state["sampleCount"] == 1:
            state["events"].append(
                sample_event(
                    phase=phase,
                    sample_index=state["sampleCount"],
                    elapsed=now - state["startedAt"],
                    sample=sample,
                    roots=roots,
                    contexts=contexts,
                )
            )
        last_selected = selected
        last_pressed = pressed
        last_watch_values = watch_values
        state["lastSelectedPointerStaticHex"] = selected
        state["lastPressedKeyOffsets"] = list(pressed)
        state["lastWatchValues"] = watch_values
        time.sleep(interval)


def focus_windows(env: dict[str, str]) -> dict[str, Any]:
    if not shutil.which("xdotool"):
        return {
            "xdotoolPath": None,
            "classSearch": {"output": ""},
            "nameSearch": {"output": ""},
            "focusedWindows": [],
        }
    class_search = run(["xdotool", "search", "--class", "hwanse2"], env)
    name_search = run(["xdotool", "search", "--name", "hwanse"], env)
    windows = parse_windows([class_search.get("output", ""), name_search.get("output", "")])
    focused = []
    for window in windows[:8]:
        run(["xdotool", "windowactivate", "--sync", window], env)
        run(["xdotool", "windowfocus", window], env)
        run(["xdotool", "mousemove", "--window", window, "320", "240", "click", "1"], env)
        focused.append(window)
        time.sleep(0.05)
    return {
        "xdotoolPath": shutil.which("xdotool"),
        "classSearch": class_search,
        "nameSearch": name_search,
        "focusedWindows": focused,
    }


def run_sequence(
    name: str,
    keys: list[str],
    *,
    startup_wait: float,
    hold: float,
    gap: float,
    interval: float,
    prelude: str,
    prefix: Path,
    roots: list[int],
    contexts: dict[int, dict],
) -> dict[str, Any]:
    display = find_free_display()
    env = env_for(prefix, display)
    prefix.mkdir(parents=True, exist_ok=True)
    xvfb = subprocess.Popen(
        ["Xvfb", display, "-screen", "0", "1280x1024x24"],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        errors="replace",
    )
    time.sleep(1)
    wine = subprocess.Popen(
        ["wine", "explorer", "/desktop=hwanse,640x480", "Hwanse2.exe"],
        cwd=ROOT,
        env=env,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        errors="replace",
    )
    time.sleep(startup_wait)
    pid = find_hwanse_pid()
    base = loaded_base(pid) if pid else None
    focus = focus_windows(env)
    prelude_rows = (
        run_input_path_prelude(focus.get("focusedWindows") or [], env, pid, base)
        if prelude == "input-path"
        else []
    )
    state = {
        "startedAt": time.time(),
        "sampleCount": 0,
        "events": [],
        "uniqueSelectedPointers": {},
        "uniqueSelectorContexts": {},
        "uniqueWatchValues": {},
        "currentRootHitCount": 0,
        "routeSelectorHitCount": 0,
        "lastSelectedPointerStaticHex": None,
        "lastPressedKeyOffsets": [],
        "lastWatchValues": {},
    }
    writes = []
    if pid and base:
        poll_until(
            pid=pid,
            base=base,
            deadline=time.time() + 0.6,
            interval=interval,
            phase="initial",
            roots=roots,
            contexts=contexts,
            state=state,
        )
        for key in keys:
            offset = KEY_OFFSETS[key]
            write_box: dict[str, Any] = {}

            def writer() -> None:
                write_box.update(write_key_buffer(pid, base, offset, duration=hold))

            thread = threading.Thread(target=writer, daemon=True)
            thread.start()
            poll_until(
                pid=pid,
                base=base,
                deadline=time.time() + hold + gap,
                interval=interval,
                phase=f"hold:{key}",
                roots=roots,
                contexts=contexts,
                state=state,
            )
            thread.join(timeout=1)
            writes.append({
                "key": key,
                "keyOffsetHex": hex32(offset),
                "write": write_box,
            })
        poll_until(
            pid=pid,
            base=base,
            deadline=time.time() + 0.8,
            interval=interval,
            phase="final",
            roots=roots,
            contexts=contexts,
            state=state,
        )
    subprocess.run(["wineserver", "-k"], cwd=ROOT, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5)
    try:
        wine_output, _ = wine.communicate(timeout=5)
    except subprocess.TimeoutExpired:
        wine.kill()
        wine_output, _ = wine.communicate(timeout=5)
    xvfb.terminate()
    try:
        xvfb_output, _ = xvfb.communicate(timeout=5)
    except subprocess.TimeoutExpired:
        xvfb.kill()
        xvfb_output, _ = xvfb.communicate(timeout=5)
    unique_pointers = sorted(
        state["uniqueSelectedPointers"].values(),
        key=lambda row: (-row["count"], row["selectedPointerStaticHex"] or ""),
    )
    unique_contexts = sorted(
        state["uniqueSelectorContexts"].values(),
        key=lambda row: (-row["count"], row["selector"] or ""),
    )
    reached_current = state["currentRootHitCount"] > 0
    reached_route = state["routeSelectorHitCount"] > 0
    return {
        "name": name,
        "keys": keys,
        "display": display,
        "linuxPid": pid,
        "loadedBaseHex": hex32(base),
        **focus,
        "prelude": prelude,
        "preludeRowCount": len(prelude_rows),
        "preludeRows": prelude_rows[:48],
        "preludeRowsTruncated": max(0, len(prelude_rows) - 48),
        "sampleCount": state["sampleCount"],
        "eventCount": len(state["events"]),
        "events": state["events"][:200],
        "eventsTruncated": max(0, len(state["events"]) - 200),
        "uniqueSelectedPointers": unique_pointers,
        "uniqueSelectorContexts": unique_contexts,
        "uniqueWatchValues": watch_values_to_rows(state["uniqueWatchValues"]),
        "writes": writes,
        "currentRootHitCount": state["currentRootHitCount"],
        "routeSelectorHitCount": state["routeSelectorHitCount"],
        "reachedCurrentRoot": reached_current,
        "reachedRouteSelectorContext": reached_route,
        "startupOutput": truncate(wine_output or ""),
        "xvfbOutput": truncate(xvfb_output or ""),
    }


def build_summary(args: argparse.Namespace) -> dict[str, Any]:
    roots, contexts = load_selector_contexts()
    sequence_rows = EXPANDED_SEQUENCES
    if args.sequence_name:
        by_name = dict(sequence_rows)
        missing = [name for name in args.sequence_name if name not in by_name]
        if missing:
            raise SystemExit(f"unknown sequence name(s): {', '.join(missing)}")
        selected = [(name, by_name[name]) for name in args.sequence_name]
    elif args.sequence:
        selected = []
        for index, raw in enumerate(args.sequence, start=1):
            if "=" in raw:
                name, key_text = raw.split("=", 1)
            else:
                name, key_text = f"custom-{index}", raw
            keys = [item.strip() for item in key_text.split(",") if item.strip()]
            if not keys:
                raise SystemExit(f"empty custom sequence: {raw!r}")
            missing = [key for key in keys if key not in KEY_OFFSETS]
            if missing:
                raise SystemExit(f"unknown key(s) in custom sequence {name!r}: {', '.join(missing)}")
            selected.append((name, keys))
    else:
        selected = sequence_rows[: args.max_sequences]
    rows = [
        run_sequence(
            name,
            keys,
            startup_wait=args.startup_wait,
            hold=args.hold,
            gap=args.gap,
            interval=args.interval,
            prelude=args.prelude,
            prefix=args.prefix,
            roots=roots,
            contexts=contexts,
        )
        for name, keys in selected
    ]
    any_current = any(row.get("reachedCurrentRoot") for row in rows)
    any_route = any(row.get("reachedRouteSelectorContext") for row in rows)
    observed_selectors = []
    seen = set()
    for row in rows:
        for context in row.get("uniqueSelectorContexts") or []:
            selector = context.get("selector")
            if selector and selector not in seen:
                observed_selectors.append(selector)
                seen.add(selector)
    total_samples = sum(row.get("sampleCount", 0) for row in rows)
    observed_watch_values = aggregate_watch_values(rows)
    return {
        "objective": "non-debugger selected-pointer polling during original EXE key-buffer input",
        "startupWaitSeconds": args.startup_wait,
        "holdSeconds": args.hold,
        "gapSeconds": args.gap,
        "pollIntervalSeconds": args.interval,
        "prelude": args.prelude,
        "sequenceCount": len(rows),
        "sampleCount": total_samples,
        "currentRootHex": hex32(CURRENT_ROOT),
        "targetSelector": TARGET_SELECTOR,
        "observedSelectors": observed_selectors,
        "observedWatchValues": observed_watch_values,
        "anyReachedCurrentRoot": any_current,
        "anyReachedRouteSelectorContext": any_route,
        "promotionStatus": "blocked",
        "rows": rows,
        "conclusion": (
            f"Polled selected-pointer global 0x0059de30 during {len(rows)} direct key-buffer sequence(s). "
            f"Prelude={args.prelude}. "
            f"Route selector {TARGET_SELECTOR} reached={any_route}, current root {hex32(CURRENT_ROOT)} reached={any_current}. "
            f"Route watch values: {watch_summary(observed_watch_values)}. "
            "This is a non-debugger polling fallback; it can catch longer-lived selected-pointer states, but it is not a replacement "
            "for a watchpoint trace if the route pointer is only transient."
        ),
    }


def markdown(summary: dict[str, Any]) -> str:
    case_alias_lines = []
    if "caseAliases" in summary:
        case_alias_lines.append(
            "- case aliases: "
            f"enabled={(summary.get('caseAliases') or {}).get('enabled')}; "
            f"removed={','.join((summary.get('caseAliasCleanup') or {}).get('removedAliases') or []) or '-'}"
        )
    lines = [
        "# Runtime Selected-Pointer Poll",
        "",
        f"- objective: {summary.get('objective')}",
        f"- sequence count: {summary.get('sequenceCount')}",
        f"- sample count: {summary.get('sampleCount')}",
        f"- poll interval seconds: `{summary.get('pollIntervalSeconds')}`",
        f"- prelude: `{summary.get('prelude') or '-'}`",
        *case_alias_lines,
        f"- current root: `{summary.get('currentRootHex')}`",
        f"- target selector: `{summary.get('targetSelector')}`",
        f"- observed selectors: `{', '.join(summary.get('observedSelectors') or []) or '-'}`",
        f"- route watch values: `{watch_summary(summary.get('observedWatchValues'))}`",
        f"- any reached current root: {summary.get('anyReachedCurrentRoot')}",
        f"- any reached route selector 2:0: {summary.get('anyReachedRouteSelectorContext')}",
        f"- promotion status: `{summary.get('promotionStatus')}`",
        "",
        summary.get("conclusion") or "",
        "",
        "## Sequences",
        "",
        "| name | keys | samples | selectors | current-root hits | route-selector hits |",
        "| --- | --- | ---: | --- | ---: | ---: |",
    ]
    for row in summary.get("rows") or []:
        selectors = ", ".join(context.get("selector") or "-" for context in row.get("uniqueSelectorContexts") or [])
        lines.append(
            f"| `{row.get('name')}` | `{','.join(row.get('keys') or [])}` | {row.get('sampleCount')} | "
            f"`{selectors or '-'}` | {row.get('currentRootHitCount')} | {row.get('routeSelectorHitCount')} |"
        )
    lines.extend(["", "## Pointer Changes", ""])
    for row in summary.get("rows") or []:
        lines.append(f"### {row.get('name')}")
        for event in (row.get("events") or [])[:12]:
            context = event.get("selectorContext") or {}
            event_watch = watch_summary({
                name: [{"valueHex": value.get("valueHex"), "count": 1}]
                for name, value in (event.get("watchValues") or {}).items()
                if isinstance(value, dict)
            })
            lines.append(
                f"- {event.get('phase')} +{event.get('elapsedMs')}ms -> "
                f"`{event.get('selectedPointerStaticHex')}` selector `{context.get('selector') or '-'}` "
                f"watch={event_watch} pressed={event.get('pressedKeyOffsets')}"
            )
        if row.get("eventsTruncated"):
            lines.append(f"- ... {row.get('eventsTruncated')} more event(s)")
        lines.append("")
    lines.extend([
        "## Route Watch Values",
        "",
        "| value | observed |",
        "| --- | --- |",
    ])
    for name, rows in (summary.get("observedWatchValues") or {}).items():
        value_text = ", ".join(f"{value.get('valueHex')} x{value.get('count')}" for value in rows) or "-"
        lines.append(f"| `{name}` | `{value_text}` |")
    return "\n".join(lines)


def html_page(summary: dict[str, Any]) -> str:
    case_alias_html = ""
    if "caseAliases" in summary:
        case_alias_html = (
            "<li>case aliases: "
            f"enabled={html.escape(str((summary.get('caseAliases') or {}).get('enabled')))}; "
            f"removed=<code>{html.escape(','.join((summary.get('caseAliasCleanup') or {}).get('removedAliases') or []) or '-')}</code></li>"
        )
    sequence_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(row.get('name')))}</code></td>"
        f"<td><code>{html.escape(','.join(row.get('keys') or []))}</code></td>"
        f"<td>{html.escape(str(row.get('sampleCount')))}</td>"
        f"<td><code>{html.escape(', '.join(context.get('selector') or '-' for context in row.get('uniqueSelectorContexts') or []) or '-')}</code></td>"
        f"<td>{html.escape(str(row.get('currentRootHitCount')))}</td>"
        f"<td>{html.escape(str(row.get('routeSelectorHitCount')))}</td>"
        "</tr>"
        for row in summary.get("rows") or []
    )
    event_sections = []
    for row in summary.get("rows") or []:
        event_rows = "\n".join(
            "<tr>"
            f"<td><code>{html.escape(str(event.get('phase')))}</code></td>"
            f"<td>{html.escape(str(event.get('elapsedMs')))}</td>"
            f"<td><code>{html.escape(str(event.get('selectedPointerStaticHex')))}</code></td>"
            f"<td><code>{html.escape(str((event.get('selectorContext') or {}).get('selector') or '-'))}</code></td>"
            f"<td><code>{html.escape(event_watch_summary(event))}</code></td>"
            f"<td>{html.escape(str(event.get('pressedKeyOffsets') or []))}</td>"
            "</tr>"
            for event in (row.get("events") or [])[:24]
        )
        event_sections.append(
            f"<h3>{html.escape(str(row.get('name')))}</h3>"
            "<table><thead><tr><th>phase</th><th>elapsed ms</th><th>selected pointer</th><th>selector</th><th>watch values</th><th>pressed</th></tr></thead><tbody>"
            f"{event_rows}</tbody></table>"
            f"<p>truncated events: {html.escape(str(row.get('eventsTruncated') or 0))}</p>"
        )
    watch_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(name))}</code></td>"
        f"<td><code>{html.escape(', '.join(f'{row.get('valueHex')} x{row.get('count')}' for row in rows) or '-')}</code></td>"
        "</tr>"
        for name, rows in (summary.get("observedWatchValues") or {}).items()
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Runtime Selected-Pointer Poll</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1100px;margin:24px auto}table{border-collapse:collapse;width:100%}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Runtime Selected-Pointer Poll</h1>",
        "<ul>",
        f"<li>sequence count: {html.escape(str(summary.get('sequenceCount')))}</li>",
        f"<li>sample count: {html.escape(str(summary.get('sampleCount')))}</li>",
        f"<li>poll interval seconds: <code>{html.escape(str(summary.get('pollIntervalSeconds')))}</code></li>",
        f"<li>prelude: <code>{html.escape(str(summary.get('prelude') or '-'))}</code></li>",
        case_alias_html,
        f"<li>current root: <code>{html.escape(str(summary.get('currentRootHex')))}</code></li>",
        f"<li>target selector: <code>{html.escape(str(summary.get('targetSelector')))}</code></li>",
        f"<li>observed selectors: <code>{html.escape(', '.join(summary.get('observedSelectors') or []) or '-')}</code></li>",
        f"<li>route watch values: <code>{html.escape(watch_summary(summary.get('observedWatchValues')))}</code></li>",
        f"<li>any reached current root: {html.escape(str(summary.get('anyReachedCurrentRoot')))}</li>",
        f"<li>any reached route selector 2:0: {html.escape(str(summary.get('anyReachedRouteSelectorContext')))}</li>",
        f"<li>promotion status: <code>{html.escape(str(summary.get('promotionStatus')))}</code></li>",
        "</ul>",
        f"<p>{html.escape(summary.get('conclusion') or '')}</p>",
        "<h2>Sequences</h2>",
        "<table><thead><tr><th>name</th><th>keys</th><th>samples</th><th>selectors</th><th>current-root hits</th><th>route-selector hits</th></tr></thead><tbody>",
        sequence_rows,
        "</tbody></table>",
        "<h2>Route Watch Values</h2>",
        "<table><thead><tr><th>value</th><th>observed</th></tr></thead><tbody>",
        watch_rows,
        "</tbody></table>",
        "<h2>Pointer Changes</h2>",
        "\n".join(event_sections),
    ])


def write_outputs(summary: dict[str, Any], out_dir: Path = OUT, output_prefix: str = "runtime_selected_pointer_poll") -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / f"{output_prefix}.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--startup-wait", type=float, default=14.0)
    parser.add_argument("--hold", type=float, default=0.7)
    parser.add_argument("--gap", type=float, default=0.25)
    parser.add_argument("--interval", type=float, default=0.02)
    parser.add_argument("--prelude", choices=["focus-only", "input-path"], default="focus-only")
    parser.add_argument("--max-sequences", type=int, default=8)
    parser.add_argument("--sequence-name", action="append", default=[])
    parser.add_argument(
        "--sequence",
        action="append",
        default=[],
        help="custom sequence as name=Key,Key or Key,Key; valid keys match probe_runtime_key_sequences.KEY_OFFSETS",
    )
    parser.add_argument("--prefix", type=Path, default=DEFAULT_PREFIX)
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--output-prefix", default="runtime_selected_pointer_poll")
    args = parser.parse_args()
    summary = build_summary(args)
    write_outputs(summary, args.out_dir, args.output_prefix)
    print(f"wrote runtime selected-pointer poll -> {args.out_dir / (args.output_prefix + '.json')}")


if __name__ == "__main__":
    main()
