123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
"""Open electronics files in Fusion 360's Electronics workspace.

Each file type gets its own command for clarity:
  fusion_open_schematic  — .sch files → Schematic Editor
  fusion_open_board      — .brd files → Board Layout editor
  fusion_show_3d_board   — switches Board Layout → 3D PCB view

All use Document.newDesignFromLocal which reliably opens these file types
and switches to the correct Electronics sub-workspace automatically.

fusion_open_lbr (in open_lbr.py) handles .lbr library files.

Fusion 360 Electronics projects are cloud-based (not .epr files like standalone EAGLE).
The project is the Fusion document itself. Open a schematic or board to work with a design.
"""

import adsk.core
import os
import time


def _open_local_document(app, file_path: str):
    """Run Document.newDesignFromLocal for a path that MAY CONTAIN SPACES.

    BUG (Drew's agent, issue #196, 2026-07-16): executeTextCommand is a SPACE-DELIMITED
    command string, so an unquoted path like
      C:/Users/drew/Downloads/e2e-pmcoil/PM COIL Molecule.brd
    was split at the first space - Fusion got ".../e2e-pmcoil/PM" and the open silently
    no-op'd, surfacing only as the generic "Fusion may not support this file" hint. A
    space-free copy of the same file opened instantly, which is what pinned it to path
    handling rather than file support.

    Two-step, so it works whichever way this Fusion build's parser behaves:
      1. QUOTE the path.
      2. If that fails and the path has spaces, retry with the Windows 8.3 SHORT path
         (GetShortPathNameW) - same file, no spaces, no copying.

    Returns (result, how) and raises the LAST exception if every attempt fails.
    """
    attempts = []
    quoted = '"%s"' % file_path.replace('"', '')
    attempts.append((quoted, "quoted"))
    if " " in file_path:
        try:
            import ctypes
            from ctypes import wintypes
            _gsp = ctypes.windll.kernel32.GetShortPathNameW
            _gsp.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD]
            _gsp.restype = wintypes.DWORD
            buf = ctypes.create_unicode_buffer(600)
            if _gsp(file_path, buf, 600) and buf.value and " " not in buf.value:
                attempts.append((buf.value, "shortpath"))
        except Exception:
            pass
    def _doc_name():
        try:
            d = app.activeDocument
            return d.name if d else None
        except Exception:
            return None

    before = _doc_name()
    last = None
    for arg, how in attempts:
        try:
            r = app.executeTextCommand("Document.newDesignFromLocal %s" % arg)
        except Exception as e:
            last = e
            continue
        # executeTextCommand does not always RAISE on failure - it can return an error
        # string and leave the document untouched. So confirm a document actually opened
        # before declaring victory, otherwise fall through to the next attempt.
        time.sleep(0.6)
        if _doc_name() != before:
            return r, how
        last = RuntimeError("open did not change the active document (result=%r)" % (r,))
    raise last if last else RuntimeError("no open attempt ran")


