import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.*;
import java.net.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.*;
import javax.swing.text.JTextComponent;

/**
 * SwingBridgeAgentV3 — Swing bridge + JavaFX (JFXPanel/Scene graph) bridge,
 * in one class, loaded under a fresh class name to avoid the "already
 * started" static-guard collision documented in REPORT.md for repeated
 * attach/detach cycles against the same long-lived JVM (see Blocker #3 in
 * the original report).
 *
 * Protocol is the original line-based text protocol on 127.0.0.1:9797,
 * PLUS four new FX* commands that reach into embedded JavaFX scene graphs
 * living inside javafx.embed.swing.JFXPanel components found anywhere in
 * the Swing component tree (Window.getWindows() walk, same discovery
 * mechanism as the original Swing TREE):
 *
 *   TREE                          -> (unchanged) full Swing component tree JSON
 *   CLICK <selector>              -> (unchanged) Swing AbstractButton.doClick()
 *   SETTEXT <selector> <value>    -> (unchanged) Swing JTextComponent.setText()
 *   GETTEXT <selector>            -> (unchanged) Swing component text
 *   PING                          -> PONG
 *
 *   FXTREE                        -> walks every JFXPanel's Scene graph (JSON)
 *   FXCLICK <selector>            -> ButtonBase.fire() / Tab select / TableView row select
 *   FXSETTEXT <selector> <value>  -> TextInputControl.setText()
 *   FXGETTEXT <selector>          -> current text of matched FX node
 *
 * CRITICAL THREADING DIFFERENCE vs the Swing half of this file: JavaFX has
 * its OWN application thread (the "JavaFX Application Thread"), completely
 * separate from the Swing EDT. All javafx.scene.* reads/writes here are
 * marshalled onto it via javafx.application.Platform.runLater(), NOT
 * SwingUtilities.invokeAndWait. A CountDownLatch is used to make the
 * TCP-handler-thread call synchronous, mirroring (in spirit, not
 * mechanism) how the Swing half uses SwingUtilities.invokeAndWait.
 *
 * All javafx.* access is done via reflection so this file can still be
 * *compiled* against a JDK that may or may not have JavaFX on its default
 * bootclasspath (older JDK8 non-JavaFX-fx builds, JDK 11+ split-out FX,
 * etc.) as long as jfxrt.jar (or the FX module) is reachable via -cp at
 * compile+run time inside the target JVM's own classpath, which it always
 * is here because MCSJ itself already loads and uses JavaFX (JFXPanel is
 * already resident in the target JVM's runtime classes by the time we
 * inject, since MCSJ's own UI created it long before we attach).
 */
public class SwingBridgeAgentV3 {

    private static final int PORT = 9797;
    private static final AtomicBoolean STARTED = new AtomicBoolean(false);

    public static void agentmain(String agentArgs, Instrumentation inst) { start(agentArgs); }
    public static void premain(String agentArgs, Instrumentation inst) { start(agentArgs); }

    private static void start(String agentArgs) {
        if (!STARTED.compareAndSet(false, true)) return;
        int port = PORT;
        if (agentArgs != null && agentArgs.trim().length() > 0) {
            try { port = Integer.parseInt(agentArgs.trim()); } catch (NumberFormatException ignore) {}
        }
        final int finalPort = port;
        Thread t = new Thread(new Runnable() {
            public void run() { runServer(finalPort); }
        }, "SwingBridgeAgentV3-Listener");
        t.setDaemon(true);
        t.start();
        System.err.println("[SwingBridgeAgentV3] listening on 127.0.0.1:" + finalPort);
    }

