/*
 * Copyright 2010-2020 Australian Signals Directorate
 *
 * 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 au.gov.asd.tac.constellation.graph.interaction.gui;

import au.gov.asd.tac.constellation.graph.Graph;
import au.gov.asd.tac.constellation.graph.GraphReadMethods;
import au.gov.asd.tac.constellation.graph.GraphWriteMethods;
import au.gov.asd.tac.constellation.graph.ReadableGraph;
import au.gov.asd.tac.constellation.graph.file.GraphDataObject;
import au.gov.asd.tac.constellation.graph.file.GraphObjectUtilities;
import au.gov.asd.tac.constellation.graph.file.SaveNotification;
import au.gov.asd.tac.constellation.graph.file.io.GraphJsonWriter;
import au.gov.asd.tac.constellation.graph.file.nebula.NebulaDataObject;
import au.gov.asd.tac.constellation.graph.file.save.AutosaveUtilities;
import au.gov.asd.tac.constellation.graph.interaction.framework.GraphVisualManagerFactory;
import au.gov.asd.tac.constellation.graph.interaction.plugins.clipboard.CopyToClipboardAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.clipboard.CutToClipboardAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.clipboard.PasteFromClipboardAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.composite.ContractAllCompositesAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.composite.ExpandAllCompositesAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.display.DrawBlazesAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.display.DrawConnectionLabelsAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.display.DrawConnectionsAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.display.DrawEdgesAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.display.DrawLinksAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.display.DrawNodeLabelsAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.display.DrawNodesAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.display.DrawTransactionsAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.display.Toggle3DAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.display.ToggleGraphVisibilityAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.draw.ToggleDrawDirectedAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.draw.ToggleSelectionModeAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.io.CloseAction;
import au.gov.asd.tac.constellation.graph.interaction.plugins.io.SaveAsAction;
import au.gov.asd.tac.constellation.graph.locking.DualGraph;
import au.gov.asd.tac.constellation.graph.monitor.GraphChangeEvent;
import au.gov.asd.tac.constellation.graph.monitor.GraphChangeListener;
import au.gov.asd.tac.constellation.graph.node.GraphNode;
import au.gov.asd.tac.constellation.graph.node.GraphNodeFactory;
import au.gov.asd.tac.constellation.graph.processing.GraphRecordStoreUtilities;
import au.gov.asd.tac.constellation.graph.processing.RecordStore;
import au.gov.asd.tac.constellation.graph.processing.RecordStoreUtilities;
import au.gov.asd.tac.constellation.graph.schema.Schema;
import au.gov.asd.tac.constellation.graph.schema.SchemaFactoryUtilities;
import au.gov.asd.tac.constellation.graph.schema.visual.attribute.objects.ConnectionMode;
import au.gov.asd.tac.constellation.graph.schema.visual.concept.VisualConcept;
import au.gov.asd.tac.constellation.graph.visual.framework.VisualGraphDefaults;
import au.gov.asd.tac.constellation.plugins.PluginException;
import au.gov.asd.tac.constellation.plugins.PluginExecution;
import au.gov.asd.tac.constellation.plugins.PluginGraphs;
import au.gov.asd.tac.constellation.plugins.PluginInteraction;
import au.gov.asd.tac.constellation.plugins.gui.PluginParametersSwingDialog;
import au.gov.asd.tac.constellation.plugins.logging.ConstellationLoggerHelper;
import au.gov.asd.tac.constellation.plugins.parameters.PluginParameter;
import au.gov.asd.tac.constellation.plugins.parameters.PluginParameters;
import au.gov.asd.tac.constellation.plugins.parameters.types.StringParameterType;
import au.gov.asd.tac.constellation.plugins.parameters.types.StringParameterValue;
import au.gov.asd.tac.constellation.plugins.templates.SimpleEditPlugin;
import au.gov.asd.tac.constellation.plugins.templates.SimplePlugin;
import au.gov.asd.tac.constellation.plugins.update.GraphUpdateController;
import au.gov.asd.tac.constellation.plugins.update.GraphUpdateManager;
import au.gov.asd.tac.constellation.plugins.update.UpdateComponent;
import au.gov.asd.tac.constellation.plugins.update.UpdateController;
import au.gov.asd.tac.constellation.preferences.ApplicationPreferenceKeys;
import au.gov.asd.tac.constellation.preferences.DeveloperPreferenceKeys;
import au.gov.asd.tac.constellation.utilities.gui.HandleIoProgress;
import au.gov.asd.tac.constellation.utilities.icon.UserInterfaceIconProvider;
import au.gov.asd.tac.constellation.utilities.memory.MemoryManager;
import au.gov.asd.tac.constellation.utilities.visual.DrawFlags;
import au.gov.asd.tac.constellation.utilities.visual.VisualManager;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ButtonGroup;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import org.netbeans.api.actions.Savable;
import org.netbeans.spi.actions.AbstractSavable;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.awt.StatusDisplayer;
import org.openide.awt.UndoRedo;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.loaders.SaveAsCapable;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
import org.openide.util.HelpCtx;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.NbPreferences;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;
import org.openide.windows.CloneableTopComponent;
import org.openide.windows.TopComponent;

/**
 * Top component which displays a JOGL canvas in which to draw a graph.
 *
 * @author algol
 * @author antares
 */