def _open_electronics(app: adsk.core.Application, file_path: str, file_type: str, expected_ext: str) -> dict:
    """Common logic for opening an electronics file in Fusion."""
    if not file_path:
        return {
            "success": False,
            "error": f"No filePath specified. Provide a {expected_ext} file path.",
            "data": {
                "hint": f"Usage: fusion_open_{file_type.replace(' ', '_')} {{\"filePath\": \"C:/path/to/file{expected_ext}\"}}"
            },
        }

    file_path = file_path.replace("\\", "/")

    if not os.path.exists(file_path):
        return {
            "success": False,
            "error": f"File not found: {file_path}",
            "_hint": "The path may exist on Docker but not on the Windows host. Verify with desktop_list_directory on the host, or use push_file to copy from Docker first.",
            "data": {"filePath": file_path, "hint": "Check that the file path exists on the Windows desktop machine, not on Docker."},
        }

    ext = os.path.splitext(file_path)[1].lower()
    if ext != expected_ext:
        return {
            "success": False,
            "error": f"Expected a {expected_ext} file, got {ext}. Use the correct command for this file type.",
            "data": {
                "hint": {
                    ".sch": "Use fusion_open_schematic for .sch files",
                    ".brd": "Use fusion_open_board for .brd files",
                    ".lbr": "Use fusion_open_lbr for .lbr files",
                    ".step": "Use fusion_import_step for .step files",
                    ".stp": "Use fusion_import_step for .stp files",
                }.get(ext, f"This command only handles {expected_ext} files"),
            },
        }

    try:
        r, _open_how = _open_local_document(app, file_path)
    except Exception as e:
        return {
            "success": False,
            "error": f"Failed to open {file_type}: {e}",
            "_hint": "Call fusion_screenshot to check for a modal dialog blocking Fusion, then fusion_send_key escape to dismiss it, then retry.",
            "data": {
                "filePath": file_path,
                "hint": ("PATH WITH SPACES? This verb now quotes the path and falls back to the "
                         "Windows 8.3 short path, so spaces should work (issue #196). If it still "
                         "fails, verify the file EXISTS at exactly this path and is valid, then try "
                         "a space-free copy to isolate it. Otherwise Fusion may not support this "
                         "file, or it is not fully loaded/responsive."),
            },
        }

    time.sleep(1.0)

    # Get workspace info to confirm we landed in the right editor
    ws_info = {}
    try:
        active_ws = app.executeTextCommand("DebugCommands.ActiveWorkspace")
        ws_info["activeWorkspace"] = active_ws.strip() if active_ws else "unknown"
    except Exception:
        pass
    try:
        doc = app.activeDocument
        if doc:
            ws_info["activeDocument"] = doc.name
    except Exception:
        pass

    # Build helpful next-steps based on file type
    next_steps = {
        "schematic": [
            "Use fusion_electron_run to send EAGLE commands (e.g. WINDOW FIT, DISPLAY ALL)",
            "Use fusion_take_screenshot to capture the schematic view",
            "Use fusion_close_document to close without saving when done",
        ],
        "board layout": [
            "Use fusion_show_3d_board to switch to 3D PCB visualization",
            "Use fusion_electron_run to send EAGLE commands (e.g. WINDOW FIT, RATSNEST)",
            "Use fusion_take_screenshot to capture the board view",
            "Use fusion_close_document to close without saving when done",
        ],
    }

    return {
        "success": True,
        "output": f"Opened {file_type}: {os.path.basename(file_path)}",
        "message": f"Opened {file_type}: {os.path.basename(file_path)}. "
                   f"IMPORTANT: Fusion may show a blocking dialog that is NOT visible to the API. "
                   f"You MUST screenshot the Fusion window now to verify no dialog is blocking.",
        "data": {
            "filePath": file_path,
            "fileType": file_type,
            "nextSteps": next_steps.get(file_type, []),
            **ws_info,
        },
        "postOpenCheck": {
            "required": True,
            "steps": [
                "Run desktop_list_windows to find the Fusion HWND",
                "Run desktop_screenshot_window with that HWND",
                "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"} or fusion_click_fusion on the Cancel/X button",
                "Screenshot again to confirm the dialog is gone",
            ],
        },
    }


def handle_open_schematic(app: adsk.core.Application, args: dict) -> dict:
    """Open a schematic (.sch) file in Fusion 360's Schematic Editor.

    PCB/ELECTRONICS NAV RULE: a board is NOT a standalone file - the schematic, 2D board and 3D board are VIEWS inside ONE electronics DESIGN. Open the DESIGN document first (fusion_open_cloud_file or fusion_open_by_urn), THEN switch to its schematic and its 2D board (fusion_show_2d_board) and 3D board (fusion_show_3d_board). A Select-Electronics-Design-File picker IS that design list of its schematic + board. An EMPTY 2D board usually means you opened a derivative or the wrong file, or it is out of sync - reopen the parent electronics DESIGN and switch to its board view; do NOT open a .brd or .sch as a standalone file.

    The schematic opens in the Electronics workspace where you can use
    EAGLE commands via fusion_electron_run (e.g. WINDOW FIT, DISPLAY ALL).

    Args:
        args: Dict with:
            - filePath: Path to the .sch file on the Windows desktop (required).
    """
    return _open_electronics(app, args.get("filePath", ""), "schematic", ".sch")


