/*
Copyright 2011-2016 Google Inc. All Rights Reserved.

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 com.google.security.zynamics.zylib.gui.zygraph.helpers;

import com.google.common.base.Preconditions;
import com.google.security.zynamics.zylib.gui.zygraph.functions.IteratorFunctions;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.ZyLabelContent;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.ZyLineContent;
import com.google.security.zynamics.zylib.types.common.ICollectionFilter;
import com.google.security.zynamics.zylib.types.common.IFilteredItemCallback;
import com.google.security.zynamics.zylib.types.common.IIterableCollection;
import com.google.security.zynamics.zylib.types.common.IterationMode;
import com.google.security.zynamics.zylib.types.graphs.GraphAlgorithms;
import com.google.security.zynamics.zylib.types.graphs.IGraphNode;
import com.google.security.zynamics.zylib.types.lists.FilledList;
import com.google.security.zynamics.zylib.types.lists.IFilledList;
import com.google.security.zynamics.zylib.yfileswrap.gui.zygraph.AbstractZyGraph;
import com.google.security.zynamics.zylib.yfileswrap.gui.zygraph.nodes.ZyGraphNode;
import com.google.security.zynamics.zylib.yfileswrap.gui.zygraph.realizers.IZyNodeRealizer;

import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * This class provides a number of helper functions that are useful when working with graphs.
 */
public class GraphHelpers {
  public static <NodeType> boolean any(final IIterableGraph<NodeType> graph,
      final INodeFilter<NodeType> filter) {
    final ArrayList<Object> lameHack = new ArrayList<Object>();

    graph.iterate(new INodeCallback<NodeType>() {

      @Override
      public IterationMode next(final NodeType item) {
        if (filter.qualifies(item)) {
          lameHack.add(item);

          return IterationMode.STOP;
        }

        return IterationMode.CONTINUE;
      }

    });

    return lameHack.size() != 0;
  }

  /**
   * Determines whether all children of a node are deselected.
   *
   * @param <NodeType> The type of the node and its children.
   *
   * @param node The node in question.
   *
   * @return True, if all children of the node are deselected. False, otherwise.
   */
  public static <
      NodeType extends IGraphNode<NodeType> & ISelectableNode> boolean areAllChildrenDeselected(
      final NodeType node) {
    final INodeFilter<NodeType> filter = StandardFilters.getSelectedFilter();

    return GraphAlgorithms.collectChildren(node, filter).size() == 0;
  }

  /**
   * Determines whether all parents of a node are deselected.
   *
   * @param <NodeType> The type of the node and its parents.
   *
   * @param node The node in question.
   *
   * @return True, if all parents of the node are deselected. False, otherwise.
   */
  public static <
      NodeType extends IGraphNode<NodeType> & ISelectableNode> boolean areAllParentsDeselected(
      final NodeType node) {
    final INodeFilter<NodeType> filter = StandardFilters.getSelectedFilter();

    return GraphAlgorithms.collectParents(node, filter).size() == 0;
  }

  /**
   * Calculates the bounding box of a list of nodes.
   *
   * @param <NodeType> The type of the nodes in the list.
   *
   * @param nodes The list of nodes.
   *
   * @return The bounding box of the list of nodes.
   */
  public static <NodeType extends IViewableNode> Rectangle2D calculateBoundingBox(
      final Collection<NodeType> nodes) {
    Preconditions.checkNotNull(nodes, "Node list argument can't be null");
    Preconditions.checkArgument(nodes.size() != 0, "Node list argument can't be empty");

    final Rectangle2D box = new Rectangle2D.Double();

    for (final NodeType node : nodes) {
      node.calcUnionRect(box);
    }

    return box;
  }

  /**
   * Expands the current selection down. This means that all children of the currently selected
   * nodes are selected too.
   *
   * @param <NodeType> The node type of all nodes in the graph.
   *
   * @param graph The graph where the current selection is expanded.
   */
  public static <NodeType extends IGraphNode<NodeType> & ISelectableNode> void expandSelectionDown(
      final ISelectableGraph<NodeType> graph) {
    Preconditions.checkNotNull(graph, "Error: Graph argument can't be null");
    final ArrayList<NodeType> toSelect = new ArrayList<NodeType>();
    final INodeFilter<NodeType> deselectedFilter = StandardFilters.getDeselectedFilter();

    // Collect all the nodes that must be selected
    graph.iterateSelected(new INodeCallback<NodeType>() {
      @Override
      public IterationMode next(final NodeType node) {
        toSelect.addAll(GraphAlgorithms.collectChildren(node, deselectedFilter));

        return IterationMode.CONTINUE;
      }
    });

    graph.selectNodes(toSelect, true);
  }