@TopComponent.Description(
        preferredID = "VisualGraphTopComponent",
        iconBase = "au/gov/asd/tac/constellation/graph/interaction/gui/resources/constellation.png",
        persistenceType = TopComponent.PERSISTENCE_NEVER
)
@TopComponent.Registration(
        mode = "editor",
        openAtStartup = false
)
@ActionID(
        category = "Window",
        id = "au.gov.asd.tac.constellation.graph.interaction.gui.VisualGraphTopComponent"
)
@ActionReferences({
    @ActionReference(path = "Menu/Window", position = 100)
})
@NbBundle.Messages({
    "CTL_VisualGraphAction=Visual Graph",
    "CTL_VisualGraphTopComponent=Visual Graph",
    "HINT_VisualGraphTopComponent=Visual Graph"
})
public final class VisualGraphTopComponent extends CloneableTopComponent implements GraphChangeListener, UndoRedo.Provider {

    public static final String NEW_GRAPH_NAME_PARAMETER_ID = PluginParameter.buildId(VisualGraphTopComponent.class, "graph_name");

    private static final Map<String, BufferedImage> ICON_CACHE = new HashMap<>();
    private static final Icon HIDDEN_ICON = UserInterfaceIconProvider.HIDDEN.buildIcon(16, Color.BLACK);
    private static final Icon VISIBLE_ICON = UserInterfaceIconProvider.VISIBLE.buildIcon(16, Color.BLACK);
    private static final Icon MODE_2D_ICON = UserInterfaceIconProvider.MODE_2D.buildIcon(16);
    private static final Icon MODE_3D_ICON = UserInterfaceIconProvider.MODE_3D.buildIcon(16);
    private static final Icon DRAWING_MODE_ICON = UserInterfaceIconProvider.DRAW_MODE.buildIcon(16);
    private static final Icon SELECT_MODE_ICON = UserInterfaceIconProvider.SELECT_MODE.buildIcon(16);
    private static final Icon DIRECTED_ICON = UserInterfaceIconProvider.DIRECTED.buildIcon(16);
    private static final Icon UNDIRECTED_ICON = UserInterfaceIconProvider.UNDIRECTED.buildIcon(16);

    private final GraphVisualManagerFactory graphVisualManagerFactory;
    private final VisualManager visualManager;
    private final InstanceContent content;
    private final Graph graph;
    private MySaveAs saveAs = null;
    private MySavable savable = null;
    private final GraphNode graphNode;

    /**
     * The countBase is the value of the counter at the most recent save when
     * the graph became unmodified).
     */
    private long graphModificationCountBase;
    private long graphModificationCount;

    // Sidebar actions.
    private ContractAllCompositesAction contractCompositesAction;
    private ExpandAllCompositesAction expandCompositesAction;
    private DrawNodesAction drawNodesAction;
    private DrawConnectionsAction drawConnectionsAction;
    private DrawNodeLabelsAction drawNodeLabelsAction;
    private DrawConnectionLabelsAction drawConnectionLabelsAction;
    private DrawBlazesAction drawBlazesAction;
    private DrawLinksAction drawLinksAction;
    private DrawEdgesAction drawEdgesAction;
    private DrawTransactionsAction drawTransactionsAction;
    private Toggle3DAction display3dAction;
    private ToggleSelectionModeAction toggleSelectModeAction;
    private ToggleDrawDirectedAction toggleDrawDirectedAction;
    private ToggleGraphVisibilityAction toggleGraphVisibilityAction;

    private final UpdateController<GraphReadMethods> updateController = new UpdateController<>();
    private final GraphUpdateController graphUpdateController = new GraphUpdateController(updateController);
    private final GraphUpdateManager graphUpdateManager = new GraphUpdateManager(graphUpdateController, 2);

    private static final String SAVE = "Save";
    private static final String DISCARD = "Discard";
    private static final String CANCEL = "Cancel";

    /**
     * Initialise the TopComponent state.
     */
    private void init() {
        displayPanel.add(visualManager.getVisualComponent(), BorderLayout.CENTER);

        DropTargetAdapter dta = new DropTargetAdapter() {
            @Override
            public void dragEnter(DropTargetDragEvent dtde) {
                dtde.acceptDrag(DnDConstants.ACTION_COPY);
            }

            @Override
            public void drop(DropTargetDropEvent dtde) {
                final Transferable transferable = dtde.getTransferable();
                if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
                    try {
                        dtde.acceptDrop(DnDConstants.ACTION_COPY);
                        @SuppressWarnings("unchecked") //files will be list of file which extends from object type
                        final List<File> files = (List<File>) transferable.getTransferData(DataFlavor.javaFileListFlavor);
                        for (final File file : files) {
                            try (final InputStream in = file.getName().endsWith(".gz") ? new GZIPInputStream(new FileInputStream(file)) : new FileInputStream(file)) {
                                final RecordStore recordStore = RecordStoreUtilities.fromTsv(in);
                                PluginExecution.withPlugin(new SimpleEditPlugin("Import record file") {
                                    @Override
                                    protected void edit(final GraphWriteMethods graph, final PluginInteraction interaction, final PluginParameters parameters) throws InterruptedException {
                                        GraphRecordStoreUtilities.addRecordStoreToGraph(graph, recordStore, false, true, null);
                                    }
                                }).executeLater(graph);
                            }
                        }
                    } catch (UnsupportedFlavorException | IOException ex) {
                        Exceptions.printStackTrace(ex);
                        dtde.rejectDrop();
                    }
                } else {
                    dtde.rejectDrop();
                }
            }
        };

        displayPanel.setDropTarget(new DropTarget(displayPanel, DnDConstants.ACTION_COPY, dta, true));

