/* ###
 * IP: GHIDRA
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package help.screenshot;

import static org.junit.Assert.*;

import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Point2D.Double;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.*;

import org.junit.*;

import docking.*;
import docking.action.DockingAction;
import docking.menu.ActionState;
import docking.menu.MultiStateDockingAction;
import docking.util.image.Callout;
import docking.util.image.CalloutComponentInfo;
import docking.widgets.dialogs.MultiLineInputDialog;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.visualization.VisualizationServer;
import edu.uci.ics.jung.visualization.VisualizationViewer;
import generic.test.TestUtils;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.app.cmd.function.DeleteFunctionCmd;
import ghidra.app.cmd.label.AddLabelCmd;
import ghidra.app.plugin.core.functiongraph.AbstractFunctionGraphTest;
import ghidra.app.plugin.core.functiongraph.FGProvider;
import ghidra.app.plugin.core.functiongraph.graph.*;
import ghidra.app.plugin.core.functiongraph.graph.layout.FGLayoutProvider;
import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex;
import ghidra.app.plugin.core.functiongraph.graph.vertex.GroupedFunctionGraphVertex;
import ghidra.app.plugin.core.functiongraph.mvc.FGController;
import ghidra.app.plugin.core.functiongraph.mvc.FGView;
import ghidra.graph.viewer.GraphViewerUtils;
import ghidra.program.model.address.Address;
import ghidra.program.model.symbol.SourceType;
import ghidra.test.TestEnv;
import ghidra.util.Msg;
import ghidra.util.exception.AssertException;

public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {

	private MyScreen screen;
	private int width = 400;
	private int height = 400;

	public FunctionGraphPluginScreenShots() {
		super();
	}

	@Override
	@Before
	public void setUp() throws Exception {

		screen = new MyScreen();
		try {
			screen.setUp();
		}
		catch (Exception e) {
			failWithException("Unepected exception in setup", e);
		}

		super.setUp();

		screen.program = program;

		setLayout();
	}

	@Override
	@After
	public void tearDown() throws Exception {
		// We share the program--don't let the screen release it's copy
		// screen.env.release(screen.program);

		super.tearDown();

		screen.saveOrDisplayImage(testName.getMethodName());
	}

	@Override
	protected TestEnv getEnv() {
		return screen.env;
	}

	@Override
	protected String getStartingProgramName() {
		return "WinHelloCPP.exe";
	}

	@Override
	protected void openProgram() {
		program = env.getProgram(getStartingProgramName());
	}

	@Override
	protected String getStartingAddress() {
		return "406630";
	}

	@Test
	public void testFunctionGraphWindow() {
		go("406630");// _strlen function

		setSize(750, 560);// size first, as the positioning is sensitive to the size
		setZoom(1.0);// full zoom
		centerDisplay();
		captureProviderWindow();
	}

	@Test
	public void testFunctionGraph_Provider_Undefined() {
		createUndefinedFunction();

		isolateProvider();
		hideSatellite();

		setSize(700, 500);// size first, as the positioning is sensitive to the size		
		setZoom(.25);// zoom out a bit to show more vertices
		centerDisplay();
		captureProvider();
		cropAndRemoveHeader(388, 194);
	}

	@Test
	public void testFunctionGraph_Stale_Graph() {
		String address = "406630";
		go(address);// _strlen function

		hideSatellite();

		setSize(612, 300);

		// make a change to trigger the 'stale graph' message
		changeLabel(getAddress(address));

		captureProviderWindow();

		cropAndKeepMessageSection();

		drawRectangleAroundMessageText();
	}

	public void testFunctionGraph_Vertex_Header() {
		String address = "406630";
		go(address);// _strlen function

		setSize(1000, 1000);// size first, as the positioning is sensitive to the size
		setZoom(1.0);// full zoom
		centerDisplay();

		captureProvider();
		cropRootVertexHeader();
	}

	@Test
	public void testFunctionGraph_Grouped_Vertex_Header() {
		String functionAddress = "405d29";
		go(functionAddress);// ___updatetmbcinfo function

		setSize(1000, 1000);// size first, as the positioning is sensitive to the size
		setZoom(1.0);// full zoom

		String a1 = "405d46";
		String a2 = "405d4c";
		FGVertex v1 = vertex(a1);
		FGVertex v2 = vertex(a2);

		GroupedFunctionGraphVertex group = group(null, v1, v2);
		centerDisplay();

		captureProvider();
		cropVertexHeader(group);
	}

	@Test
	public void testFunctionGraph_Pre_Group() {
		String functionAddress = "405d29";
		go(functionAddress);// ___updatetmbcinfo function
		hideSatellite();

		setNestedLayout();

		setSize(1000, 600);// size first, as the positioning is sensitive to the size
		setZoom(1d);// full zoom

		FGVertex root = vertex(functionAddress);

		String a1 = "405d46";
		String a2 = "405d4c";
		FGVertex v1 = vertex(a1);
		FGVertex v2 = vertex(a2);

		pickVertices(v1, v2);
		moveGraphToUpperLeftCorner(root);

		captureProvider();
		cropVertices(root, v1, v2);

//		createGroupButtonCallout_PlayArea(v1, "FunctionGraph_Pre_Group.png");
		createGroupButtonCallout(v1);
	}

	@Test
	public void testFunctionGraph_Post_Group() {
		String functionAddress = "405d29";
		go(functionAddress);// ___updatetmbcinfo function

		setNestedLayout();

		setSize(1000, 500);// size first, as the positioning is sensitive to the size
		setZoom(.85);

		FGVertex root = vertex(functionAddress);
		moveGraphToUpperLeftCorner(root);

		String a1 = "405d46";
		String a2 = "405d4c";
		FGVertex v1 = vertex(a1);
		FGVertex v2 = vertex(a2);

		GroupedFunctionGraphVertex group = group(null, v1, v2);

		captureProvider();
		cropVertices(root, group);
	}

	@Test
	public void testFunctionGraph_Group_Text_Dialog() throws Exception {
		String functionAddress = "405d29";
		go(functionAddress);// ___updatetmbcinfo function

		FGVertex root = vertex(functionAddress);
		moveGraphToUpperLeftCorner(root);

		String a1 = "405d46";
		String a2 = "405d4c";
		FGVertex v1 = vertex(a1);
		FGVertex v2 = vertex(a2);

		DialogComponentProvider dialog = showGroupTextDialog(v1, v2);
		screen.captureDialog(dialog);
		pressButtonByText(dialog.getComponent(), "Cancel");
	}

	@Test
	public void testFunctionGraph_Provider_Header() {

		setSize(800, 400);

		captureProviderWindow();

		GenericHeader providerHeader = getHeader();
		cropHeaderToActions(providerHeader);
	}

	@Test
	public void testFunctionGraph_Vertex_Drop_Shadow() {
		//
		// This image is a bit abnormal.  It is a picture of a vertex next to itself after it
		// has been zoomed out past its 'interaction threshold'.  Further, both vertices are 
		// zoomed out past the point of readability.
		//
		String functionAddress = "4057c4";
		go(functionAddress);// __fflush_nolock

		setNestedLayout();

		setSize(500, 500);// size first, as the positioning is sensitive to the size
		double beforeThresholdZoom = GraphViewerUtils.INTERACTION_ZOOM_THRESHOLD + .01;
		setZoom(beforeThresholdZoom);
		moveGraphToUpperLeftCorner();

		Image graphImage1 = captureGraph();

		double pastThresholdZoom = GraphViewerUtils.INTERACTION_ZOOM_THRESHOLD - .01;
		setZoom(pastThresholdZoom);
		moveGraphToUpperLeftCorner();

		Image graphImage2 = captureGraph();

		// create an empty image onto which we will place both images, side-by-side
		int bufferSpace = 50;
		int graphWidth = graphImage1.getWidth(null) * 2 + bufferSpace;
		int graphHeight = graphImage1.getHeight(null);
		BufferedImage fullImage = screen.createEmptyImage(graphWidth, graphHeight);

		Graphics2D g = (Graphics2D) fullImage.getGraphics();
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g.drawImage(graphImage1, 0, 0, null);

		int xOffset = graphImage1.getWidth(null) + bufferSpace;
		g.drawImage(graphImage2, xOffset, 0, null);

		int w = 3;
		int x1 = graphImage1.getWidth(null) + (bufferSpace / 2) - (w / 2);// between images
		int y1 = 10;// down a bit

		int h = (graphImage1.getHeight(null) - 10) - y1;

		g.setColor(Palette.BLACK);
		g.fillRect(x1, y1, w, h);

		screen.image = fullImage;
	}

// For Debug of callout	
//	public void testCallout() {
//		go("406630"); // _strlen function
//
//		setSize(750, 560); // size first, as the positioning is sensitive to the size
//		centerDisplay();
//
//		doTestCallout();
//	}
//
//	private void doTestCallout() {
//		ComponentProvider provider = screen.getComponentProvider(FunctionGraphProvider.class);
//		JComponent providerComponent = provider.getComponent();
//		DockableComponent parent = getDockableComponent(providerComponent);
//		JButton component = getToolbarButton(provider, "Go To Function Entry Point");
//
//		Rectangle bounds = component.getBounds();
//		Point relativeLocation = bounds.getLocation();
//		Dimension size = bounds.getSize();
//		Point screenLocation = new Point(relativeLocation);
//		SwingUtilities.convertPointToScreen(screenLocation, component.getParent());
//		CalloutComponentInfo info =
//			new CalloutComponentInfo(parent, component, screenLocation, relativeLocation, size);
//
//		createCallout(parent, info);
//		showImage("FunctionGraphPlugin", "FunctionGraph_Post_Group.png");
//	}

//==================================================================================================
// Private Methods
//==================================================================================================	

	private Image captureGraph() {
		FGController controller = getFunctionGraphController();
		FGView view = controller.getView();
		VisualizationViewer<FGVertex, FGEdge> viewer = view.getPrimaryGraphViewer();
		FunctionGraph functionGraph = getFunctionGraph();
		Graph<FGVertex, FGEdge> graph = functionGraph;
		Collection<FGVertex> vertices = graph.getVertices();
		Rectangle layoutBounds =
			GraphViewerUtils.getBoundsForVerticesInLayoutSpace(viewer, vertices);
		Rectangle viewBounds =
			GraphViewerUtils.translateRectangleFromLayoutSpaceToViewSpace(viewer, layoutBounds);

		// add some padding
		int padding = 40;
		viewBounds.x -= padding;
		viewBounds.y -= padding;
		viewBounds.width += (2 * padding);
		viewBounds.height += (2 * padding);

		FGProvider provider = screen.getProvider(FGProvider.class);
		JComponent parent = provider.getComponent();
		DockableComponent dc = getDockableComponent(parent);

		viewBounds = SwingUtilities.convertRectangle(provider.getComponent(), viewBounds, dc);

		captureProvider();

		screen.crop(viewBounds);

		return screen.image;
	}

	private Image captureVertex(FGVertex v) {

		// make sure the vertex is showing
		moveGraphToUpperLeftCorner(v);

		FGController controller = getFunctionGraphController();
		FGView view = controller.getView();
		VisualizationViewer<FGVertex, FGEdge> viewer = view.getPrimaryGraphViewer();

		FGProvider provider = screen.getProvider(FGProvider.class);
		JComponent parent = provider.getComponent();
		DockableComponent dc = getDockableComponent(parent);

		Rectangle vertexBounds = GraphViewerUtils.getVertexBoundsInViewSpace(viewer, v);
		JComponent component = v.getComponent();

		vertexBounds = SwingUtilities.convertRectangle(component.getParent(), vertexBounds, dc);

		captureProvider();

		screen.crop(vertexBounds);

		return screen.image;
	}

	private void cropHeaderToActions(final GenericHeader header) {
		final AtomicReference<Rectangle> ref = new AtomicReference<>();
		runSwing(() -> {
			int actionsWidth = header.getToolBarWidth();

			Window window = windowForComponent(header);
			Rectangle headerBounds = header.getBounds();
			Rectangle bounds = new Rectangle(headerBounds);

			// ugh--apparently the toolbar does not correctly calculate its minimum size
			int padding = 30;

			bounds = SwingUtilities.convertRectangle(header.getParent(), headerBounds, window);
			bounds.x = bounds.x + bounds.width - (actionsWidth + padding);

			padding = 8;// now some vertical padding
			bounds.y -= padding;
			bounds.height += (2 * padding);

			ref.set(bounds);
		});

		Rectangle bounds = ref.get();
		screen.crop(bounds);
	}

	private GenericHeader getHeader() {
		FGController controller = getFunctionGraphController();
		ComponentProvider provider = controller.getProvider();
		DockableComponent dc = getDockableComponent(provider.getComponent());
		return dc.getHeader();
	}

	private void createCallout(JComponent parentComponent, CalloutComponentInfo calloutInfo) {
		// create image of parent with extra space for callout feature
		Image parentImage = screen.captureComponent(parentComponent);

		Callout callout = new Callout();
		screen.image = callout.createCalloutOnImage(parentImage, calloutInfo);

//		createCalloutOnImage(parentImage, parentComponent, calloutInfo);
	}

	private void createGroupButtonCallout(FGVertex v) {

		JButton component = getToolbarButton(v, "Group Vertices");
		FGProvider provider = screen.getProvider(FGProvider.class);
		JComponent parent = provider.getComponent();

		FGController controller = getFunctionGraphController();
		FGView view = controller.getView();
		VisualizationViewer<FGVertex, FGEdge> viewer = view.getPrimaryGraphViewer();

		Rectangle bounds = component.getBounds();
		Dimension size = bounds.getSize();
		Point location = bounds.getLocation();

		JComponent vertexComponent = v.getComponent();
		Point newLocation =
			SwingUtilities.convertPoint(component.getParent(), location, vertexComponent);

		Point relativePoint = GraphViewerUtils.translatePointFromVertexRelativeSpaceToViewSpace(
			viewer, v, newLocation);

		Point screenLocation = new Point(relativePoint);
		SwingUtilities.convertPointToScreen(screenLocation, parent);

		CalloutComponentInfo calloutInfo = new FGCalloutComponentInfo(parent, component,
			screenLocation, relativePoint, size, viewer, v);

		createCallout(parent, calloutInfo);
	}

	private JButton getToolbarButton(FGVertex vertex, String actionName) {

		JComponent component = vertex.getComponent();
		GenericHeader header = (GenericHeader) getInstanceField("genericHeader", component);
		return getToolbarButton(header, actionName);
	}

	private JButton getToolbarButton(GenericHeader header, String actionName) {
		// get the header's 'toolBarMgr'  -  DockableToolBarManager
		Object toolBarMgr = getInstanceField("toolBarMgr", header);

		// get the toolbar manager's 'toolBarManager'  - ToolBarManager
		Object toolBarManager = getInstanceField("toolBarManager", toolBarMgr);

		// get the tool bar manager's manager's 'toolBar'
		JComponent toolbar = (JComponent) getInstanceField("toolBar", toolBarManager);
		Component[] components = toolbar.getComponents();
		for (Component c : components) {
			if (!(c instanceof JButton)) {
				continue;
			}

			String name = c.getName();
			if (actionName.equals(name)) {
				return (JButton) c;
			}
		}

		Assert.fail("Could not find action button");
		return null;
	}

	private void drawRectangleAroundMessageText() {
		int imageHeight = screen.image.getHeight(null);

		int boxSize = 60;
		int boxThickness = 3;
		int x = 1;
		int y = imageHeight - boxSize;
		int w = 250;
		int h = boxSize - boxThickness;
		Rectangle rect = new Rectangle(x, y, w, h);

		// drop shadow
		Color color = Palette.GRAY;
		screen.drawRectangle(color, rect, boxThickness);

		// box
		x -= 1;
		y -= 2;
		color = Palette.getColor("greenyellow");
		rect.x = x;
		rect.y = y;
		screen.drawRectangle(color, rect, boxThickness);
	}

	private void changeLabel(Address address) {
		AddLabelCmd cmd = new AddLabelCmd(address, "Test.Label", SourceType.USER_DEFINED);
		int id = program.startTransaction("Test");
		try {
			cmd.applyTo(program);
		}
		finally {
			program.endTransaction(id, true);
		}
	}

	private void cropRootVertexHeader() {
		FunctionGraph functionGraph = getFunctionGraph();
		FGVertex rootVertex = functionGraph.getRootVertex();
		cropVertexHeader(rootVertex);
	}

	private void cropVertexHeader(FGVertex vertex) {
		FGPrimaryViewer viewer = getPrimaryGraphViewer();
		Rectangle bounds = GraphViewerUtils.getVertexBoundsInViewSpace(viewer, vertex);

		DockableComponent dockableComponent = getDockableComponent(viewer);
		Point loc = SwingUtilities.convertPoint(viewer, bounds.getLocation(), dockableComponent);
		bounds.setLocation(loc);

		//
		// We want to keep the whole header, with a buffer space around it of about 10px
		//
		Rectangle area = new Rectangle(bounds);
		int offset = 10;
		area.x -= offset;
		area.y -= offset;
		area.width += (2 * offset);
		area.height = 50;
		screen.crop(area);
	}

	private void cropVertices(FGVertex... vertices) {

		FGPrimaryViewer viewer = getPrimaryGraphViewer();
		List<FGVertex> list = Arrays.asList(vertices);
		Rectangle bounds = GraphViewerUtils.getBoundsForVerticesInLayoutSpace(viewer, list);
		bounds = GraphViewerUtils.translateRectangleFromLayoutSpaceToViewSpace(viewer, bounds);

		DockableComponent dockableComponent = getDockableComponent(viewer);
		bounds = SwingUtilities.convertRectangle(viewer, bounds, dockableComponent);

		//
		// Put a buffer space around the area
		//
		Rectangle area = new Rectangle(bounds);
		int offset = 20;
		area.x -= offset;
		area.y -= offset;
		area.width += (2 * offset);
		area.height += (2 * offset);
		screen.crop(area);
	}

	private void cropAndKeepMessageSection() {
		int imageWidth = screen.image.getWidth(null);
		int imageHeight = screen.image.getHeight(null);

		// keep about 100 pixels of display to get the message area and a bit above
		Rectangle area = new Rectangle();
		area.x = 0;
		area.y = imageHeight - 120;
		area.width = imageWidth;
		area.height = 120;
		screen.crop(area);
	}

	private void go(String address) {
		goToAddress(address);
	}

	private void centerDisplay() {
		FunctionGraph functionGraph = getFunctionGraph();
		FGVertex v = functionGraph.getRootVertex();
		setVertexToCenterTop(v);
	}

	private void moveGraphToUpperLeftCorner(final FGVertex anchorVertex) {
		waitForBusyGraph();

		runSwing(() -> {
			FGPrimaryViewer viewer = getPrimaryGraphViewer();
			Point p = getOffsetFromUpperLeftForVertexInLayoutSpace(viewer, anchorVertex);
			moveViewerLocationWithoutAnimation(p);
		});
	}

	private void moveGraphToUpperLeftCorner() {
		waitForBusyGraph();

		final FunctionGraph functionGraph = getFunctionGraph();
		runSwing(() -> {
			FGController controller = getFunctionGraphController();
			FGView view = controller.getView();
			VisualizationViewer<FGVertex, FGEdge> viewer = view.getPrimaryGraphViewer();

			FunctionGraph graph = functionGraph;
			Collection<FGVertex> vertices = graph.getVertices();
			Rectangle layoutBounds =
				GraphViewerUtils.getBoundsForVerticesInLayoutSpace(viewer, vertices);
			Rectangle viewBounds =
				GraphViewerUtils.translateRectangleFromLayoutSpaceToViewSpace(viewer, layoutBounds);

			Point location = viewBounds.getLocation();
			Point layoutPoint = getOffsetFromUpperLeftForViewPointInLayoutSpace(viewer, location);

			moveViewerLocationWithoutAnimation(layoutPoint);
		});
	}

	private <V, E> Point getOffsetFromUpperLeftForVertexInLayoutSpace(
			VisualizationServer<V, E> viewer, V vertex) {

		//
		// We need an offset from the current vertex to the center top of the viewer
		//
		Rectangle vertexBoundsInViewSpace =
			GraphViewerUtils.getVertexBoundsInViewSpace(viewer, vertex);
		Point vertexLocationInViewSpace = vertexBoundsInViewSpace.getLocation();

		// this padding is enough to not see the edges for most operations--it can be increased
		int xWithPadding = 20;
		int yWithPadding = 20;
		Double point = new Point2D.Double(xWithPadding, yWithPadding);// upper, left

		Point vertexPointInLayoutSpace = GraphViewerUtils.translatePointFromViewSpaceToLayoutSpace(
			vertexLocationInViewSpace, viewer);
		Point upperLeftInLayoutSpace =
			GraphViewerUtils.translatePointFromViewSpaceToLayoutSpace(point, viewer);

		double offsetX = upperLeftInLayoutSpace.getX() - vertexPointInLayoutSpace.getX();
		double offsetY = upperLeftInLayoutSpace.getY() - vertexPointInLayoutSpace.getY();
		return new Point((int) offsetX, (int) offsetY);
	}

	private <V, E> Point getOffsetFromUpperLeftForViewPointInLayoutSpace(
			VisualizationServer<V, E> viewer, Point viewPoint) {

		// this padding is enough to not see the edges for most operations--it can be increased
		int xWithPadding = 50;
		int yWithPadding = 50;
		Double offsetPoint = new Point2D.Double(xWithPadding, yWithPadding);// upper, left

		Point vertexPointInLayoutSpace =
			GraphViewerUtils.translatePointFromViewSpaceToLayoutSpace(viewPoint, viewer);
		Point viewOffsetPointInLayoutSpace =
			GraphViewerUtils.translatePointFromViewSpaceToLayoutSpace(offsetPoint, viewer);

		double offsetX = viewOffsetPointInLayoutSpace.getX() - vertexPointInLayoutSpace.getX();
		double offsetY = viewOffsetPointInLayoutSpace.getY() - vertexPointInLayoutSpace.getY();
		return new Point((int) offsetX, (int) offsetY);
	}

	private void isolateProvider() {
		ComponentProvider provider = tool.getWindowManager().getComponentProvider(FGProvider.class);
		screen.moveProviderToItsOwnWindow(provider);
	}

	private void setSize(final int width, final int height) {
		this.width = width;
		this.height = height;

		final ComponentProvider provider =
			tool.getWindowManager().getComponentProvider(FGProvider.class);
		runSwing(() -> {
			Window window = tool.getWindowManager().getProviderWindow(provider);
			if (window == null) {
				throw new AssertException("Could not find window for " +
					"provider--is it showing?: " + provider.getName());
			}

			window.setSize(new Dimension(width, height));
		});
	}

	private void captureProvider() {
		screen.captureIsolatedProvider(FGProvider.class, width, height);
	}

	private void captureProviderWindow() {
		screen.captureIsolatedProviderWindow(FGProvider.class, width, height);
	}

	private void cropAndRemoveHeader(int w, int h) {
		int imageWidth = screen.image.getWidth(null);
		int x = (imageWidth / 2) - (w / 2);
		int y = 20;// down a bit from the top
		Rectangle newBounds = new Rectangle(x, y, w, h);
		screen.crop(newBounds);
	}

	private void createUndefinedFunction() {
		String address = "00401c25";
		go(address);

		DeleteFunctionCmd cmd = new DeleteFunctionCmd(getAddress(address));
		int id = program.startTransaction("Test");
		try {
			cmd.applyTo(program);
		}
		finally {
			program.endTransaction(id, true);
		}
	}

	private DialogComponentProvider showGroupTextDialog(FGVertex... vertices) {
		HashSet<FGVertex> set = new HashSet<>();
		for (FGVertex v : vertices) {
			set.add(v);
		}

		pickVertices(set);

		FGVertex aVertex = vertices[0];
		JComponent component = getComponent(aVertex);
		DockingAction action = (DockingAction) TestUtils.getInstanceField("groupAction", component);
		performAction(action, graphProvider, false);
		waitForAnimation();

		return waitForDialogComponent(MultiLineInputDialog.class);
	}

	private void pickVertices(FGVertex... vertices) {
		pickVertices(new HashSet<>(Arrays.asList(vertices)));
	}

	private JComponent getComponent(final FGVertex vertex) {
		final AtomicReference<JComponent> reference = new AtomicReference<>();
		runSwing(() -> reference.set(vertex.getComponent()));
		return reference.get();
	}

	private void setNestedLayout() {

		Object actionManager = getInstanceField("actionManager", graphProvider);
		@SuppressWarnings("unchecked")
		final MultiStateDockingAction<Class<? extends FGLayoutProvider>> action =
			(MultiStateDockingAction<Class<? extends FGLayoutProvider>>) getInstanceField(
				"layoutAction", actionManager);
		runSwing(() -> {
			List<ActionState<Class<? extends FGLayoutProvider>>> states =
				action.getAllActionStates();
			for (ActionState<Class<? extends FGLayoutProvider>> state : states) {
				Class<? extends FGLayoutProvider> layoutClass = state.getUserData();
				if (layoutClass.getSimpleName().equals("DecompilerNestedLayoutProvider")) {
					action.setCurrentActionState(state);
					return;
				}
			}

			throw new RuntimeException("Could not find layout!!");
		});
	}

	private void createGroupButtonCallout_PlayArea(final FGVertex v, final String imageName) {

		FGProvider provider = screen.getProvider(FGProvider.class);
		Window window = windowForComponent(provider.getComponent());
		final JDialog dialog = new JDialog(window);
		dialog.setModal(true);

		JPanel panel = new JPanel(new BorderLayout());
		JButton button = new JButton("Repaint");
		button.addActionListener(e -> new Thread() {
			@Override
			public void run() {
				Thread.currentThread().setName("Show Image[" + System.identityHashCode(this) + "]");

				createGroupButtonCallout(v);
				screen.saveOrDisplayImage(imageName);
			}
		}.start());

		JButton closeButton = new JButton("Close");
		closeButton.addActionListener(e -> dialog.setVisible(false));

		panel.add(button);
		panel.add(closeButton, BorderLayout.SOUTH);
		dialog.getContentPane().add(panel);

		dialog.setSize(300, 200);
		dialog.setLocation(1300, 100);
		dialog.setVisible(true);
	}

	@SuppressWarnings("rawtypes")
	private void setLayout() {
		long start = System.currentTimeMillis();
		Object actionManager = getInstanceField("actionManager", graphProvider);
		final MultiStateDockingAction<?> action =
			(MultiStateDockingAction<?>) getInstanceField("layoutAction", actionManager);

		Object minCrossState = null;
		List<?> states = action.getAllActionStates();
		for (Object state : states) {
			if (((ActionState) state).getName().indexOf("Nested Code Layout") != -1) {
				minCrossState = state;
				break;
			}
		}

		assertNotNull("Could not find min cross layout!", minCrossState);

		//@formatter:off
		invokeInstanceMethod( "setCurrentActionState", 
							  action, 
							  new Class<?>[] { ActionState.class },
							  new Object[] { minCrossState });
		//@formatter:on

		runSwing(() -> action.actionPerformed(new DefaultActionContext()));

		// wait for the threaded graph layout code
		FGController controller = getFunctionGraphController();
		waitForBusyRunManager(controller);
		waitForAnimation();
		getPrimaryGraphViewer().repaint();
		waitForSwing();

		long end = System.currentTimeMillis();
		Msg.debug(this, "relayout time: " + ((end - start) / 1000.0) + "s");
	}

//==================================================================================================
// Inner Classes
//==================================================================================================

	private class MyScreen extends GhidraScreenShotGenerator {

		public MyScreen() {
			super();
		}

		@Override
		public void loadProgram() {
			// do nothing, we will put our own copy inside the screen
		}

		@Override
		// overridden so that we use the outer class's name when finding the help topic 
		protected File getHelpTopic() {
			Class<?> clazz = FunctionGraphPluginScreenShots.class;
			String simpleName = clazz.getSimpleName();
			simpleName = simpleName.replace("ScreenShots", "");
			File helpTopicDir = getHelpTopicDir(simpleName);
			assertNotNull("Unable to find help topic for test file: " + clazz.getName(),
				helpTopicDir);
			return helpTopicDir;
		}
	}

	private class FGCalloutComponentInfo extends CalloutComponentInfo {

		private VisualizationViewer<FGVertex, FGEdge> viewer;
		private FGVertex vertex;

		FGCalloutComponentInfo(Component destinationComponent, Component component,
				Point locationOnScreen, Point relativeLocation, Dimension size,
				VisualizationViewer<FGVertex, FGEdge> viewer, FGVertex vertex) {

			super(destinationComponent, component, locationOnScreen, relativeLocation, size);
			this.viewer = viewer;
			this.vertex = vertex;
		}

		@Override
		public Point convertPointToParent(Point location) {
			// TODO: this won't work for now if the graph is scaled.   This is because there is
			//       point information that is calculated by the client of this class that does 
			//       not take into account the scaling of the graph.  This is a known issue--
			//       don't use this class when the graph is scaled.
			return location;
		}
	}
}