def handle_open_board(app: adsk.core.Application, args: dict) -> dict:
    """Open a board layout (.brd) file in Fusion 360's Board Layout editor.

    PCB/ELECTRONICS NAV RULE: a board is NOT a standalone file - the schematic, 2D board and 3D board are VIEWS inside ONE electronics DESIGN. Open the DESIGN document first (fusion_open_cloud_file or fusion_open_by_urn), THEN switch to its schematic and its 2D board (fusion_show_2d_board) and 3D board (fusion_show_3d_board). A Select-Electronics-Design-File picker IS that design list of its schematic + board. An EMPTY 2D board usually means you opened a derivative or the wrong file, or it is out of sync - reopen the parent electronics DESIGN and switch to its board view; do NOT open a .brd or .sch as a standalone file.

    Opens the 2D board layout where you can see traces, pads, and components.
    Use fusion_show_3d_board to switch to the 3D PCB visualization.
    Use fusion_electron_run {"command": "BOARD"} to return to 2D.

    Args:
        args: Dict with:
            - filePath: Path to the .brd file on the Windows desktop (required).
    """
    return _open_electronics(app, args.get("filePath", ""), "board layout", ".brd")


def handle_show_3d_board(app: adsk.core.Application, args: dict) -> dict:
    """Switch to the 3D PCB board view in Fusion 360.

    Must have a board (.brd) open in the Board Layout editor first.
    The board MUST have an outline on layer 20 (Dimension) — without it,
    Fusion cannot generate the 3D model.

    Uses the Fusion command Switch3dPcbDocCmd to switch workspaces.

    To return to 2D board layout, use:
        fusion_execute_text_command {"command": "Commands.Start Electron::Insert2DPCB"}
    """
    # Check current workspace
    ws = "unknown"
    try:
        ws = app.executeTextCommand("DebugCommands.ActiveWorkspace").strip()
    except Exception:
        pass

    # Already in 3D PCB?
    if ws == "3D PCB":
        return {
            "success": True,
            "output": "Already in 3D PCB view.",
            "data": {
                "activeWorkspace": ws,
                "nextSteps": [
                    "Use fusion_take_screenshot to capture the 3D board view",
                    "Use fusion_show_2d_board to return to 2D layout",
                    "Use fusion_close_document to close without saving when done",
                ],
            },
        }

    # Must be in PCB Editor (board layout) to switch to 3D
    is_pcb_editor = False
    try:
        app.executeTextCommand("Electron.run DISPLAY")
        is_pcb_editor = True
    except Exception:
        pass

    if not is_pcb_editor:
        return {
            "success": False,
            "error": f"Not in Electronics editor (current workspace: {ws}). "
                     "Open a .brd board file first with fusion_open_board.",
            "_hint": "Call fusion_open_board {\"filePath\":\"...\"} to load a board, then retry fusion_show_3d_board.",
            "data": {
                "activeWorkspace": ws,
                "hint": "You need to open a .brd file first: fusion_open_board {\"filePath\": \"C:/path/to/board.brd\"}",
            },
        }

    # Switch to 3D PCB using Fusion's workspace switch command
    try:
        result = app.executeTextCommand("Commands.Start Switch3dPcbDocCmd")
    except Exception as e:
        err_msg = str(e)
        if "outline" in err_msg.lower() or "missing" in err_msg.lower():
            return {
                "success": False,
                "error": f"3D PCB failed: {e}. The board may be missing an outline on layer 20 (Dimension).",
                "_hint": "Call fusion_electron_run with a WIRE command on layer 20 to add a board outline, then retry fusion_show_3d_board.",
                "data": {
                    "hint": "Add a board outline on layer 20 (Dimension) using WIRE command, then try again.",
                },
            }
        return {
            "success": False,
            "error": f"Switch to 3D PCB failed: {e}",
            "data": {"activeWorkspace": ws},
        }

    time.sleep(2.0)

    # Zoom to fit in 3D view
    try:
        app.executeTextCommand("Commands.Start FitCommand")
    except Exception:
        pass  # FitCommand may report missing 3D models as an "error" but still works

    # Verify we're now in 3D PCB
    new_ws = "unknown"
    try:
        new_ws = app.executeTextCommand("DebugCommands.ActiveWorkspace").strip()
    except Exception:
        pass

    if new_ws != "3D PCB":
        return {
            "success": False,
            "error": f"Switch to 3D PCB may have failed (workspace: {new_ws}). "
                     "Ensure the board has an outline on layer 20.",
            "_hint": "Call fusion_screenshot to check state, then verify the board has a layer-20 outline before retrying.",
            "data": {"activeWorkspace": new_ws},
        }

    return {
        "success": True,
        "output": "Switched to 3D PCB board view. Board outline, copper, solder mask, and component placeholders are shown in 3D.",
        "message": "Switched to 3D PCB view. IMPORTANT: Fusion may show a 'What to design?' or "
                   "'PCB out of date' dialog. You MUST screenshot the Fusion window to verify.",
        "data": {
            "activeWorkspace": new_ws,
            "nextSteps": [
                "FIRST: Screenshot Fusion window to check for blocking dialogs — dismiss with fusion_send_key escape",
                "Use fusion_take_screenshot to capture the 3D board view",
                "Use fusion_show_2d_board to return to 2D layout",
                "Use fusion_close_document to close without saving when done",
            ],
        },
        "postOpenCheck": {
            "required": True,
            "steps": [
                "Run desktop_list_windows to find the Fusion HWND",
                "Run desktop_screenshot_window with that HWND",
                "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"}",
                "Screenshot again to confirm the dialog is gone",
            ],
        },
    }