        content.add(getActionMap());
        savable = new MySavable();
        saveAs = new MySaveAs();
        content.add(saveAs);
        content.add(graphNode.getDataObject());
        content.add(graph);
        content.add(graphNode);
        associateLookup(new AbstractLookup(content));
        setActivatedNodes(new Node[]{
            graphNode
        });

        getActionMap().put("cut-to-clipboard", new CutToClipboardAction(graphNode));
        getActionMap().put("copy-to-clipboard", new CopyToClipboardAction(graphNode));
        getActionMap().put("paste-from-clipboard", new PasteFromClipboardAction(graphNode));

        // The actions below are per-graph.
        // NetBeans creates a single instance of an action and uses it globally, which doesn't do us any good,
        // because we want to have different toggle states on different graphs, for instance.
        // Therefore, we'll ignore NetBeans and create our own per-graph action instances.
        expandCompositesAction = new ExpandAllCompositesAction(graphNode);
        contractCompositesAction = new ContractAllCompositesAction(graphNode);
        drawNodesAction = new DrawNodesAction(graphNode);
        drawConnectionsAction = new DrawConnectionsAction(graphNode);
        drawNodeLabelsAction = new DrawNodeLabelsAction(graphNode);
        drawConnectionLabelsAction = new DrawConnectionLabelsAction(graphNode);
        drawBlazesAction = new DrawBlazesAction(graphNode);
        final ButtonGroup drawButtonGroup = new ButtonGroup();
        drawLinksAction = new DrawLinksAction(graphNode, drawButtonGroup);
        drawEdgesAction = new DrawEdgesAction(graphNode, drawButtonGroup);
        drawTransactionsAction = new DrawTransactionsAction(graphNode, drawButtonGroup);
        final ButtonGroup displayModeButtonGroup = new ButtonGroup();
        display3dAction = new Toggle3DAction(graphNode, displayModeButtonGroup);
        final ButtonGroup addModeButtonGroup = new ButtonGroup();
        toggleSelectModeAction = new ToggleSelectionModeAction(graphNode, addModeButtonGroup);
        final ButtonGroup directedModeButtonGroup = new ButtonGroup();
        toggleDrawDirectedAction = new ToggleDrawDirectedAction(graphNode, directedModeButtonGroup);
        toggleGraphVisibilityAction = new ToggleGraphVisibilityAction(graphNode);

        final JToolBar sidebar = new JToolBar(SwingConstants.VERTICAL);
        sidebar.setFloatable(false);
        sidebar.setRollover(true);
        sidebar.add(display3dAction.getToolbarPresenter());
        sidebar.addSeparator();

        sidebar.add(drawLinksAction.getToolbarPresenter());
        sidebar.add(drawEdgesAction.getToolbarPresenter());
        sidebar.add(drawTransactionsAction.getToolbarPresenter());
        sidebar.addSeparator();

        sidebar.add(drawNodesAction.getToolbarPresenter());
        sidebar.add(drawConnectionsAction.getToolbarPresenter());
        sidebar.add(drawNodeLabelsAction.getToolbarPresenter());
        sidebar.add(drawConnectionLabelsAction.getToolbarPresenter());
        sidebar.add(drawBlazesAction.getToolbarPresenter());

        sidebar.addSeparator();
        sidebar.add(toggleGraphVisibilityAction.getToolbarPresenter());

        sidebar.addSeparator();
        sidebar.add(expandCompositesAction.getToolbarPresenter());
        sidebar.add(contractCompositesAction.getToolbarPresenter());

        sidebar.addSeparator();
        sidebar.add(toggleSelectModeAction.getToolbarPresenter());
        sidebar.add(toggleDrawDirectedAction.getToolbarPresenter());

        // Add this so the side bar isn't too long.
        // Without this, the side bar has a height that extends past the icons and stops other TopComponents
        // from growing past it.
        sidebar.setMinimumSize(new Dimension(0, 0));

        // Set the modification counters to whatever they are now.
        // This causes any setup changes to be ignored.
        final ReadableGraph rg = graph.getReadableGraph();
        try {
            graphModificationCountBase = rg.getGlobalModificationCounter();
            graphModificationCount = graphModificationCountBase;
        } finally {
            rg.release();
        }

        // Initial update so that the sidebar actions are updated to match the graph.
        visualUpdate();

        this.add(sidebar, BorderLayout.WEST);

        // Listen to graph changes so we can update our modified flag. This will determine
        // whether or not we need to enable saving of the graph.
        graph.addGraphChangeListener(this);

        final InputMap keys = getInputMap(VisualGraphTopComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        final KeyStroke key = KeyStroke.getKeyStroke("Control W");
        final CloseAction ca = new CloseAction(graphNode);
        keys.put(key, ca);

        // Set the icon.
        final Schema schema = graph.getSchema();
        final Image image = getBufferedImageForSchema(schema, false);

        VisualGraphTopComponent.this.setIcon(getNebulaIcon(image));

        final UpdateComponent<GraphReadMethods> visualUpdateComponent = new UpdateComponent<GraphReadMethods>() {

            @Override
            protected boolean update(GraphReadMethods updateState) {
                visualUpdate();
                return true;
            }
        };
        visualUpdateComponent.dependOn(graphUpdateController.createAttributeUpdateComponent(VisualConcept.GraphAttribute.DRAW_FLAGS));
        visualUpdateComponent.dependOn(graphUpdateController.createAttributeUpdateComponent(VisualConcept.GraphAttribute.VISIBLE_ABOVE_THRESHOLD));
        visualUpdateComponent.dependOn(graphUpdateController.createAttributeUpdateComponent(VisualConcept.GraphAttribute.DISPLAY_MODE_3D));
        visualUpdateComponent.dependOn(graphUpdateController.createAttributeUpdateComponent(VisualConcept.GraphAttribute.DRAWING_MODE));
        visualUpdateComponent.dependOn(graphUpdateController.createAttributeUpdateComponent(VisualConcept.GraphAttribute.DRAW_DIRECTED_TRANSACTIONS));
        visualUpdateComponent.dependOn(graphUpdateController.createAttributeUpdateComponent(VisualConcept.GraphAttribute.CONNECTION_MODE));
        graphUpdateManager.setManaged(true);
    }

