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.atomic.AtomicBoolean;
import javax.swing.*;
import javax.swing.text.JTextComponent;

/**
 * Generic in-process Swing control bridge.
 *
 * Attach dynamically to any running JVM with jattach:
 *   jattach.exe <pid> load instrument false "C:/path/to/Agent.jar="
 *
 * Once loaded, agentmain() starts a background TCP listener on
 * 127.0.0.1:9797 speaking a simple line-based text protocol:
 *
 *   TREE                        -> dumps the full Swing component tree (JSON)
 *   CLICK <selector>            -> finds first matching AbstractButton and calls doClick()
 *   SETTEXT <selector> <value>  -> finds first matching text component and calls setText()
 *   GETTEXT <selector>          -> returns current text of matched component
 *
 * <selector> matches a component by exact getName() OR exact visible-text match
 * (case-sensitive first pass, case-insensitive fallback), first match wins in
 * a pre-order walk over all top-level windows.
 *
 * All Swing access is executed via SwingUtilities.invokeAndWait to stay on
 * the EDT, as required by Swing's single-threaded rule.
 *
 * Nothing here is MCSJ-specific: it only depends on java.awt / javax.swing.
 */
public class SwingBridgeAgentV2 {

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

    // Entry point used when the agent is attached to an ALREADY RUNNING JVM
    // via jattach.exe load instrument false "Agent.jar=<args>"
    public static void agentmain(String agentArgs, Instrumentation inst) {
        start(agentArgs);
    }

    // Entry point used if the jar is instead loaded at JVM startup with -javaagent
    public static void premain(String agentArgs, Instrumentation inst) {
        start(agentArgs);
    }

    private static void start(String agentArgs) {
        if (!STARTED.compareAndSet(false, true)) {
            return; // already running (e.g. attached twice)
        }
        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);
            }
        }, "SwingBridgeAgentV2-Listener");
        t.setDaemon(true);
        t.start();
        System.err.println("[SwingBridgeAgentV2] 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("[SwingBridgeAgentV2] server error: " + t);
            t.printStackTrace();
        } finally {
            if (server != null) {
                try { server.close(); } catch (IOException ignore) {}
            }
        }
    }

    private static void handleClientSafely(final Socket client) {
        // Handle each client on its own thread so a slow/hanging client can't
        // block the accept loop; EDT access is still serialized by Swing itself.
        Thread worker = new Thread(new Runnable() {
            public void run() {
                try {
                    handleClient(client);
                } catch (Throwable t) {
                    System.err.println("[SwingBridgeAgentV2] client error: " + t);
                } finally {
                    try { client.close(); } catch (IOException ignore) {}
                }
            }
        }, "SwingBridgeAgentV2-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 {
            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
    // ---------------------------------------------------------------

    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));
            }
        }

        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 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();
            // Generic reflection fallback for any other Window subtype with 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 "";
        }
    }

    /**
     * Best-effort extraction of "visible text" from an arbitrary Component.
     * Generic: tries common Swing text-bearing interfaces/classes via
     * instanceof first (fast path), then falls back to reflection calling
     * getText()/getTitle() if present, for any component type not covered
     * explicitly (custom subclasses, third-party widgets, etc).
     */
    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()); // JButton, JCheckBox, JRadioButton, JMenuItem, JToggleButton...
            if (c instanceof JTextComponent) return nullToEmpty(((JTextComponent) c).getText()); // JTextField, JTextArea, JPasswordField...
            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) {
            // fall through to reflection
        }
        // Generic reflection fallback: try getText() on anything else.
        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
    // ---------------------------------------------------------------

    /** Pre-order walk of every top-level Window and its descendants. */
    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);
        }
    }

    /**
     * Find first component whose getName() or visible text exactly matches
     * selector (case-sensitive), then fall back to case-insensitive match if
     * no exact match found.
     */
    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);
    }
}