def handle_import_electronics(app: adsk.core.Application, args: dict) -> dict:
    """Import a Fusion-native electronics file (.fsch/.fbrd/.flbr) as a new local project.

    These are Fusion 360's own electronics formats (not legacy EAGLE .sch/.brd).
    Uses Document.newDesignFromLocal to create a new document from the local file.

    After importing, use fusion_save_to_cloud to persist to the Fusion cloud.

    Args:
        args: Dict with:
            - filePath: Path to the .fsch, .fbrd, or .flbr file on Windows (required).
    """
    file_path = args.get("filePath", "")
    if not file_path:
        return {
            "success": False,
            "error": "No filePath specified. Provide a .fsch, .fbrd, or .flbr file path.",
            "data": {
                "hint": "Usage: fusion_import_electronics {\"filePath\": \"C:/path/to/file.fsch\"}",
                "supportedExtensions": [".fsch", ".fbrd", ".flbr"],
            },
        }

    file_path = file_path.replace("\\", "/")

    if not os.path.exists(file_path):
        return {
            "success": False,
            "error": f"File not found: {file_path}",
            "_hint": "The path may exist on Docker but not on the Windows host. Use push_file to copy it from Docker first, or verify the host-side path.",
            "data": {"filePath": file_path, "hint": "Check that the file path exists on the Windows desktop machine."},
        }

    ext = os.path.splitext(file_path)[1].lower()
    valid_extensions = {".fsch", ".fbrd", ".flbr"}
    if ext not in valid_extensions:
        ext_hints = {
            ".sch": "Use fusion_open_schematic for legacy EAGLE .sch files",
            ".brd": "Use fusion_open_board for legacy EAGLE .brd files",
            ".lbr": "Use fusion_open_lbr for legacy EAGLE .lbr files",
            ".step": "Use fusion_import_step for .step files",
            ".stp": "Use fusion_import_step for .stp files",
        }
        return {
            "success": False,
            "error": f"Unsupported extension '{ext}'. This command handles Fusion-native electronics formats: .fsch, .fbrd, .flbr",
            "_hint": ext_hints.get(ext, f"Supported: {', '.join(sorted(valid_extensions))}"),
            "data": {
                "hint": ext_hints.get(ext, f"Supported: {', '.join(sorted(valid_extensions))}"),
                "supportedExtensions": sorted(valid_extensions),
            },
        }

    file_type_map = {
        ".fsch": "Fusion schematic",
        ".fbrd": "Fusion board",
        ".flbr": "Fusion library",
    }
    file_type = file_type_map[ext]

    try:
        r, _open_how = _open_local_document(app, file_path)
    except Exception as e:
        return {
            "success": False,
            "error": f"Failed to import {file_type}: {e}",
            "data": {
                "filePath": file_path,
                "hint": "Fusion 360 may not support this file version. Ensure Fusion is fully loaded and responsive.",
            },
        }

    time.sleep(1.5)

    # Get workspace info to confirm we landed in the right editor
    ws_info = {}
    try:
        active_ws = app.executeTextCommand("DebugCommands.ActiveWorkspace")
        ws_info["activeWorkspace"] = active_ws.strip() if active_ws else "unknown"
    except Exception:
        pass
    try:
        doc = app.activeDocument
        if doc:
            ws_info["activeDocument"] = doc.name
    except Exception:
        pass

    next_steps = []
    if ext == ".fsch":
        next_steps = [
            "Use fusion_electron_run to send EAGLE commands (e.g. WINDOW FIT)",
            "Use fusion_save_to_cloud to persist this document to the Fusion cloud",
            "Use fusion_close_document to close without saving when done",
        ]
    elif ext == ".fbrd":
        next_steps = [
            "Use fusion_show_3d_board to switch to 3D PCB visualization",
            "Use fusion_electron_run to send EAGLE commands (e.g. WINDOW FIT, RATSNEST)",
            "Use fusion_save_to_cloud to persist this document to the Fusion cloud",
            "Use fusion_close_document to close without saving when done",
        ]
    elif ext == ".flbr":
        next_steps = [
            "Use fusion_export_lbr to verify library contents (.scr text format)",
            "Use fusion_save_to_cloud to persist this library to the Fusion cloud",
            "Use fusion_close_document to close without saving when done",
        ]

    return {
        "success": True,
        "output": f"Imported {file_type}: {os.path.basename(file_path)}",
        "message": f"Imported {file_type}: {os.path.basename(file_path)}. "
                   f"This is a NEW local document — use fusion_save_to_cloud to persist it. "
                   f"IMPORTANT: Fusion may show a blocking dialog. Screenshot to verify.",
        "data": {
            "filePath": file_path,
            "fileType": file_type,
            "extension": ext,
            "nextSteps": next_steps,
            **ws_info,
        },
        "postOpenCheck": {
            "required": True,
            "steps": [
                "Run desktop_list_windows to find the Fusion HWND",
                "Run desktop_screenshot_window with that HWND",
                "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"}",
                "Screenshot again to confirm the dialog is gone",
            ],
        },
    }