    @Override
    public UndoRedo getUndoRedo() {
        return graphNode.getUndoRedoManager();
    }

    /**
     * Construct a new TopComponent with an empty graph.
     */
    public VisualGraphTopComponent() {
        initComponents();
        setName(NbBundle.getMessage(VisualGraphTopComponent.class, "CTL_VisualGraphTopComponent"));
        setToolTipText(NbBundle.getMessage(VisualGraphTopComponent.class, "HINT_VisualGraphTopComponent"));

        final GraphDataObject gdo = GraphObjectUtilities.createMemoryDataObject("graph", true);
        this.graph = new DualGraph(null);

        graphVisualManagerFactory = Lookup.getDefault().lookup(GraphVisualManagerFactory.class);
        visualManager = graphVisualManagerFactory.constructVisualManager(graph);
        visualManager.startProcessing();
        graphNode = new GraphNode(graph, gdo, this, visualManager);
        content = new InstanceContent();

        init();
        MemoryManager.newObject(VisualGraphTopComponent.class);
    }

    /**
     * Construct a new TopComponent.
     *
     * @param gdo The DataObject that this graph is associated with.
     * @param graph The graph.
     */
    public VisualGraphTopComponent(final GraphDataObject gdo, final Graph graph) {
        initComponents();
        setName(gdo.getName());
        setToolTipText(gdo.getToolTipText());

        this.graph = graph;
        graphVisualManagerFactory = Lookup.getDefault().lookup(GraphVisualManagerFactory.class);
        visualManager = graphVisualManagerFactory.constructVisualManager(graph);
        visualManager.startProcessing();

        Schema schema = graph.getSchema();
        if (schema instanceof GraphNodeFactory) {
            graphNode = ((GraphNodeFactory) schema).createGraphNode(graph, gdo, this, visualManager);
        } else {
            graphNode = new GraphNode(graph, gdo, this, visualManager);
        }

        content = new InstanceContent();

        init();
        MemoryManager.newObject(VisualGraphTopComponent.class);
    }

    @Override
    public void requestActive() {
        super.requestActive();
        visualManager.getVisualComponent().requestFocusInWindow();
    }

    /**
     * This is required to display the name of the DataObject in the "Save?"
     * dialog box.
     *
     * @return The display name of the DataObject.
     */
    @Override
    public String getDisplayName() {
        // need to check that savable is registered since it will be unregistered with each 'save' call regardless of its outcome
        // therefore we need to re-register, and this method is always called as part of the save command.
        savable.resetRegistry();
        return graphNode.getDataObject().getName();
    }

    /**
     * Return the GraphNode belonging to this TopComponent.
     *
     * @return The GraphNode belonging to this TopComponent.
     */
    public GraphNode getGraphNode() {
        return graphNode;
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        displayPanel = new javax.swing.JPanel();

        setLayout(new java.awt.BorderLayout());

        displayPanel.setBackground(new java.awt.Color(0, 0, 0));
        displayPanel.setLayout(new java.awt.BorderLayout());
        add(displayPanel, java.awt.BorderLayout.CENTER);
    }// </editor-fold>//GEN-END:initComponents
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JPanel displayPanel;
    // End of variables declaration//GEN-END:variables

    @Override
    public void componentOpened() {
        super.componentOpened();

        // Try to free up any unused memory
        final boolean forceGarbageCollectOnOpen = NbPreferences.forModule(ApplicationPreferenceKeys.class).getBoolean(DeveloperPreferenceKeys.FORCE_GC_ON_OPEN, DeveloperPreferenceKeys.FORCE_GC_ON_OPEN_DEFAULT);
        if (forceGarbageCollectOnOpen) {
            System.gc();
        }

        graphUpdateManager.setManaged(true);
    }

    @Override
    public void componentClosed() {
        super.componentClosed();

        setActivatedNodes(new Node[]{});

        graph.removeGraphChangeListener(this);

        visualManager.stopProcessing();
        displayPanel.remove(visualManager.getVisualComponent());
        content.remove(saveAs);
        content.remove(graphNode.getDataObject());
        content.remove(graph);
        content.remove(graphNode);
        content.remove(getActionMap());

        // Get rid of any autosaved files on a user-requested close.
        // Note that canClose() will catch unsaved graphs, so at this point the graph has either been saved or discarded by the user.
        AutosaveUtilities.deleteAutosave(graph.getId());

        graphNode.destroy();

        visualManager.destroy();

        StatusDisplayer.getDefault().setStatusText("Closed " + graphNode.getDataObject().getName());

        // Try to free up any unused memory
        final boolean forceGarbageCollectOnClose = NbPreferences.forModule(ApplicationPreferenceKeys.class).getBoolean(DeveloperPreferenceKeys.FORCE_GC_ON_CLOSE, DeveloperPreferenceKeys.FORCE_GC_ON_CLOSE_DEFAULT);
        if (forceGarbageCollectOnClose) {
            System.gc();
        }

        graphUpdateManager.setManaged(false);
    }