  /**
   * Expands the current selection up. This means that all parents of the currently selected nodes
   * are selected too.
   *
   * @param <NodeType> Type of the nodes in the graph.
   *
   * @param graph The graph in question.
   */
  public static <NodeType extends IGraphNode<NodeType> & ISelectableNode> void expandSelectionUp(
      final ISelectableGraph<NodeType> graph) {
    Preconditions.checkNotNull(graph, "Error: Graph argument can't be null");
    final ArrayList<NodeType> toSelect = new ArrayList<NodeType>();
    final INodeFilter<NodeType> deselectedFilter = StandardFilters.getDeselectedFilter();

    graph.iterateSelected(new INodeCallback<NodeType>() {
      @Override
      public IterationMode next(final NodeType node) {
        toSelect.addAll(GraphAlgorithms.collectParents(node, deselectedFilter));

        return IterationMode.CONTINUE;
      }

    });

    graph.selectNodes(toSelect, true);
  }

  /**
   * Returns a list of nodes that match the given filter.
   *
   * @param <NodeType> Type of the nodes in the graph.
   *
   * @param graph The graph that provides the nodes.
   * @param filter The filter that selects the nodes.
   *
   * @return The list of nodes that passes the filter.
   */
  public static <NodeType> List<NodeType> filter(
      final IIterableCollection<INodeCallback<NodeType>> graph,
      final ICollectionFilter<NodeType> filter) {
    final List<NodeType> items = new ArrayList<NodeType>();

    graph.iterate(new INodeCallback<NodeType>() {
      @Override
      public IterationMode next(final NodeType node) {
        if (filter.qualifies(node)) {
          items.add(node);
        }

        return IterationMode.CONTINUE;
      }
    });

    return items;
  }

  public static <EdgeType> List<EdgeType> getEdges(final IEdgeIterableGraph<EdgeType> graph) {
    final INodeFilter<EdgeType> trueFilter = StandardFilters.getTrueFilter();

    return getEdges(graph, trueFilter);
  }

  public static <EdgeType> List<EdgeType> getEdges(final IEdgeIterableGraph<EdgeType> graph,
      final INodeFilter<EdgeType> filter) {
    Preconditions.checkNotNull(graph, "Error: Graph argument can't be null");
    Preconditions.checkNotNull(filter, "Error: Filter argument can't be null");
    final ArrayList<EdgeType> nodes = new ArrayList<EdgeType>();

    graph.iterateEdges(new IEdgeCallback<EdgeType>() {
      @Override
      public IterationMode nextEdge(final EdgeType node) {
        if (filter.qualifies(node)) {
          nodes.add(node);
        }

        return IterationMode.CONTINUE;
      }
    });

    return nodes;
  }

  public static <NodeType extends ISelectableNode> List<NodeType> getInvisibleNodes(
      final IViewableGraph<NodeType> graph) {
    final ArrayList<NodeType> nodes = new ArrayList<NodeType>();

    graph.iterateInvisible(new INodeCallback<NodeType>() {
      @Override
      public IterationMode next(final NodeType node) {
        nodes.add(node);
        return IterationMode.CONTINUE;
      }
    });

    return nodes;
  }

  /**
   * Returns all nodes in the graph.
   *
   * @param <NodeType> Type of the nodes in the graph.
   *
   * @param graph The graph in question.
   *
   * @return A list of all nodes in the graph.
   */
  public static <NodeType> List<NodeType> getNodes(final IIterableGraph<NodeType> graph) {
    final INodeFilter<NodeType> trueFilter = StandardFilters.getTrueFilter();

    return getNodes(graph, trueFilter);
  }

  /**
   * Returns all nodes in the graph that pass a filter check.
   *
   * @param <NodeType> Type of the nodes in the graph.
   *
   * @param graph The graph in question.
   *
   * @return A list of all nodes in the graph that pass the filter check.
   */
  public static <NodeType> List<NodeType> getNodes(final IIterableGraph<NodeType> graph,
      final INodeFilter<NodeType> filter) {
    Preconditions.checkNotNull(graph, "Error: Graph argument can't be null");
    Preconditions.checkNotNull(filter, "Error: Filter argument can't be null");

    final ArrayList<NodeType> nodes = new ArrayList<NodeType>();

    graph.iterate(new INodeCallback<NodeType>() {
      @Override
      public IterationMode next(final NodeType node) {
        if (filter.qualifies(node)) {
          nodes.add(node);
        }

        return IterationMode.CONTINUE;
      }
    });

    return nodes;
  }