def handle_show_2d_board(app: adsk.core.Application, args: dict) -> dict:
    """Switch back to the 2D board layout from 3D PCB view.

    Uses the Fusion command Electron::Insert2DPCB to return to PCB Editor.
    """
    ws = "unknown"
    try:
        ws = app.executeTextCommand("DebugCommands.ActiveWorkspace").strip()
    except Exception:
        pass

    if ws == "PCB Editor":
        return {
            "success": True,
            "output": "Already in 2D board layout.",
            "data": {"activeWorkspace": ws},
        }

    try:
        app.executeTextCommand("Commands.Start SwitchPcbDocCmd")
    except Exception as e:
        return {
            "success": False,
            "error": f"Switch to 2D board failed: {e}",
            "_hint": "Call fusion_open_board {\"filePath\":\"...\"} to ensure a board is loaded, then retry.",
            "data": {"activeWorkspace": ws},
        }

    time.sleep(1.0)

    new_ws = "unknown"
    try:
        new_ws = app.executeTextCommand("DebugCommands.ActiveWorkspace").strip()
    except Exception:
        pass

    return {
        "success": True,
        "output": "Switched to 2D board layout. EAGLE commands via fusion_electron_run will work.",
        "data": {
            "activeWorkspace": new_ws,
            "nextSteps": [
                "Use fusion_electron_run to send EAGLE commands",
                "Use fusion_show_3d_board to switch back to 3D view",
            ],
        },
    }