    @Override
    public void graphChanged(final GraphChangeEvent evt) {
        long modificationCount;

        ReadableGraph rg = graph.getReadableGraph();
        try {
            modificationCount = rg.getGlobalModificationCounter();
        } finally {
            rg.release();
        }

        if (modificationCount != graphModificationCount && graphModificationCount == graphModificationCountBase) {
            graphModificationCount = modificationCount;
            savable.setModified(true);
            SwingUtilities.invokeLater(() -> {
                setHtmlDisplayName(String.format("<html><font color=\"#0000ff\"><b>%s</b></font></html>", getDisplayName()));
                requestVisible();
            });
        }
    }

    @Override
    public void finalize() throws Throwable {
        try {
            MemoryManager.finalizeObject(VisualGraphTopComponent.class);
        } finally {
            super.finalize();
        }
    }

    private void visualUpdate() {

        final ReadableGraph rg = graph.getReadableGraph();
        try {
            final int drawFlagsAttribute = VisualConcept.GraphAttribute.DRAW_FLAGS.get(rg);
            final int visibleAboveThresholdAttribute = VisualConcept.GraphAttribute.VISIBLE_ABOVE_THRESHOLD.get(rg);
            final int displayModeIs3DAttribute = VisualConcept.GraphAttribute.DISPLAY_MODE_3D.get(rg);
            final int drawingModeAttribute = VisualConcept.GraphAttribute.DRAWING_MODE.get(rg);
            final int drawDirectedAttribute = VisualConcept.GraphAttribute.DRAW_DIRECTED_TRANSACTIONS.get(rg);
            final int connectionModeAttribute = VisualConcept.GraphAttribute.CONNECTION_MODE.get(rg);

            // Read relevant visual attributes from the graph and update the sidebar.
            final DrawFlags drawFlags;
            final ConnectionMode connectionMode;
            final boolean visibleAboveThreshold;
            final boolean isDisplay3D;
            final boolean isDrawingMode;
            final boolean isDrawingDirectedTransactions;
            drawFlags = drawFlagsAttribute != Graph.NOT_FOUND ? rg.getObjectValue(drawFlagsAttribute, 0) : VisualGraphDefaults.DEFAULT_DRAW_FLAGS;
            visibleAboveThreshold = visibleAboveThresholdAttribute != Graph.NOT_FOUND ? rg.getBooleanValue(visibleAboveThresholdAttribute, 0) : VisualGraphDefaults.DEFAULT_GRAPH_VISIBILITY;
            isDisplay3D = displayModeIs3DAttribute != Graph.NOT_FOUND ? rg.getBooleanValue(displayModeIs3DAttribute, 0) : VisualGraphDefaults.DEFAULT_DISPLAY_MODE_3D;
            isDrawingMode = drawingModeAttribute != Graph.NOT_FOUND ? rg.getBooleanValue(drawingModeAttribute, 0) : VisualGraphDefaults.DEFAULT_DRAWING_MODE;
            isDrawingDirectedTransactions = drawDirectedAttribute != Graph.NOT_FOUND ? rg.getBooleanValue(drawDirectedAttribute, 0) : VisualGraphDefaults.DEFAULT_DRAWING_DIRECTED_TRANSACTIONS;
            connectionMode = connectionModeAttribute != Graph.NOT_FOUND ? rg.getObjectValue(connectionModeAttribute, 0) : VisualGraphDefaults.DEFAULT_CONNECTION_MODE;

            drawNodesAction.putValue(Action.SELECTED_KEY, drawFlags.drawNodes());
            drawConnectionsAction.putValue(Action.SELECTED_KEY, drawFlags.drawConnections());
            drawNodeLabelsAction.putValue(Action.SELECTED_KEY, drawFlags.drawNodeLabels());
            drawConnectionLabelsAction.putValue(Action.SELECTED_KEY, drawFlags.drawConnectionLabels());
            drawBlazesAction.putValue(Action.SELECTED_KEY, drawFlags.drawBlazes());
            display3dAction.putValue(Action.SELECTED_KEY, isDisplay3D);
            display3dAction.putValue(Action.SMALL_ICON, isDisplay3D ? MODE_3D_ICON : MODE_2D_ICON);
            toggleGraphVisibilityAction.putValue(Action.SELECTED_KEY, visibleAboveThreshold);
            toggleGraphVisibilityAction.putValue(Action.SMALL_ICON, visibleAboveThreshold ? VISIBLE_ICON : HIDDEN_ICON);
            toggleSelectModeAction.putValue(Action.SELECTED_KEY, isDrawingMode);
            toggleSelectModeAction.putValue(Action.SMALL_ICON, isDrawingMode ? DRAWING_MODE_ICON : SELECT_MODE_ICON);
            toggleDrawDirectedAction.putValue(Action.SELECTED_KEY, isDrawingDirectedTransactions);
            toggleDrawDirectedAction.putValue(Action.SMALL_ICON, isDrawingDirectedTransactions ? DIRECTED_ICON : UNDIRECTED_ICON);
            toggleDrawDirectedAction.setEnabled(isDrawingMode);

            switch (connectionMode) {
                case LINK:
                    drawLinksAction.putValue(Action.SELECTED_KEY, true);
                    break;
                case EDGE:
                    drawEdgesAction.putValue(Action.SELECTED_KEY, true);
                    break;
                case TRANSACTION:
                    drawTransactionsAction.putValue(Action.SELECTED_KEY, true);
                    break;
                default:
                    throw new IllegalStateException("Unknown ConnectionMode: " + connectionMode);
            }
        } finally {
            rg.release();
        }
    }

