import java.awt.*;
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.embed.swing.SwingNode;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;

/**
 * Reproduces SwingNode rendering artifacts with JavaFX 19.0.2.1, 21.0.9, and 25.0.1.
 */
public class SwingNodeRenderingBugReproducer extends Application {

    @Override
    public void start(final Stage primaryStage) {
        final TabPane tabPane = new TabPane();

        for (int i = 1; i <= 4; i++) {
            final Tab tab = createTabWithTable("Tab " + i);
            tabPane.getTabs().add(tab);
        }

        final BorderPane root = new BorderPane(tabPane);
        root.setStyle("-fx-padding: 1em;");
        final Label label = new Label("""
                After this window opens, resize it a few times and select a few different tabs.
                Rapid, significant size changes seem to help reproduce the bug.
                
                After a few tries, one or more of the unselected tabs will have JComponent rendering artifacts,
                and you will see the problem after you select the tab.
                
                Press the button below to clear the rendering artifacts once.
                Resizing the window also seems to reliably clear the rendering artifacts.
                
                The bug seems to happen when the SwingNode size changes while it is hierarchically invisible,
                and then transitions to hierarchically visible.
                For example, when TabPaneSkin's TabContentRegion sets itself invisible when deselected, visible when selected.
                
                Select the checkbox below to clear rendering artifacts every time each SwingNode
                becomes hierarchically visible.
                Doing that seems to reliably compensate for the rendering bug.
                """
        );
        label.setStyle("-fx-padding: 1em;");

        final Button button = new Button(
                "Revalidate and repaint selected tab's SwingNode content");
        button.setOnAction(e -> {
            final Tab selectedTab =
                    tabPane.getSelectionModel().getSelectedItem();
            if (selectedTab != null) {
                final SwingNode swingNode = (SwingNode) selectedTab.getContent();
                final JComponent content = swingNode.getContent();
                content.revalidate();
                content.repaint();
            }
        });

        final CheckBox checkBox = new CheckBox(
                "Apply hierarchical SwingNode visibility workaround.\n" +
                        "While checked, the workaround seems to reliably remove rendering artifacts.");
        checkBox.selectedProperty().addListener(
                observable -> {
                    if (checkBox.isSelected()) {
                        // To prevent confusion, clear rendering artifacts on the selected Tab now.
                        button.fire();
                    }
                }
        );

        final VBox vBox = new VBox(label, button, checkBox);
        vBox.setStyle("-fx-spacing: 1em;");
        vBox.setAlignment(Pos.CENTER);
        root.setBottom(vBox);
        final Scene scene = new Scene(root, 800, 600);

        primaryStage.setTitle("SwingNode Rendering Bug Reproducer");
        primaryStage.setScene(scene);
        primaryStage.show();

        tabPane.getTabs().forEach(tab -> {
            final SwingNode swingNode = (SwingNode) tab.getContent();

            // Apply the HierarchicalVisibilityTracker.
            HierarchicalVisibilityTracker tracker =
                    new HierarchicalVisibilityTracker(swingNode);

            tracker.hierarchicallyVisibleProperty().addListener(
                    (obs, wasVisible, isNowHierarchicallyVisible) -> {
                        // If the SwingNode just became hierarchically visible and the
                        // checkbox is selected, apply the workaround to clear rendering artifacts.
                        if (isNowHierarchicallyVisible && checkBox.isSelected()) {
                            SwingUtilities.invokeLater(() -> {
                                JComponent content = swingNode.getContent();
                                if (content != null) {
                                    // Clear rendering artifacts.
                                    content.revalidate();
                                    content.repaint();
                                }
                            });
                        }
                    });
        });
    }

    private Tab createTabWithTable(final String title) {
        final SwingNode swingNode = new SwingNode();
        final JPanel panel = new JPanel(new BorderLayout());
        final JTable table = createTable();

        final JScrollPane scrollPane = new JScrollPane(table);
        panel.add(scrollPane, BorderLayout.CENTER);
        swingNode.setContent(panel);

        final Tab tab = new Tab(title);
        tab.setContent(swingNode);
        return tab;
    }

    private JTable createTable() {
        final String[] columnNames =
                {"Column 1", "Column 2", "Column 3", "Column 4"};
        final Object[][] emptyData = new Object[30][4];

        final DefaultTableModel tableModel =
                new DefaultTableModel(emptyData, columnNames);
        final JTable table = new JTable(tableModel);
        for (int i = 0; i < 30; i++) {
            table.setValueAt("Row " + (i + 1) + ", Col 1", i, 0);
            table.setValueAt("Row " + (i + 1) + ", Col 2", i, 1);
            table.setValueAt("Row " + (i + 1) + ", Col 3", i, 2);
            table.setValueAt("Row " + (i + 1) + ", Col 4", i, 3);
        }
        return table;
    }

    private static class HierarchicalVisibilityTracker {

        private final ReadOnlyBooleanWrapper hierarchicallyVisible =
                new ReadOnlyBooleanWrapper(false);

        private final Node nodeToTrack;

        public HierarchicalVisibilityTracker(Node nodeToTrack) {
            this.nodeToTrack = nodeToTrack;

            // Add listeners to track visibility of the node and its *current* ancestors.
            // A production quality implementation would dynamically add and remove listeners
            // as nodeToTrack's ancestry changes, and conditionally on whether nodeToTrack
            // is contained in a Scene. But, in this bug reproducer we won't change
            // nodeToTrack's ancestry or its Scene, so this simpler implementation suffices.
            InvalidationListener visibilityListener =
                    obs -> updateHierarchicallyVisibleProperty();
            Node node = nodeToTrack;
            while (node != null) {
                node.visibleProperty().addListener(visibilityListener);
                node = node.getParent();
            }

            // Initial update of hierarchical visibility.
            updateHierarchicallyVisibleProperty();
        }

        public ReadOnlyBooleanProperty hierarchicallyVisibleProperty() {
            return hierarchicallyVisible.getReadOnlyProperty();
        }

        private void updateHierarchicallyVisibleProperty() {
            Node node = nodeToTrack;
            boolean visible = true;

            // Traverse through nodeToTrack and its ancestors to check visibility
            while (node != null) {
                if (!node.isVisible()) {
                    visible = false;
                    break;
                }
                node = node.getParent();
            }

            hierarchicallyVisible.set(visible);
        }
    }

    static void main(String[] args) {
        Application.launch(SwingNodeRenderingBugReproducer.class, args);
    }
} 