    private static void runServer(int port) {
        ServerSocket server = null;
        try {
            server = new ServerSocket();
            server.setReuseAddress(true);
            server.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), port));
            while (true) {
                Socket client = server.accept();
                handleClientSafely(client);
            }
        } catch (Throwable t) {
            System.err.println("[SwingBridgeAgentV3] server error: " + t);
            t.printStackTrace();
        } finally {
            if (server != null) { try { server.close(); } catch (IOException ignore) {} }
        }
    }

    private static void handleClientSafely(final Socket client) {
        Thread worker = new Thread(new Runnable() {
            public void run() {
                try { handleClient(client); }
                catch (Throwable t) { System.err.println("[SwingBridgeAgentV3] client error: " + t); }
                finally { try { client.close(); } catch (IOException ignore) {} }
            }
        }, "SwingBridgeAgentV3-Client");
        worker.setDaemon(true);
        worker.start();
    }

    private static void handleClient(Socket client) throws IOException {
        client.setSoTimeout(30000);
        BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8"));
        OutputStream rawOut = client.getOutputStream();
        PrintWriter out = new PrintWriter(new OutputStreamWriter(rawOut, "UTF-8"), false);

        String line = in.readLine();
        if (line == null) return;
        String response;
        try {
            response = dispatch(line);
        } catch (Throwable t) {
            StringWriter sw = new StringWriter();
            t.printStackTrace(new PrintWriter(sw));
            response = "ERROR " + t.toString() + "\n" + sw.toString();
        }
        out.print(response);
        if (!response.endsWith("\n")) out.print("\n");
        out.flush();
    }

    // ---------------------------------------------------------------
    // Command dispatch
    // ---------------------------------------------------------------

    private static String dispatch(String line) throws Exception {
        line = line.trim();
        if (line.length() == 0) return "ERROR empty command";

        String cmd;
        String rest;
        int sp = line.indexOf(' ');
        if (sp < 0) { cmd = line; rest = ""; } else { cmd = line.substring(0, sp); rest = line.substring(sp + 1).trim(); }
        cmd = cmd.toUpperCase(Locale.ROOT);

        if (cmd.equals("TREE")) {
            return runOnEdtForString(new java.util.concurrent.Callable<String>() {
                public String call() { return dumpTree(); }
            });
        } else if (cmd.equals("CLICK")) {
            final String selector = rest;
            return runOnEdtForString(new java.util.concurrent.Callable<String>() {
                public String call() { return doClick(selector); }
            });
        } else if (cmd.equals("SETTEXT")) {
            int sp2 = rest.indexOf(' ');
            final String selector = sp2 < 0 ? rest : rest.substring(0, sp2);
            final String value = sp2 < 0 ? "" : rest.substring(sp2 + 1);
            return runOnEdtForString(new java.util.concurrent.Callable<String>() {
                public String call() { return doSetText(selector, value); }
            });
        } else if (cmd.equals("GETTEXT")) {
            final String selector = rest;
            return runOnEdtForString(new java.util.concurrent.Callable<String>() {
                public String call() { return doGetText(selector); }
            });
        } else if (cmd.equals("PING")) {
            return "PONG";
        } else if (cmd.equals("FXTREE")) {
            return runOnFxThreadForString(new java.util.concurrent.Callable<String>() {
                public String call() throws Exception { return fxDumpTree(); }
            });
        } else if (cmd.equals("FXCLICK")) {
            final String selector = rest;
            return runOnFxThreadForString(new java.util.concurrent.Callable<String>() {
                public String call() throws Exception { return fxDoClick(selector); }
            });
        } else if (cmd.equals("FXSETTEXT")) {
            int sp2 = rest.indexOf(' ');
            final String selector = sp2 < 0 ? rest : rest.substring(0, sp2);
            final String value = sp2 < 0 ? "" : rest.substring(sp2 + 1);
            return runOnFxThreadForString(new java.util.concurrent.Callable<String>() {
                public String call() throws Exception { return fxDoSetText(selector, value); }
            });
        } else if (cmd.equals("FXGETTEXT")) {
            final String selector = rest;
            return runOnFxThreadForString(new java.util.concurrent.Callable<String>() {
                public String call() throws Exception { return fxDoGetText(selector); }
            });
        } else {
            return "ERROR unknown command: " + cmd;
        }
    }

    private static String runOnEdtForString(final java.util.concurrent.Callable<String> task) throws Exception {
        if (SwingUtilities.isEventDispatchThread()) {
            try { return task.call(); } catch (Exception e) { throw e; } catch (Throwable t) { throw new RuntimeException(t); }
        }
        final String[] result = new String[1];
        final Throwable[] error = new Throwable[1];
        SwingUtilities.invokeAndWait(new Runnable() {
            public void run() {
                try { result[0] = task.call(); } catch (Throwable t) { error[0] = t; }
            }
        });
        if (error[0] != null) {
            if (error[0] instanceof Exception) throw (Exception) error[0];
            throw new RuntimeException(error[0]);
        }
        return result[0];
    }

    // ---------------------------------------------------------------
    // TREE (Swing) — identical logic to SwingBridgeAgent.java
    // ---------------------------------------------------------------

    private static String dumpTree() {
        StringBuilder sb = new StringBuilder();
        Window[] windows = Window.getWindows();
        sb.append("{\"windowCount\":").append(windows.length).append(",\"windows\":[\n");
        for (int i = 0; i < windows.length; i++) {
            appendComponentJson(sb, windows[i], 1);
            if (i < windows.length - 1) sb.append(",");
            sb.append("\n");
        }
        sb.append("]}");
        return sb.toString();
    }

    private static void appendComponentJson(StringBuilder sb, Component c, int indent) {
        indentSb(sb, indent);
        sb.append("{");
        sb.append("\"class\":").append(jsonStr(c.getClass().getName())).append(",");
        sb.append("\"name\":").append(jsonStr(safeName(c))).append(",");
        sb.append("\"text\":").append(jsonStr(safeText(c))).append(",");
        Rectangle b = c.getBounds();
        sb.append("\"bounds\":{\"x\":").append(b.x).append(",\"y\":").append(b.y)
          .append(",\"w\":").append(b.width).append(",\"h\":").append(b.height).append("},");
        sb.append("\"visible\":").append(c.isVisible()).append(",");
        sb.append("\"enabled\":").append(c.isEnabled()).append(",");
        sb.append("\"showing\":").append(c.isShowing());

        if (c instanceof Window) {
            String title = getTitleIfAny(c);
            if (title != null) sb.append(",\"title\":").append(jsonStr(title));
        }

        boolean isJfxPanel = isJFXPanel(c);
        if (isJfxPanel) {
            sb.append(",\"isJFXPanel\":true");
        }

        Component[] children = getChildren(c);
        if (children != null && children.length > 0) {
            sb.append(",\"children\":[\n");
            for (int i = 0; i < children.length; i++) {
                appendComponentJson(sb, children[i], indent + 1);
                if (i < children.length - 1) sb.append(",");
                sb.append("\n");
            }
            indentSb(sb, indent);
            sb.append("]");
        }
        sb.append("}");
    }

    private static boolean isJFXPanel(Component c) {
        try {
            return Class.forName("javafx.embed.swing.JFXPanel").isInstance(c);
        } catch (Throwable t) {
            return false;
        }
    }

    private static Component[] getChildren(Component c) {
        if (c instanceof Container) return ((Container) c).getComponents();
        return null;
    }

    private static String getTitleIfAny(Component c) {
        try {
            if (c instanceof Frame) return ((Frame) c).getTitle();
            if (c instanceof Dialog) return ((Dialog) c).getTitle();
            Method m = c.getClass().getMethod("getTitle");
            Object v = m.invoke(c);
            return v == null ? null : v.toString();
        } catch (Throwable t) { return null; }
    }

    private static String safeName(Component c) {
        try { String n = c.getName(); return n == null ? "" : n; }
        catch (Throwable t) { return ""; }
    }

    private static String safeText(Component c) {
        try {
            if (c instanceof JLabel) return nullToEmpty(((JLabel) c).getText());
            if (c instanceof AbstractButton) return nullToEmpty(((AbstractButton) c).getText());
            if (c instanceof JTextComponent) return nullToEmpty(((JTextComponent) c).getText());
            if (c instanceof JComboBox) {
                Object sel = ((JComboBox) c).getSelectedItem();
                return sel == null ? "" : sel.toString();
            }
            if (c instanceof Frame) return nullToEmpty(((Frame) c).getTitle());
            if (c instanceof Dialog) return nullToEmpty(((Dialog) c).getTitle());
        } catch (Throwable ignore) {}
        try {
            Method m = c.getClass().getMethod("getText");
            Object v = m.invoke(c);
            if (v != null) return v.toString();
        } catch (Throwable ignore) {}
        return "";
    }

    private static String nullToEmpty(String s) { return s == null ? "" : s; }

    private static void indentSb(StringBuilder sb, int n) { for (int i = 0; i < n; i++) sb.append("  "); }

    private static String jsonStr(String s) {
        if (s == null) return "null";
        StringBuilder sb = new StringBuilder("\"");
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            switch (ch) {
                case '"': sb.append("\\\""); break;
                case '\\': sb.append("\\\\"); break;
                case '\n': sb.append("\\n"); break;
                case '\r': sb.append("\\r"); break;
                case '\t': sb.append("\\t"); break;
                default:
                    if (ch < 0x20) sb.append(String.format("\\u%04x", (int) ch));
                    else sb.append(ch);
            }
        }
        sb.append("\"");
        return sb.toString();
    }

    // ---------------------------------------------------------------
    // Selector-based find (Swing)
    // ---------------------------------------------------------------

    private static List<Component> allComponents() {
        List<Component> out = new ArrayList<Component>();
        Window[] windows = Window.getWindows();
        for (Window w : windows) collect(w, out);
        return out;
    }

    private static void collect(Component c, List<Component> out) {
        out.add(c);
        Component[] kids = getChildren(c);
        if (kids != null) for (Component k : kids) collect(k, out);
    }

    private static Component findComponent(String selector) {
        if (selector == null) return null;
        selector = selector.trim();
        if (selector.length() == 0) return null;
        List<Component> all = allComponents();
        for (Component c : all) if (selector.equals(safeName(c)) || selector.equals(safeText(c))) return c;
        for (Component c : all) if (selector.equalsIgnoreCase(safeName(c)) || selector.equalsIgnoreCase(safeText(c))) return c;
        return null;
    }

    private static String doClick(String selector) {
        Component c = findComponent(selector);
        if (c == null) return "NOTFOUND selector=" + selector;
        if (c instanceof AbstractButton) {
            ((AbstractButton) c).doClick();
            return "OK class=" + c.getClass().getName();
        }
        return "NOTCLICKABLE class=" + c.getClass().getName();
    }

    private static String doSetText(String selector, String value) {
        Component c = findComponent(selector);
        if (c == null) return "NOTFOUND selector=" + selector;
        if (c instanceof JTextComponent) {
            ((JTextComponent) c).setText(value);
            return "OK class=" + c.getClass().getName();
        }
        return "NOTTEXTCOMPONENT class=" + c.getClass().getName();
    }

    private static String doGetText(String selector) {
        Component c = findComponent(selector);
        if (c == null) return "NOTFOUND selector=" + selector;
        return "OK class=" + c.getClass().getName() + " text=" + safeText(c);
    }

    // =================================================================
    // JAVAFX BRIDGE
    //
    // All JavaFX access below is done via reflection to avoid a hard
    // compile-time dependency on any one JavaFX packaging scheme, and
    // everything that touches the scene graph is marshalled onto the
    // JavaFX Application Thread via Platform.runLater() + CountDownLatch.
    // =================================================================

    private static final long FX_TIMEOUT_SECONDS = 20;

    /** Runs task on the JavaFX Application Thread and blocks (with timeout) for the result. */
    private static String runOnFxThreadForString(final java.util.concurrent.Callable<String> task) throws Exception {
        ensureFxToolkitInitPossible();
        final String[] result = new String[1];
        final Throwable[] error = new Throwable[1];
        final CountDownLatch latch = new CountDownLatch(1);

        Class<?> platformClass = Class.forName("javafx.application.Platform");
        Method runLater = platformClass.getMethod("runLater", Runnable.class);
        runLater.invoke(null, (Runnable) new Runnable() {
            public void run() {
                try {
                    result[0] = task.call();
                } catch (Throwable t) {
                    error[0] = t;
                } finally {
                    latch.countDown();
                }
            }
        });

        boolean completed = latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS);
        if (!completed) {
            throw new RuntimeException("FX operation timed out after " + FX_TIMEOUT_SECONDS +
                "s waiting for JavaFX Application Thread (Platform.runLater callback never completed)");
        }
        if (error[0] != null) {
            if (error[0] instanceof Exception) throw (Exception) error[0];
            throw new RuntimeException(error[0]);
        }
        return result[0];
    }

    /**
     * Platform.runLater() silently drops the runnable (or throws
     * IllegalStateException on some versions) if the FX toolkit was never
     * initialized in this JVM. Since MCSJ itself already uses JFXPanel
     * (which internally calls Platform.startup / implicit-exit management
     * the first time one is constructed), by the time we attach, the FX
     * toolkit is already up as long as at least one JFXPanel has been
     * constructed somewhere in the Swing tree. We proactively check for at
     * least one JFXPanel instance and give a clear error otherwise, rather
     * than hanging silently on a runLater() that never fires.
     */
    private static void ensureFxToolkitInitPossible() throws Exception {
        boolean found = false;
        for (Component c : allComponents()) {
            if (isJFXPanel(c)) { found = true; break; }
        }
        if (!found) {
            throw new IllegalStateException(
                "No javafx.embed.swing.JFXPanel found anywhere in the current Swing tree " +
                "(Window.getWindows() walk) -- the JavaFX toolkit may not be initialized in " +
                "this JVM yet, or no FX content is currently showing. FXTREE/FXCLICK/etc " +
                "require at least one live JFXPanel to exist first.");
        }
    }

    /** Collect every live JFXPanel anywhere in the Swing tree. */
    private static List<Component> findAllJFXPanels() {
        List<Component> out = new ArrayList<Component>();
        for (Component c : allComponents()) {
            if (isJFXPanel(c)) out.add(c);
        }
        return out;
    }

    // ---------------------------------------------------------------
    // FXTREE
    // ---------------------------------------------------------------

    /** MUST be called already on the JavaFX Application Thread. */
    private static String fxDumpTree() throws Exception {
        List<Component> panels = findAllJFXPanels();
        StringBuilder sb = new StringBuilder();
        sb.append("{\"jfxPanelCount\":").append(panels.size()).append(",\"panels\":[\n");
        for (int i = 0; i < panels.size(); i++) {
            Component panel = panels.get(i);
            indentSb(sb, 1);
            sb.append("{\"panelClass\":").append(jsonStr(panel.getClass().getName())).append(",");
            sb.append("\"panelName\":").append(jsonStr(safeName(panel))).append(",");
            Rectangle pb = panel.getBounds();
            sb.append("\"panelBounds\":{\"x\":").append(pb.x).append(",\"y\":").append(pb.y)
              .append(",\"w\":").append(pb.width).append(",\"h\":").append(pb.height).append("},");

            Method getScene = panel.getClass().getMethod("getScene");
            Object scene = getScene.invoke(panel);
            if (scene == null) {
                sb.append("\"scene\":null}");
            } else {
                sb.append("\"scene\":");
                appendFxSceneJson(sb, scene, 2);
                sb.append("}");
            }
            if (i < panels.size() - 1) sb.append(",");
            sb.append("\n");
        }
        sb.append("]}");
        return sb.toString();
    }

    private static void appendFxSceneJson(StringBuilder sb, Object scene, int indent) throws Exception {
        Method getRoot = scene.getClass().getMethod("getRoot");
        Object root = getRoot.invoke(scene);
        sb.append("{\"sceneClass\":").append(jsonStr(scene.getClass().getName())).append(",\"root\":");
        appendFxNodeJson(sb, root, indent + 1);
        sb.append("}");
    }

    private static void appendFxNodeJson(StringBuilder sb, Object node, int indent) throws Exception {
        if (node == null) { sb.append("null"); return; }
        sb.append("{");
        sb.append("\"class\":").append(jsonStr(node.getClass().getName())).append(",");
        sb.append("\"id\":").append(jsonStr(fxGetId(node))).append(",");
        sb.append("\"text\":").append(jsonStr(fxGetText(node))).append(",");

        Rectangle2DLike boundsInParent = fxGetBounds(node, "getBoundsInParent");
        Rectangle2DLike boundsInLocal = fxGetBounds(node, "getBoundsInLocal");
        appendFxBoundsJson(sb, "boundsInParent", boundsInParent);
        sb.append(",");
        appendFxBoundsJson(sb, "boundsInLocal", boundsInLocal);
        sb.append(",");

        double[] screenXY = fxLocalToScreen(node, boundsInLocal);
        if (screenXY != null) {
            sb.append("\"screenX\":").append(fmtNum(screenXY[0])).append(",");
            sb.append("\"screenY\":").append(fmtNum(screenXY[1])).append(",");
        }

        sb.append("\"visible\":").append(fxGetBool(node, "isVisible")).append(",");
        sb.append("\"disabled\":").append(fxGetBool(node, "isDisabled")).append(",");
        sb.append("\"disable\":").append(fxGetBool(node, "isDisable")).append(",");
        sb.append("\"focused\":").append(fxGetBool(node, "isFocused"));

        // TabPane -> expose tabs and currently selected tab
        if (isInstanceOfFx(node, "javafx.scene.control.TabPane")) {
            try {
                Method getTabs = node.getClass().getMethod("getTabs");
                Object tabsList = getTabs.invoke(node);
                Method getSelectionModel = node.getClass().getMethod("getSelectionModel");
                Object selModel = getSelectionModel.invoke(node);
                Object selectedItem = selModel == null ? null : selModel.getClass().getMethod("getSelectedItem").invoke(selModel);
                sb.append(",\"selectedTabText\":").append(jsonStr(selectedItem == null ? "" : fxGetText(selectedItem)));
                sb.append(",\"tabs\":[");
                if (tabsList instanceof List) {
                    List<?> tabs = (List<?>) tabsList;
                    for (int i = 0; i < tabs.size(); i++) {
                        Object tab = tabs.get(i);
                        sb.append("{\"text\":").append(jsonStr(fxGetText(tab)))
                          .append(",\"id\":").append(jsonStr(fxGetId(tab)))
                          .append(",\"selected\":").append(tab.equals(selectedItem))
                          .append("}");
                        if (i < tabs.size() - 1) sb.append(",");
                    }
                }
                sb.append("]");
            } catch (Throwable ignore) {}
        }

        // TableView -> expose column headers and row count (not full data dump to keep this generic/light)
        if (isInstanceOfFx(node, "javafx.scene.control.TableView")) {
            try {
                Method getColumns = node.getClass().getMethod("getColumns");
                Object colsList = getColumns.invoke(node);
                Method getItems = node.getClass().getMethod("getItems");
                Object itemsList = getItems.invoke(node);
                sb.append(",\"rowCount\":").append(itemsList instanceof List ? ((List<?>) itemsList).size() : 0);
                sb.append(",\"columns\":[");
                if (colsList instanceof List) {
                    List<?> cols = (List<?>) colsList;
                    for (int i = 0; i < cols.size(); i++) {
                        sb.append(jsonStr(fxGetText(cols.get(i))));
                        if (i < cols.size() - 1) sb.append(",");
                    }
                }
                sb.append("]");
            } catch (Throwable ignore) {}
        }

        // Recurse into children if this node is a Parent
        if (isInstanceOfFx(node, "javafx.scene.Parent")) {
            try {
                Method getChildrenUnmod = node.getClass().getMethod("getChildrenUnmodifiable");
                Object childList = getChildrenUnmod.invoke(node);
                if (childList instanceof List) {
                    List<?> kids = (List<?>) childList;
                    if (!kids.isEmpty()) {
                        sb.append(",\"children\":[\n");
                        for (int i = 0; i < kids.size(); i++) {
                            indentSb(sb, indent + 1);
                            appendFxNodeJson(sb, kids.get(i), indent + 1);
                            if (i < kids.size() - 1) sb.append(",");
                            sb.append("\n");
                        }
                        indentSb(sb, indent);
                        sb.append("]");
                    }
                }
            } catch (Throwable ignore) {}
        }

        sb.append("}");
    }

    private static void appendFxBoundsJson(StringBuilder sb, String key, Rectangle2DLike r) {
        sb.append("\"").append(key).append("\":");
        if (r == null) { sb.append("null"); return; }
        sb.append("{\"x\":").append(fmtNum(r.minX)).append(",\"y\":").append(fmtNum(r.minY))
          .append(",\"w\":").append(fmtNum(r.width)).append(",\"h\":").append(fmtNum(r.height)).append("}");
    }

    private static String fmtNum(double d) {
        if (d == Math.floor(d) && !Double.isInfinite(d)) return String.valueOf((long) d);
        return String.valueOf(d);
    }

    private static class Rectangle2DLike {
        double minX, minY, width, height;
    }

    private static Rectangle2DLike fxGetBounds(Object node, String methodName) {
        try {
            Method m = node.getClass().getMethod(methodName);
            Object bounds = m.invoke(node);
            if (bounds == null) return null;
            Rectangle2DLike r = new Rectangle2DLike();
            r.minX = ((Number) bounds.getClass().getMethod("getMinX").invoke(bounds)).doubleValue();
            r.minY = ((Number) bounds.getClass().getMethod("getMinY").invoke(bounds)).doubleValue();
            r.width = ((Number) bounds.getClass().getMethod("getWidth").invoke(bounds)).doubleValue();
            r.height = ((Number) bounds.getClass().getMethod("getHeight").invoke(bounds)).doubleValue();
            return r;
        } catch (Throwable t) { return null; }
    }

    /** node.localToScreen(minX, minY) via reflection -> [screenX, screenY], or null. */
    private static double[] fxLocalToScreen(Object node, Rectangle2DLike localBounds) {
        if (localBounds == null) return null;
        try {
            Method m = node.getClass().getMethod("localToScreen", double.class, double.class);
            Object point2D = m.invoke(node, localBounds.minX, localBounds.minY);
            if (point2D == null) return null;
            double x = ((Number) point2D.getClass().getMethod("getX").invoke(point2D)).doubleValue();
            double y = ((Number) point2D.getClass().getMethod("getY").invoke(point2D)).doubleValue();
            return new double[]{x, y};
        } catch (Throwable t) { return null; }
    }

    private static boolean fxGetBool(Object node, String methodName) {
        try {
            Method m = node.getClass().getMethod(methodName);
            Object v = m.invoke(node);
            return v instanceof Boolean && (Boolean) v;
        } catch (Throwable t) { return false; }
    }

    private static String fxGetId(Object node) {
        try {
            Method m = node.getClass().getMethod("getId");
            Object v = m.invoke(node);
            return v == null ? "" : v.toString();
        } catch (Throwable t) { return ""; }
    }

    private static boolean isInstanceOfFx(Object node, String className) {
        try { return Class.forName(className).isInstance(node); }
        catch (Throwable t) { return false; }
    }

    /**
     * Generic, MCSJ-agnostic visible-text extraction for JavaFX nodes.
     * Covers the common javafx.scene.control widget family via instanceof
     * (fast path) and falls back to reflection on getText()/getTitle() for
     * anything else (custom cell factories, skins, third-party controls).
     */
    private static String fxGetText(Object node) {
        if (node == null) return "";
        try {
            if (isInstanceOfFx(node, "javafx.scene.control.Labeled")) { // Label, Button, ToggleButton, CheckBox, RadioButton, Tab-content-ish
                Method m = node.getClass().getMethod("getText");
                Object v = m.invoke(node);
                if (v != null) return v.toString();
            }
        } catch (Throwable ignore) {}
        try {
            if (isInstanceOfFx(node, "javafx.scene.control.TextInputControl")) { // TextField, TextArea
                Method m = node.getClass().getMethod("getText");
                Object v = m.invoke(node);
                if (v != null) return v.toString();
            }
        } catch (Throwable ignore) {}
        try {
            if (isInstanceOfFx(node, "javafx.scene.control.ComboBoxBase")) { // ComboBox, DatePicker
                Method m = node.getClass().getMethod("getValue");
                Object v = m.invoke(node);
                if (v != null) return v.toString();
            }
        } catch (Throwable ignore) {}
        try {
            if (isInstanceOfFx(node, "javafx.scene.control.Tab")) {
                Method m = node.getClass().getMethod("getText");
                Object v = m.invoke(node);
                if (v != null) return v.toString();
            }
        } catch (Throwable ignore) {}
        try {
            if (isInstanceOfFx(node, "javafx.scene.control.TableColumnBase")) { // TableColumn, TreeTableColumn
                Method m = node.getClass().getMethod("getText");
                Object v = m.invoke(node);
                if (v != null) return v.toString();
            }
        } catch (Throwable ignore) {}
        try {
            if (isInstanceOfFx(node, "javafx.scene.text.Text")) {
                Method m = node.getClass().getMethod("getText");
                Object v = m.invoke(node);
                if (v != null) return v.toString();
            }
        } catch (Throwable ignore) {}
        // Generic reflection fallback for anything not covered above.
        try {
            Method m = node.getClass().getMethod("getText");
            Object v = m.invoke(node);
            if (v != null) return v.toString();
        } catch (Throwable ignore) {}
        return "";
    }

    // ---------------------------------------------------------------
    // FX selector-based find
    // ---------------------------------------------------------------

    /** Pre-order walk of every JFXPanel's Scene graph (all panels, all windows). */
    private static List<Object> allFxNodes() throws Exception {
        List<Object> out = new ArrayList<Object>();
        for (Component panel : findAllJFXPanels()) {
            Method getScene = panel.getClass().getMethod("getScene");
            Object scene = getScene.invoke(panel);
            if (scene == null) continue;
            Method getRoot = scene.getClass().getMethod("getRoot");
            Object root = getRoot.invoke(scene);
            collectFx(root, out);
        }
        return out;
    }

    private static void collectFx(Object node, List<Object> out) throws Exception {
        if (node == null) return;
        out.add(node);
        if (isInstanceOfFx(node, "javafx.scene.Parent")) {
            Method getChildrenUnmod = node.getClass().getMethod("getChildrenUnmodifiable");
            Object childList = getChildrenUnmod.invoke(node);
            if (childList instanceof List) {
                for (Object kid : (List<?>) childList) collectFx(kid, out);
            }
        }
        // TabPane holds Tab objects that are NOT part of getChildrenUnmodifiable()
        // (Tab is not a Node); walk them separately so selectors can find Tab text/id.
        if (isInstanceOfFx(node, "javafx.scene.control.TabPane")) {
            try {
                Method getTabs = node.getClass().getMethod("getTabs");
                Object tabsList = getTabs.invoke(node);
                if (tabsList instanceof List) {
                    for (Object tab : (List<?>) tabsList) {
                        out.add(tab);
                        // A Tab's content Node may itself contain further controls
                        // (e.g. a GridPane of fields inside a tab) not otherwise
                        // reached because Tab isn't a Node in getChildrenUnmodifiable().
                        try {
                            Method getContent = tab.getClass().getMethod("getContent");
                            Object content = getContent.invoke(tab);
                            if (content != null) collectFx(content, out);
                        } catch (Throwable ignore) {}
                    }
                }
            } catch (Throwable ignore) {}
        }
    }

    /**
     * find first FX node whose fx:id or visible text matches selector.
     *
     * Matching is done in 6 passes, each a full scan, in priority order:
     *   1. exact match, "interactive" control classes (Tab, ButtonBase,
     *      TextInputControl, ComboBoxBase, TableView/TableColumn) --
     *      excludes plain Labeled/Label on purpose, see below
     *   2. exact match, any other "notable" public-API class (Labeled/Label)
     *   3. exact match, any node at all (including internal skin classes)
     *   4-6. same three passes again, case-insensitive
     *
     * The staged priority exists because JavaFX skins duplicate a Tab's
     * text onto an internal, but PUBLIC-API-typed, javafx.scene.control.Label
     * living inside the TabPane's header skin (e.g. selecting "TabTwo"
     * would otherwise land on that decorative header Label instead of the
     * actual Tab object, since both are "notable" classes and the Label is
     * reached first in traversal order). Ranking actionable control types
     * above Labeled fixes this without needing MCSJ-specific hacks.
     */
    private static Object findFxNode(String selector) throws Exception {
        if (selector == null) return null;
        selector = selector.trim();
        if (selector.length() == 0) return null;
        List<Object> all = allFxNodes();

        for (Object n : all) {
            if (isInteractiveFxClass(n) && (selector.equals(fxGetId(n)) || selector.equals(fxGetText(n)))) return n;
        }
        for (Object n : all) {
            if (isNotableFxClass(n) && (selector.equals(fxGetId(n)) || selector.equals(fxGetText(n)))) return n;
        }
        for (Object n : all) {
            if (selector.equals(fxGetId(n)) || selector.equals(fxGetText(n))) return n;
        }
        for (Object n : all) {
            if (isInteractiveFxClass(n) && (selector.equalsIgnoreCase(fxGetId(n)) || selector.equalsIgnoreCase(fxGetText(n)))) return n;
        }
        for (Object n : all) {
            if (isNotableFxClass(n) && (selector.equalsIgnoreCase(fxGetId(n)) || selector.equalsIgnoreCase(fxGetText(n)))) return n;
        }
        for (Object n : all) {
            if (selector.equalsIgnoreCase(fxGetId(n)) || selector.equalsIgnoreCase(fxGetText(n))) return n;
        }
        return null;
    }

    /** Highest-priority actionable control types: things FXCLICK/FXSETTEXT can actually do something with. */
    private static boolean isInteractiveFxClass(Object node) {
        if (node == null) return false;
        String cn = node.getClass().getName();
        if (cn.startsWith("com.sun.javafx.")) return false;
        return isInstanceOfFx(node, "javafx.scene.control.Tab")
            || isInstanceOfFx(node, "javafx.scene.control.ButtonBase")
            || isInstanceOfFx(node, "javafx.scene.control.TextInputControl")
            || isInstanceOfFx(node, "javafx.scene.control.ComboBoxBase")
            || isInstanceOfFx(node, "javafx.scene.control.TableView")
            || isInstanceOfFx(node, "javafx.scene.control.TableColumnBase");
    }

    /**
     * True for real public-API javafx.scene.control.* / javafx.scene.Parent
     * node types a caller is likely to actually mean (Tab, ButtonBase,
     * TextInputControl, ComboBoxBase, TableView/TableColumn, Labeled),
     * false for internal skin/implementation classes
     * (com.sun.javafx.scene.control.skin.*, *Skin$*, etc.) that merely
     * happen to carry the same visible text as decoration.
     */
    private static boolean isNotableFxClass(Object node) {
        if (node == null) return false;
        String cn = node.getClass().getName();
        if (cn.startsWith("com.sun.javafx.")) return false;
        if (cn.contains("Skin$") || cn.endsWith("Skin")) return false;
        return isInteractiveFxClass(node) || isInstanceOfFx(node, "javafx.scene.control.Labeled");
    }

    // ---------------------------------------------------------------
    // FXCLICK / FXSETTEXT / FXGETTEXT — MUST be called already on the
    // JavaFX Application Thread (invoked from inside a Platform.runLater).
    // ---------------------------------------------------------------

    private static String fxDoClick(String selector) throws Exception {
        Object node = findFxNode(selector);
        if (node == null) return "NOTFOUND selector=" + selector;

        // Case 1: ButtonBase (Button, ToggleButton, CheckBox, RadioButton, Hyperlink...)
        // -- fire() invokes the action handler directly, no synthetic mouse event needed.
        if (isInstanceOfFx(node, "javafx.scene.control.ButtonBase")) {
            Method fire = node.getClass().getMethod("fire");
            fire.setAccessible(true);
            fire.invoke(node);
            return "OK class=" + node.getClass().getName() + " method=fire()";
        }

        // Case 2: Tab -- select it via its owning TabPane's selection model.
        if (isInstanceOfFx(node, "javafx.scene.control.Tab")) {
            Method getTabPane = node.getClass().getMethod("getTabPane");
            Object tabPane = getTabPane.invoke(node);
            if (tabPane == null) {
                return "ERROR tab has no owning TabPane (not attached?) class=" + node.getClass().getName();
            }
            Method getSelectionModel = tabPane.getClass().getMethod("getSelectionModel");
            Object selModel = getSelectionModel.invoke(tabPane);
            Method select = selModel.getClass().getMethod("select", Class.forName("javafx.scene.control.Tab"));
            // The concrete selection-model class (e.g. TabPane$TabPaneSelectionModel) is
            // package-private even though its methods are public, so a plain invoke()
            // throws IllegalAccessException. setAccessible(true) bypasses that language-level
            // check; safe here since we're already running as an injected in-process agent
            // with full reflective access to this JVM.
            select.setAccessible(true);
            select.invoke(selModel, node);
            return "OK class=" + node.getClass().getName() + " method=TabPane.getSelectionModel().select(tab)";
        }

        // Case 3: TableView -- select a row by index encoded as the selector
        // in the form "TableView:<index>" is NOT how selectors work here
        // (selector matches id/text) -- instead if the node itself IS a
        // TableView, select its first row as a conservative default, and
        // note this limitation clearly rather than guessing further.
        if (isInstanceOfFx(node, "javafx.scene.control.TableView")) {
            Method getSelectionModel = node.getClass().getMethod("getSelectionModel");
            Object selModel = getSelectionModel.invoke(node);
            Method selectFirst = selModel.getClass().getMethod("selectFirst");
            selectFirst.setAccessible(true);
            selectFirst.invoke(selModel);
            return "OK class=" + node.getClass().getName() +
                " method=TableView.getSelectionModel().selectFirst() " +
                "(NOTE: FXCLICK on a TableView selects row 0 only; row-index / cell-level " +
                "selection is not implemented in this generic bridge -- would need a " +
                "TableView-specific selector syntax, out of scope here)";
        }

        // Case 4: anything else that exposes a no-arg fire()-like or
        // requestFocus()+synthetic MouseEvent fallback. Try a generic
        // "fire" method via reflection first (covers MenuItem, which is
        // not a Node but is common inside FX menu bars).
        try {
            Method fire = node.getClass().getMethod("fire");
            fire.invoke(node);
            return "OK class=" + node.getClass().getName() + " method=fire() (generic reflection path)";
        } catch (NoSuchMethodException nsme) {
            // fall through to synthetic MouseEvent as last resort
        }

        // Last resort: synthetic MouseEvent with click count 2 (double-click),
        // matching the task brief's suggestion for TableView cell editing.
        // Attempted only if the node is a javafx.scene.Node (has fireEvent).
        if (isInstanceOfFx(node, "javafx.scene.Node")) {
            try {
                Object screenPoint = null; // best-effort center point
                double cx = 0, cy = 0;
                try {
                    Method getBoundsInLocal = node.getClass().getMethod("getBoundsInLocal");
                    Object b = getBoundsInLocal.invoke(node);
                    double w = ((Number) b.getClass().getMethod("getWidth").invoke(b)).doubleValue();
                    double h = ((Number) b.getClass().getMethod("getHeight").invoke(b)).doubleValue();
                    cx = w / 2; cy = h / 2;
                } catch (Throwable ignore) {}

                Class<?> meClass = Class.forName("javafx.scene.input.MouseEvent");
                Class<?> metClass = Class.forName("javafx.scene.input.MouseButton");
                Object primary = metClass.getField("PRIMARY").get(null);

                Constructor<?> ctor = null;
                for (Constructor<?> c2 : meClass.getConstructors()) {
                    if (c2.getParameterTypes().length == 15) { ctor = c2; break; } // classic 15-arg MouseEvent ctor
                }
                if (ctor == null) {
                    return "ERROR no synthetic-MouseEvent constructor found (15-arg overload) for class=" + node.getClass().getName();
                }
                Class<?> eventTypeClass = Class.forName("javafx.event.EventType");
                Object mouseClicked = meClass.getField("MOUSE_CLICKED").get(null);
                Object evt = ctor.newInstance(
                    mouseClicked, cx, cy, cx, cy, primary, 2 /*clickCount*/,
                    false, false, false, false, true /*primaryButtonDown*/, false, false,
                    false, false, false, null);
                Method fireEvent = Class.forName("javafx.event.Event").getMethod("fireEvent",
                    Class.forName("javafx.event.EventTarget"), Class.forName("javafx.event.Event"));
                fireEvent.invoke(null, node, evt);
                return "OK class=" + node.getClass().getName() +
                    " method=Event.fireEvent(node, synthetic MOUSE_CLICKED clickCount=2) (fallback path, less reliable than fire())";
            } catch (Throwable t) {
                return "ERROR synthetic MouseEvent fallback failed for class=" + node.getClass().getName() + ": " + t;
            }
        }

        return "NOTCLICKABLE class=" + node.getClass().getName();
    }

    private static String fxDoSetText(String selector, String value) throws Exception {
        Object node = findFxNode(selector);
        if (node == null) return "NOTFOUND selector=" + selector;
        if (isInstanceOfFx(node, "javafx.scene.control.TextInputControl")) {
            Method setText = node.getClass().getMethod("setText", String.class);
            setText.setAccessible(true);
            setText.invoke(node, value);
            return "OK class=" + node.getClass().getName();
        }
        return "NOTTEXTCOMPONENT class=" + node.getClass().getName();
    }

    private static String fxDoGetText(String selector) throws Exception {
        Object node = findFxNode(selector);
        if (node == null) return "NOTFOUND selector=" + selector;
        return "OK class=" + node.getClass().getName() + " text=" + fxGetText(node);
    }
}