    @Override
    public boolean canClose() {
        if (savable.isModified()) {
            final String message = String.format("Graph %s is modified. Save?", getDisplayName());
            final Object[] options = new Object[]{
                SAVE, DISCARD, CANCEL
            };
            final NotifyDescriptor d = new NotifyDescriptor(message, "Close", NotifyDescriptor.YES_NO_CANCEL_OPTION, NotifyDescriptor.QUESTION_MESSAGE, options, "Save");
            final Object o = DialogDisplayer.getDefault().notify(d);

            if (o.equals(DISCARD)) {
                savable.setModified(false);
            } else if (o.equals(SAVE)){
                try {
                    savable.handleSave();
                    if (!savable.isSaved()){
                        return false;
                    }
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
            } else {
                return false;
            }
        }

        return true;
    }

    @Override
    public Action[] getActions() {
        // Add new actions above the default actions.
        final ArrayList<Action> actionList = new ArrayList<>();

        // An action that closes the topcomponent without saving the (possibly modified) graph.
        final Action discard = new AbstractAction(DISCARD) {
            @Override
            public void actionPerformed(final ActionEvent e) {
                savable.setModified(false);
                close();
            }
        };
        actionList.add(discard);

        // If this graph is in a nebula, add some nebula-related actions.
        final NebulaDataObject nebula = getGraphNode().getDataObject().getNebulaDataObject();
        if (nebula != null) {
            // Discard the nebula without saving.
            final Action discardNebula = new AbstractAction("Discard nebula") {
                @Override
                public void actionPerformed(final ActionEvent e) {
                    TopComponent.getRegistry().getOpened().stream().filter(tc -> (tc instanceof VisualGraphTopComponent)).map(tc -> (VisualGraphTopComponent) tc).forEach(vtc -> {
                        final NebulaDataObject ndo = vtc.getGraphNode().getDataObject().getNebulaDataObject();
                        if (nebula.equalsPath(ndo)) {
                            vtc.savable.setModified(false);
                            vtc.close();
                        }
                    });
                }
            };
            actionList.add(discardNebula);

            // Are there any graphs in this nebula (if it exists) that need saving?
            final List<Savable> savables = getNebulaSavables(nebula);
            if (!savables.isEmpty()) {
                // There's at least one graph in this nebula that needs saving...
                final Action saveNebula = new AbstractAction("Save nebula") {
                    @Override
                    public void actionPerformed(final ActionEvent e) {
                        try {
                            for (final Savable s : savables) {
                                s.save();
                            }
                        } catch (final IOException ex) {
                            Exceptions.printStackTrace(ex);
                        }
                    }
                };
                actionList.add(saveNebula);
            } else {
                // No graphs in this nebula need saving, so offer to close the nebula.
                final Action closeNebula = new AbstractAction("Close nebula") {
                    @Override
                    public void actionPerformed(final ActionEvent e) {
                        TopComponent.getRegistry().getOpened().stream().filter(tc -> (tc instanceof VisualGraphTopComponent)).map(tc -> (VisualGraphTopComponent) tc).forEach(vtc -> {
                            final NebulaDataObject ndo = vtc.getGraphNode().getDataObject().getNebulaDataObject();
                            if (nebula.equalsPath(ndo)) {
                                vtc.close();
                            }
                        });
                    }
                };
                actionList.add(closeNebula);
            }
        }

        // An action that renames the topcomponent without saving the (possibly modified) graph.
        final Action rename = new AbstractAction("Rename") {
            @Override
            public void actionPerformed(final ActionEvent e) {
                final PluginParameters parameters = new PluginParameters();
                final PluginParameter<StringParameterValue> newGraphNameParameter = StringParameterType.build(NEW_GRAPH_NAME_PARAMETER_ID);
                newGraphNameParameter.setName("New Graph Name");
                newGraphNameParameter.setStringValue(graphNode.getDisplayName());
                newGraphNameParameter.storeRecentValue();
                parameters.addParameter(newGraphNameParameter);

                final PluginParametersSwingDialog dialog = new PluginParametersSwingDialog("Rename Graph", parameters);
                dialog.showAndWait();

                if (PluginParametersSwingDialog.OK.equals(dialog.getResult())) {
                    final String newGraphName = parameters.getStringValue(NEW_GRAPH_NAME_PARAMETER_ID);

                    if (!newGraphName.isEmpty()) {
                        try {
                            // set the graph object name so the name is retained when you Save As
                            graphNode.getDataObject().rename(newGraphName);

                            // set the other graph name properties
                            graphNode.setName(newGraphName);
                            graphNode.setDisplayName(newGraphName);

                            // set the top component
                            setName(newGraphName);
                            setDisplayName(newGraphName);
                            setHtmlDisplayName(newGraphName); // this changes the text on the tab
                        } catch (IOException ex) {
                            throw new RuntimeException(String.format("The name %s already exists.", newGraphName), ex);
                        }
                        savable.setModified(true);
                    }
                }
            }
        };
        actionList.add(rename);

        // Add the default actions.
        for (final Action action : super.getActions()) {
            actionList.add(action);
        }

        return actionList.toArray(new Action[actionList.size()]);
    }

    public boolean forceClose() {
        savable.setModified(false);
        return close();
    }

    public void saveGraph() throws IOException {
        savable.handleSave();
    }

    /**
     * A List of Savable instances in this nebula.
     *
     * @param nebula The nebula that this graph is in.
     *
     * @return A List of Savable instances in this nebula.
     */
    private static List<Savable> getNebulaSavables(final NebulaDataObject nebula) {
        final List<Savable> savableList = new ArrayList<>();
        final Collection<? extends Savable> savables = Savable.REGISTRY.lookupAll(Savable.class);
        savables.stream().filter(s -> (s instanceof MySavable)).forEach(s -> {
            final NebulaDataObject otherNDO = ((MySavable) s).tc().getGraphNode().getDataObject().getNebulaDataObject();
            if (nebula.equalsPath(otherNDO)) {
                savableList.add(s);
            }
        });

        return savableList;
    }

    @Override
    public HelpCtx getHelpCtx() {
        return new HelpCtx("au.gov.asd.tac.constellation.visual.about");
    }

    private Image getNebulaIcon(final Image image) {
        final Color nebulaColor = getGraphNode().getDataObject().getNebulaColor();
        if (nebulaColor == null) {
            return image;
        }

        final int w = 6;
        final int h = 16;

        final BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        final Graphics2D g = bi.createGraphics();
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, w, h);
        g.setColor(nebulaColor);
        g.fillRect(1, 1, w - 2, h - 2);
        g.dispose();

        return ImageUtilities.mergeImages(bi, image, w, 0);
    }