  /**
   * Returns the predecessors of the selected nodes of a graph.
   *
   * @param <NodeType> The type of the nodes in the graph.
   *
   * @param graph The graph in question.
   *
   * @return A collection of all predecessor nodes of the selected graph nodes.
   */
  public static <
      NodeType extends IGraphNode<NodeType> & ISelectableNode> Collection<NodeType> getPredecessorsOfSelection(
      final ISelectableGraph<NodeType> graph) {
    return GraphAlgorithms.getPredecessors(getSelectedNodes(graph));
  }

  public static <NodeType extends ZyGraphNode<?>> String getSelectedContent(
      final AbstractZyGraph<NodeType, ?> graph) {
    final StringBuilder selection = new StringBuilder();

    IteratorFunctions.iterateSelected(graph, new INodeCallback<NodeType>() {
      @Override
      public IterationMode next(final NodeType node) {
        final IZyNodeRealizer realizer = node.getRealizer();

        final ZyLabelContent content = realizer.getNodeContent();

        for (final ZyLineContent line : content) {
          selection.append(line.getText());
          selection.append("\n");
        }

        selection.append("\n");

        return IterationMode.CONTINUE;
      }
    });

    return selection.toString();
  }

  /**
   * Creates a list of all selected nodes of the graph.
   *
   * @param <NodeType> The type of the nodes in the graph.
   *
   * @param graph The graph in question.
   *
   * @return All selected nodes of the graph.
   */
  @Deprecated // Use m_graph.getSelectedNodes
  public static <NodeType extends ISelectableNode> IFilledList<NodeType> getSelectedNodes(
      final ISelectableGraph<NodeType> graph) {
    Preconditions.checkNotNull(graph, "Error: Graph argument can't be null");
    final IFilledList<NodeType> nodes = new FilledList<NodeType>();

    graph.iterateSelected(new INodeCallback<NodeType>() {
      @Override
      public IterationMode next(final NodeType node) {
        nodes.add(node);
        return IterationMode.CONTINUE;
      }
    });

    return nodes;
  }

  public static <
      NodeType extends IGraphNode<NodeType> & ISelectableNode> Collection<NodeType> getSuccessors(
      final NodeType node) {
    final List<NodeType> nodes = new ArrayList<NodeType>();

    nodes.add(node);

    return GraphAlgorithms.getSuccessors(nodes);
  }

  /**
   * Returns the successors of the selected nodes of a graph.
   *
   * @param <NodeType> The type of the nodes in the graph.
   *
   * @param graph The graph in question.
   *
   * @return A collection of all successor nodes of the selected graph nodes.
   */
  public static <
      NodeType extends IGraphNode<NodeType> & ISelectableNode> Collection<NodeType> getSuccessorsOfSelection(
      final ISelectableGraph<NodeType> graph) {
    return GraphAlgorithms.getSuccessors(getSelectedNodes(graph));
  }

  /**
   * Creates a list of all unselected nodes of the graph.
   *
   * @param <NodeType> The type of the nodes in the graph.
   *
   * @param graph The graph in question.
   *
   * @return All selected nodes of the graph.
   */
  public static <NodeType extends ISelectableNode> List<NodeType> getUnselectedNodes(
      final IIterableGraph<NodeType> graph) {
    Preconditions.checkNotNull(graph, "Error: Graph argument can't be null");

    final ArrayList<NodeType> nodes = new ArrayList<NodeType>();

    graph.iterate(new INodeCallback<NodeType>() {
      @Override
      public IterationMode next(final NodeType node) {
        if (!node.isSelected()) {
          nodes.add(node);
        }

        return IterationMode.CONTINUE;
      }
    });

    return nodes;
  }

  /**
   * Creates a list of all visible nodes of the graph.
   *
   * @param <NodeType> The type of the nodes in the graph.
   *
   * @param graph The graph in question.
   *
   * @return All visible nodes of the graph.
   */
  public static <NodeType extends ISelectableNode> Set<NodeType> getVisibleNodes(
      final IViewableGraph<NodeType> graph) {
    final Set<NodeType> nodes = new HashSet<NodeType>();

    graph.iterateVisible(new INodeCallback<NodeType>() {
      @Override
      public IterationMode next(final NodeType node) {
        nodes.add(node);
        return IterationMode.CONTINUE;
      }
    });

    return nodes;
  }