def handle_show_schematic(app: adsk.core.Application, args: dict) -> dict:
    """Switch the OPEN electronics design to its SCHEMATIC view.

    The schematic / 2D board / 3D board are VIEWS of ONE electronics design - open the
    PROJECT first (fusion_open_cloud_file / fusion_open_by_urn), then switch views with
    fusion_show_schematic / fusion_show_2d_board / fusion_show_3d_board. Uses Fusion's
    SwitchSchDocCmd (the symmetric partner of SwitchPcbDocCmd / Switch3dPcbDocCmd).
    Drive the schematic for demos with fusion_electron_zoom / _pan / _select.
    """
    ws = "unknown"
    try:
        ws = app.executeTextCommand("DebugCommands.ActiveWorkspace").strip()
    except Exception:
        pass

    # Already viewing the schematic?
    cur_pt = ""
    try:
        cur_pt = app.activeProduct.productType
    except Exception:
        pass
    if cur_pt == "SchematicProductType":
        return {
            "success": True,
            "output": "Already in the schematic view.",
            "data": {"activeWorkspace": ws, "productType": cur_pt},
        }

    try:
        app.executeTextCommand("Commands.Start SwitchSchDocCmd")
    except Exception as e:
        return {
            "success": False,
            "error": f"Switch to schematic failed: {e}",
            "_hint": "Open the electronics DESIGN first (fusion_open_cloud_file / fusion_open_by_urn), "
                     "then retry. The schematic is a VIEW of the design, not a standalone file.",
            "data": {"activeWorkspace": ws},
        }

    time.sleep(1.2)
    try:
        app.executeTextCommand("Electron.run WINDOW FIT")
    except Exception:
        pass

    new_ws = "unknown"
    new_pt = ""
    try:
        new_ws = app.executeTextCommand("DebugCommands.ActiveWorkspace").strip()
    except Exception:
        pass
    try:
        new_pt = app.activeProduct.productType
    except Exception:
        pass

    return {
        "success": True,
        "output": "Switched to the schematic view. Drive it with fusion_electron_zoom / _pan / _select "
                  "or fusion_electron_run WINDOW commands.",
        "message": "Switched to the schematic. Fusion may show a blocking dialog that is NOT visible to "
                   "the API - screenshot the Fusion window to verify.",
        "data": {
            "activeWorkspace": new_ws,
            "productType": new_pt,
            "nextSteps": [
                "fusion_electron_zoom / fusion_electron_pan to move the view for a recording",
                "fusion_show_2d_board / fusion_show_3d_board to switch to the board / 3D",
                "fusion_take_screenshot to capture the schematic",
            ],
        },
        "postOpenCheck": {
            "required": True,
            "steps": [
                "Run desktop_screenshot_window on the Fusion HWND",
                "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"}",
            ],
        },
    }