    /**
     * Cache a BufferedImage per schema so that it can be retrieved to avoid an
     * icon rebuild
     *
     * @param schema The Schema representing the graph
     * @return A BufferedImage for the schema
     */
    private BufferedImage getBufferedImageForSchema(Schema schema, final boolean isModified) {
        if (schema == null) {
            schema = SchemaFactoryUtilities.getDefaultSchemaFactory().createSchema();
        }

        if (isModified) {
            if (!ICON_CACHE.containsKey(schema.getFactory().getModifiedIcon().getName())) {
                ICON_CACHE.put(schema.getFactory().getModifiedIcon().getName(),
                        schema.getFactory().getModifiedIcon().buildBufferedImage(16));
            }
            return ICON_CACHE.get(schema.getFactory().getModifiedIcon().getName());
        } else {
            if (!ICON_CACHE.containsKey(schema.getFactory().getIcon().getName())) {
                ICON_CACHE.put(schema.getFactory().getIcon().getName(),
                        schema.getFactory().getIcon().buildBufferedImage(16));
            }
            return ICON_CACHE.get(schema.getFactory().getIcon().getName());
        }
    }

    /**
     * A custom Savable.
     */
    private class MySavable extends AbstractSavable implements Icon {

        private boolean isModified;
        private boolean isSaved = false;

        /**
         * Construct a new MySavable instance.
         */
        MySavable() {
            isModified = false;
        }

        /**
         * Set this savable as modified/unmodified.
         * <p>
         * The Savable will be registered/unregistered with the SavableRegistry
         * as required.
         *
         * @param modified Modification flag.
         */
        public void setModified(final boolean modified) {
            if (modified) {
                if (!isModified) {
                    content.add(this);
                    register();
                }
            } else {
                if (isModified) {
                    content.remove(this);
                    unregister();
                }
            }

            Schema schema = graph.getSchema();
            final Image image = getBufferedImageForSchema(schema, modified);

            VisualGraphTopComponent.this.setIcon(getNebulaIcon(image));

            isModified = modified;
        }

        /**
         * Has this Savable been modified?
         *
         * @return True if modified, false if not.
         */
        public boolean isModified() {
            return isModified;
        }

        public boolean isSaved() {
            return isSaved;
        }
        /**
         * register the instance if in modified state
         */
        public void resetRegistry() {
            if (this.isModified()) {
                register();
            }
        }

        @Override
        protected String findDisplayName() {
            return getDisplayName();
        }

        /**
         * Save the graph.
         * <p>
         * The graph file is not overwritten. This would be dangerous, since a
         * large graph may take some time to write, and an interruption would
         * leave a corrupted file. Instead, the graph is written to a new file;
         * when the write is complete, the old file is deleted and the new file
         * is renamed.
         *
         * @throws IOException When I/O errors happen.
         */
        @Override
        protected void handleSave() throws IOException {
            final GraphDataObject gdo = graphNode.getDataObject();

            if (gdo.isInMemory()) {
                // We don't want to do a save if this is an in-memory DataObject.
                // Instead, we'll do the "Save As..." action and let it naturally do the right thing.
                // We have to make sure that this TopComponent is the the active one, because SaveAsAction
                // just saves the current graph. If we don't do this and we have multiple graphs, the same
                // graph will get saved each time.
                requestActive();

                SaveAsAction action = new SaveAsAction();
                action.actionPerformed(null);
                isSaved = action.isSaved();
                return;
            }

            final String name = gdo.getName();

            // Create a new file and write to it.
            final String tmpnam = String.format("%s_tmp%08x", name, gdo.hashCode());
            final GraphDataObject freshGdo = (GraphDataObject) gdo.createFromTemplate(gdo.getFolder(), tmpnam);
            final BackgroundWriter writer = new BackgroundWriter(name, freshGdo, true);
            writer.execute();
        }

        /**
         * Return the parent VisualTopComponent.
         *
         * @return The parent VisualTopComponent.
         */
        VisualGraphTopComponent tc() {
            return VisualGraphTopComponent.this;
        }

        @Override
        public boolean equals(final Object obj) {
            if (obj instanceof MySavable) {
                final MySavable m = (MySavable) obj;
                return tc() == m.tc();
            }

            return false;
        }

        @Override
        public int hashCode() {
            return tc().hashCode();
        }