  public static <ItemType,
      CollectionType extends IIterableCollection<INodeCallback<ItemType>>> void iterate(
      final CollectionType collection, final IFilteredItemCallback<ItemType> callback) {
    collection.iterate(new INodeCallback<ItemType>() {
      @Override
      public IterationMode next(final ItemType node) {
        if (callback.qualifies(node)) {
          return callback.next(node);
        }

        return IterationMode.CONTINUE;
      }
    });
  }

  /**
   * Selects all predecessors of a node.
   *
   * @param <NodeType> The type of the nodes in the graph.
   *
   * @param graph The graph in question.
   */
  public static <NodeType extends IGraphNode<NodeType> & ISelectableNode> void selectPredecessors(
      final ISelectableGraph<NodeType> graph, final NodeType node) {
    graph.selectNodes(GraphAlgorithms.getPredecessors(node), true);
  }

  /**
   * Selects all predecessors of the selected nodes.
   *
   * @param <NodeType> The type of the nodes in the graph.
   *
   * @param graph The graph in question.
   */
  public static <
      NodeType extends IGraphNode<NodeType> & ISelectableNode> void selectPredecessorsOfSelection(
      final ISelectableGraph<NodeType> graph) {
    graph.selectNodes(getPredecessorsOfSelection(graph), true);
  }

  public static <NodeType extends IGraphNode<NodeType> & ISelectableNode,
      GraphType extends ISelectableGraph<NodeType>> void selectSuccessors(final GraphType graph,
      final NodeType node) {
    graph.selectNodes(GraphAlgorithms.getSuccessors(node), true);
  }

  /**
   * Selects all successors of the selected nodes.
   *
   * @param <NodeType> The type of the nodes in the graph.
   *
   * @param graph The graph in question.
   */
  public static <
      NodeType extends IGraphNode<NodeType> & ISelectableNode> void selectSuccessorsOfSelection(
      final ISelectableGraph<NodeType> graph) {
    graph.selectNodes(getSuccessorsOfSelection(graph), true);
  }

  /**
   * Shrinks the current selection down. This means that all selected nodes without incoming edges
   * are unselected.
   *
   * @param <NodeType> The type of the nodes in the graph.
   *
   * @param graph The graph in question.
   */
  public static <NodeType extends IGraphNode<NodeType> & ISelectableNode> void shrinkSelectionDown(
      final ISelectableGraph<NodeType> graph) {
    final ArrayList<NodeType> deselect = new ArrayList<NodeType>();

    graph.iterateSelected(new INodeCallback<NodeType>() {
      @Override
      public IterationMode next(final NodeType node) {
        // Nodes without incoming edges into the subgraph formed by the selected nodes
        // are unselected. Those are just the nodes which have no selected parents.

        if (areAllParentsDeselected(node)) {
          if (!deselect.contains(node)) {
            deselect.add(node);
          }
        }

        return IterationMode.CONTINUE;
      }
    });

    graph.selectNodes(deselect, false);
  }

  /**
   * Shrinks the current selection up. This means that all selected nodes without outgoing edges are
   * unselected.
   *
   * @param <NodeType> The type of the nodes in the graph.
   *
   * @param graph The graph in question.
   */
  public static <NodeType extends IGraphNode<NodeType> & ISelectableNode> void shrinkSelectionUp(
      final ISelectableGraph<NodeType> graph) {
    final ArrayList<NodeType> deselect = new ArrayList<NodeType>();

    graph.iterateSelected(new INodeCallback<NodeType>() {
      @Override
      public IterationMode next(final NodeType node) {
        // Nodes without outgoing edges into the subgraph formed by the selected nodes
        // are unselected. Those are just the nodes which have no selected children.

        if (areAllChildrenDeselected(node)) {
          if (!deselect.contains(node)) {
            deselect.add(node);
          }
        }

        return IterationMode.CONTINUE;
      }
    });

    graph.selectNodes(deselect, false);
  }

  /**
   * Zooms so far into the graph that all selected nodes are displayed as big as possible.
   *
   * @param <NodeType> The type of the nodes in the graph.
   * @param <GraphType> The type of the graph.
   *
   * @param graph The graph in question.
   */
  public static <NodeType extends ISelectableNode,
      GraphType extends ISelectableGraph<NodeType> & IZoomableGraph<NodeType>> void zoomToSelected(
      final GraphType graph) {
    final List<NodeType> selectedNodes = getSelectedNodes(graph);

    if (selectedNodes.size() == 0) {
      throw new IllegalStateException("Error: There are no selected nodes");
    }

    graph.zoomToNodes(selectedNodes);
  }
}