        @Override
        public void paintIcon(final Component c, final Graphics g, final int x, final int y) {
            final Schema schema = graph.getSchema();
            final Icon icon = ImageUtilities.image2Icon(getBufferedImageForSchema(schema, false));
            icon.paintIcon(c, g, x, y);
        }

        @Override
        public int getIconWidth() {
            final Schema schema = graph.getSchema();
            final Icon icon = ImageUtilities.image2Icon(getBufferedImageForSchema(schema, false));
            return icon.getIconWidth();
        }

        @Override
        public int getIconHeight() {
            final Schema schema = graph.getSchema();
            final Icon icon = ImageUtilities.image2Icon(getBufferedImageForSchema(schema, false));
            return icon.getIconHeight();
        }
    }

    private class MySaveAs implements SaveAsCapable {

        @Override
        public void saveAs(final FileObject folder, String name) throws IOException {
            StatusDisplayer.getDefault().setStatusText("Save as " + folder.getPath() + "(" + name + ")");

            // The Save As dialog box has already asked if we want to overwrite an existing file,
            // so just go ahead and delete it if it exists.
            final FileObject existing = folder.getFileObject(name);
            if (existing != null) {
                existing.delete();
            }

            final String ext = GraphDataObject.FILE_EXTENSION;
            if (name.endsWith(ext)) {
                name = name.substring(0, name.length() - ext.length());
            }

            final File newFile = new File(folder.getPath(), name + ext);
            final FileObject fo = FileUtil.createData(newFile);
            final GraphDataObject freshGdo = (GraphDataObject) DataObject.find(fo);
//            final GraphDataObject freshGdo = (GraphDataObject)DataObject.find(newFile);
//            final GraphDataObject freshGdo = (GraphDataObject)gdo.createFromTemplate(DataFolder.findFolder(folder), name);
            final BackgroundWriter writer = new BackgroundWriter(name, freshGdo, false);
            writer.execute();
        }
    }

    /**
     * Write a graph to a file in a background thread so the EDT doesn't freeze.
     */
    private class BackgroundWriter extends SwingWorker<Void, Object> {

        private final String name;
        private final GraphDataObject freshGdo;
        private final boolean deleteOldGdo;
        private boolean cancelled;
        private GraphReadMethods copy;

        /**
         * Construct a new BackgroundWriter.
         *
         * @param name The name of the file to write.
         * @param freshGdo The current GraphDataObject will be replaced by this
         * GDO in the Lookup if the write succeeds.
         * @param deleteOldGdo If true, delete the file represented by the old
         * GDO.
         */
        BackgroundWriter(final String name, final GraphDataObject freshGdo, final boolean deleteOldGdo) {
            this.name = name;
            this.freshGdo = freshGdo;
            this.deleteOldGdo = deleteOldGdo;
            cancelled = false;

            makeBusy(true);
        }

        @Override
        protected Void doInBackground() throws Exception {
            final ReadableGraph rg = graph.getReadableGraph();
            try {
                copy = rg.copy();
            } finally {
                rg.release();
            }
            try {
                try (OutputStream out = new BufferedOutputStream(freshGdo.getPrimaryFile().getOutputStream())) {
                    // Write the graph.
                    cancelled = new GraphJsonWriter().writeGraphToZip(copy, out, new HandleIoProgress("Writing..."));
                }
                SaveNotification.saved(freshGdo.getPrimaryFile().getPath());
            } catch (Exception ex) {
                Exceptions.printStackTrace(ex);
            }

            return null;
        }

        @Override
        protected void done() {
            try {
                if (cancelled) {
                    freshGdo.delete();
                } else {
                    GraphDataObject gdo = graphNode.getDataObject();
                    freshGdo.setNebulaDataObject(gdo.getNebulaDataObject());
                    freshGdo.setNebulaColor(gdo.getNebulaColor());

                    // Delete the old DataObject and remove the old GDO from the lookup.
                    if (deleteOldGdo) {
//                        System.out.printf("@VTC Delete %s\n", FileUtil.getFileDisplayName(gdo.getPrimaryFile()));
//                        gdo.getPrimaryFile().delete();
                        gdo.delete();
//                        System.out.printf("@VTX Exists %s\n", FileUtil.toFile(gdo.getPrimaryFile()).exists());
                    }
                    content.remove(gdo);

                    // Rename the new file and add the new GDO to the lookup.
//                    System.out.printf("@VTC Rename %s -> %s\n", FileUtil.getFileDisplayName(freshGdo.getPrimaryFile()), name);
                    freshGdo.rename(name);
                    gdo = freshGdo;
                    content.add(gdo);

                    graphNode.setDataObject(gdo);

                    setToolTipText(gdo.getToolTipText());

                    // Reset the modification data.
                    setHtmlDisplayName(getDisplayName());
                    graphModificationCountBase = copy.getGlobalModificationCounter();
                    graphModificationCount = graphModificationCountBase;
                    savable.setModified(false);
                }
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            }

            PluginExecution.withPlugin(new SimplePlugin("Write Graph File") {
                @Override
                protected void execute(PluginGraphs graphs, PluginInteraction interaction, PluginParameters parameters) throws InterruptedException, PluginException {
                    ConstellationLoggerHelper.exportPropertyBuilder(
                            this,
                            GraphRecordStoreUtilities.getVertices(copy, false, false, false).getAll(GraphRecordStoreUtilities.SOURCE + VisualConcept.VertexAttribute.LABEL),
                            new File(freshGdo.getPrimaryFile().getPath()),
                            ConstellationLoggerHelper.SUCCESS
                    );
                }
            }).executeLater(null);

            makeBusy(false);
        }
    }
}