content: /*****************************************************************
   *   Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you 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
   *
   *    https://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 org.apache.cayenne.modeler.graph;
  
  import java.awt.Color;
  import java.awt.Dimension;
  import java.awt.Font;
  import java.awt.Point;
  import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.awt.geom.Point2D;
  import java.awt.geom.Rectangle2D;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  
  import javax.swing.JPopupMenu;
  import javax.swing.border.LineBorder;
  import javax.swing.event.UndoableEditEvent;
  
  import org.apache.cayenne.configuration.ConfigurationNodeVisitor;
  import org.apache.cayenne.configuration.DataChannelDescriptor;
  import org.apache.cayenne.configuration.event.DataMapEvent;
  import org.apache.cayenne.configuration.event.DataMapListener;
  import org.apache.cayenne.map.Attribute;
  import org.apache.cayenne.map.DataMap;
  import org.apache.cayenne.map.Entity;
  import org.apache.cayenne.map.Relationship;
  import org.apache.cayenne.map.event.EntityEvent;
  import org.apache.cayenne.map.event.RelationshipEvent;
  import org.apache.cayenne.modeler.Application;
  import org.apache.cayenne.modeler.ProjectController;
  import org.apache.cayenne.modeler.action.ActionManager;
  import org.apache.cayenne.modeler.action.CreateAttributeAction;
  import org.apache.cayenne.modeler.action.CreateRelationshipAction;
  import org.apache.cayenne.modeler.graph.action.EntityDisplayAction;
  import org.apache.cayenne.modeler.graph.action.RemoveEntityAction;
  import org.apache.cayenne.util.XMLEncoder;
  import org.jgraph.JGraph;
  import org.jgraph.graph.AttributeMap;
  import org.jgraph.graph.DefaultCellViewFactory;
  import org.jgraph.graph.DefaultEdge;
  import org.jgraph.graph.DefaultGraphCell;
  import org.jgraph.graph.DefaultGraphModel;
  import org.jgraph.graph.GraphConstants;
  import org.jgraph.graph.GraphLayoutCache;
  import org.jgraph.graph.GraphModel;
  
  import com.jgraph.layout.JGraphFacade;
  import com.jgraph.layout.organic.JGraphOrganicLayout;
  
  /**
   * Base class for building graphs of entities
   */
  abstract class BaseGraphBuilder<E extends Entity<E, A, R>, A extends Attribute<E, A, R>, R extends Relationship<E, A, R>>
          implements GraphBuilder<E, A, R>, DataMapListener {
  
      static final Font EDGE_FONT = new Font("Verdana", Font.PLAIN, 10);
  
      /**
       * Graph
       */
      protected JGraph graph;
  
      /**
       * Domain
       */
      protected transient DataChannelDescriptor domain;
  
      /**
       * Created entity cells. Maps to entity name, since GraphBuilder can be
       * serialized
       */
      protected Map<String, DefaultGraphCell> entityCells;
  
      /**
       * Created relationship cells Maps to relationship qualified name, since
       * GraphBuilder can be serialized
       */
      protected Map<String, DefaultEdge> relCells;
  
      /**
       * Created non-isolated objects
       */
      protected List<DefaultGraphCell> createdObjects;
  
      /**
       * Current project controller
       */
      protected transient ProjectController mediator;
  
      protected transient Entity<E, A, R> selectedEntity;
  
      transient JPopupMenu popup;
  
      boolean undoEventsDisabled;
  
      @Override
      public void buildGraph(ProjectController mediator, DataChannelDescriptor domain, boolean doLayout) {
          if (graph != null) {
              // graph already built, exiting silently
              return;
          }
  
          graph = new JGraph();
          GraphModel model = new DefaultGraphModel();
          graph.setModel(model);
  
          setProjectController(mediator);
          setDataDomain(domain);
  
          GraphLayoutCache view = new GraphLayoutCache(model, new DefaultCellViewFactory());
          graph.setGraphLayoutCache(view);
  
          addMouseListeners();
  
          entityCells = new HashMap<>();
          createdObjects = new ArrayList<>();
          relCells = new HashMap<>();
  
          /*
           * an array for entities that are not connected to anyone. We add them
           * separately so that layout doesn't touch them
           */
          List<DefaultGraphCell> isolatedObjects = new ArrayList<>();
  
          /*
           * 1. Add all entities
           */
          for (DataMap map : domain.getDataMaps()) {
              DefaultGraphCell mapCell = new DefaultGraphCell();
              createdObjects.add(mapCell);
  
              for (E entity : getEntities(map)) {
                  DefaultGraphCell cell = createEntityCell(entity);
  
                  // mapCell.add(cell);
                  // cell.setParent(mapCell);
  
                  List<DefaultGraphCell> array = !isIsolated(domain, entity) ? createdObjects : isolatedObjects;
                  array.add(cell);
                  array.add((DefaultGraphCell) cell.getChildAt(0)); // port
              }
          }
  
          /*
           * 2. Add all relationships
           */
          for (DataMap map : domain.getDataMaps()) {
              for (E entity : getEntities(map)) {
                  DefaultGraphCell sourceCell = entityCells.get(entity.getName());
  
                  postProcessEntity(entity, sourceCell);
              }
          }
          view.insert(createdObjects.toArray());
  
          setLayout(doLayout);
  
          /*
           * Adding isolated objects
           * 
           * We're placing them so that they will take maximum space in left top
           * corner. The sample order is below:
           * 
           * 1 2 6 7... 3 5 8 ... 4 9... 10 ...
           */
          addIsolatedObjects(isolatedObjects);
  
          view.insert(isolatedObjects.toArray());
          graph.getModel().addUndoableEditListener(this);
      }
  
  	private void setLayout(boolean doLayout) {
  		if (doLayout) {
              JGraphFacade facade = new JGraphFacade(graph);
  
              JGraphOrganicLayout layout = new JGraphOrganicLayout();
              layout.setNodeDistributionCostFactor(5000000000000.0);
              layout.setEdgeLengthCostFactor(1000);
              layout.setEdgeCrossingCostFactor(1000000);
              layout.setOptimizeBorderLine(false);
              layout.setOptimizeEdgeDistance(false);
  
              // JGraphHierarchicalLayout layout = new JGraphHierarchicalLayout();
              // layout.setInterHierarchySpacing(150.0);
              // layout.setIntraCellSpacing(150.0);
              // layout.setInterRankCellSpacing(150.0);
  
              // JGraphSimpleLayout layout = new JGraphSimpleLayout(JGraphSimpleLayout.TYPE_TILT, 4000, 2000);
              layout.run(facade);
              // Obtain a map of the resulting attribute changes from the facade
              Map<?,?> nested = facade.createNestedMap(true, true);
  
              // Apply the results to the actual graph
              edit(nested);
          }
  	}
  
  	private void addMouseListeners() {
  		graph.addMouseListener(new MouseAdapter() {
              @SuppressWarnings("unchecked")
              @Override
              public void mouseReleased(MouseEvent e) {
                  if (e.isPopupTrigger()) {
                      Object selected = graph.getSelectionCell();
                      if (selected instanceof DefaultGraphCell) {
                          Object userObject = ((DefaultGraphCell) selected).getUserObject();
                          if (userObject instanceof EntityCellMetadata) {
                              showPopup(e.getPoint(), ((EntityCellMetadata<E,A,R>) userObject).fetchEntity());
                          }
                      }
                  }
              }
          });
  		
  		graph.addMouseWheelListener(e -> {
              // limit scale
              double scale = graph.getScale() / Math.pow(ZOOM_FACTOR, e.getWheelRotation());
              scale = Math.max(scale, 0.1);
              scale = Math.min(scale, 3);
              graph.setScale(scale);
          });
  	}
  
  	private void addIsolatedObjects(List<DefaultGraphCell> isolatedObjects) {
  		if (isolatedObjects.size() > 0) {
              int n = isolatedObjects.size() / 2; // number of isolated entities
              int x = (int) Math.ceil((Math.sqrt(1 + 8 * n) - 1) / 2); // side of
                                                                       // triangle
  
              Dimension pref = graph.getPreferredSize();
              int dx = pref.width / 2 / x; // x-distance between entities
              int dy = pref.height / 2 / x; // y-distance between entities
  
              int posX = dx / 2;
              int posY = dy / 2;
  
              int row = 0;
  
              for (int isolatedIndex = 0; isolatedIndex < isolatedObjects.size();) {
                  for (int i = 0; isolatedIndex < isolatedObjects.size() && i < x - row; i++) {
                      GraphConstants.setBounds(isolatedObjects.get(isolatedIndex).getAttributes(),
                              new Rectangle2D.Double(pref.width - posX, pref.height - (3.0 * posY) / 2, 10, 10));
                      isolatedIndex += 2; // because every 2nd object is port
                      posX += dx;
                  }
                  posX = dx / 2;
                  posY += dy / 2;
                  row++;
              }
          }
  	}
  
      protected DefaultGraphCell createEntityCell(E entity) {
          DefaultGraphCell cell = new DefaultGraphCell(getCellMetadata(entity));
  
          GraphConstants.setResize(cell.getAttributes(), true);
          GraphConstants.setBorder(cell.getAttributes(), new LineBorder(Color.BLACK));
  
          GraphConstants.setEditable(cell.getAttributes(), false);
          entityCells.put(entity.getName(), cell);
  
          cell.addPort();
          return cell;
      }
  
      @Override
      public DefaultGraphCell getEntityCell(String entityName) {
          return entityCells.get(entityName);
      }
  
      /**
       * Post (i.e. after creation on entity cell) process of the entity
       */
      protected void postProcessEntity(E entity, DefaultGraphCell cell) {
          for (R rel : entity.getRelationships()) {
              if (rel.getSourceEntity() != null && rel.getTargetEntity() != null) {
                  DefaultEdge edge = createRelationshipCell(rel);
                  if (edge != null) {
                      createdObjects.add(edge);
                  }
              }
          }
      }
  
      /**
       * Returns whether an entity is not connected to any other
       * TODO: not fine algorithm, it iterates through all entities and all rels
       */
      protected boolean isIsolated(DataChannelDescriptor domain, E entity) {
          if (entity.getRelationships().size() == 0) {
              // searching for rels that have a target="entity"
  
              for (DataMap map : domain.getDataMaps()) {
                  for (E source : getEntities(map)) {
                      if (source.getAnyRelationship(entity) != null) {
                          return false;
                      }
                  }
              }
              return true;
          }
          return false;
      }
  
      protected abstract Collection<E> getEntities(DataMap map);
  
      /**
       * Returns label for relationship on the graph, considering its "mandatory"
       * and "to-many" properties
       */
      private static <E extends Entity<E, A, R>, A extends Attribute<E, A, R>, R extends Relationship<E, A, R>>
      String getRelationshipLabel(Relationship<E, A, R> rel) {
          if (rel == null) {
              return null;
          }
  
          if (rel.isToMany()) {
              return "0..*";
          }
          return rel.isMandatory() ? "1" : "0..1";
      }
  
      /**
       * Returns metadata (user object) for this cell
       */
      protected abstract EntityCellMetadata<E, A, R> getCellMetadata(E e);
  
      protected void showPopup(Point p, Entity<E, A, R> entity) {
          selectedEntity = entity;
          if (popup == null) {
              popup = createPopupMenu();
          }
          popup.show(graph, p.x, p.y);
      }
  
      @Override
      public Entity<E, A, R> getSelectedEntity() {
          return selectedEntity;
      }
  
      /**
       * Creates popup menu
       */
      protected JPopupMenu createPopupMenu() {
          ActionManager actionManager = Application.getInstance().getActionManager();
  
          JPopupMenu menu = new JPopupMenu();
          menu.add(new EntityDisplayAction(this).buildMenu());
          menu.addSeparator();
          menu.add(new EntityDisplayAction(this, actionManager.getAction(CreateAttributeAction.class)).buildMenu());
          menu.add(new EntityDisplayAction(this, actionManager.getAction(CreateRelationshipAction.class)).buildMenu());
          menu.addSeparator();
          menu.add(new RemoveEntityAction(this));
  
          return menu;
      }
  
      /**
       * Updates specified entity on the graph
       */
      protected void updateEntityCell(E e) {
          DefaultGraphCell cell = entityCells.get(e.getName());
          if (cell != null) {
              GraphConstants.setValue(cell.getAttributes(), getCellMetadata(e));
              GraphConstants.setResize(cell.getAttributes(), true);
  
              Map<DefaultGraphCell, AttributeMap> nested = new HashMap<>();
              nested.put(cell, cell.getAttributes());
  
              edit(nested);
          }
      }
  
      protected void updateRelationshipCell(R rel) {
          if (rel.getSourceEntity() != null && rel.getTargetEntity() != null) {
              DefaultEdge edge = relCells.get(getQualifiedName(rel));
              if (edge != null) {
                  updateRelationshipLabels(edge, rel, rel.getReverseRelationship());
  
                  Map<DefaultEdge, AttributeMap> nested = new HashMap<>();
                  nested.put(edge, edge.getAttributes());
                  edit(nested);
              } else {
                  insertRelationshipCell(rel);
              }
          }
      }
  
      protected void removeEntityCell(E e) {
          final DefaultGraphCell cell = entityCells.get(e.getName());
          if (cell != null) {
              runWithUndoDisabled(() -> graph.getGraphLayoutCache().remove(new Object[] { cell }, true, true));
              entityCells.remove(e.getName());
          }
      }
  
      protected void removeRelationshipCell(R rel) {
          final DefaultEdge edge = relCells.get(getQualifiedName(rel));
          if (edge != null) {
              runWithUndoDisabled(() -> graph.getGraphLayoutCache().remove(new Object[] { edge }));
              relCells.remove(getQualifiedName(rel));
          }
      }
  
      protected DefaultEdge createRelationshipCell(R rel) {
          if (!relCells.containsKey(getQualifiedName(rel))) {
              Relationship<E, A, R> reverse = rel.getReverseRelationship();
  
              DefaultEdge edge = new DefaultEdge();
  
              // GraphConstants.setLineStyle(edge.getAttributes(),
              // GraphConstants.STYLE_ORTHOGONAL);
              // GraphConstants.setRouting(edge.getAttributes(),
              // GraphConstants.ROUTING_SIMPLE);
  
              GraphConstants.setEditable(edge.getAttributes(), false);
              GraphConstants.setLabelAlongEdge(edge.getAttributes(), true);
              GraphConstants.setSelectable(edge.getAttributes(), false);
              GraphConstants.setFont(edge.getAttributes(), EDGE_FONT);
  
              updateRelationshipLabels(edge, rel, reverse);
  
              relCells.put(getQualifiedName(rel), edge);
  
              if (reverse != null) {
                  relCells.put(getQualifiedName(reverse), edge);
              }
  
              return edge;
          }
          return null;
      }
  
      protected void insertRelationshipCell(R rel) {
          DefaultEdge edge = createRelationshipCell(rel);
          insert(edge);
      }
  
      protected void insertEntityCell(E entity) {
          DefaultGraphCell cell = createEntityCell(entity);
  
          // putting cell to a random posistion..
          GraphConstants.setBounds(cell.getAttributes(),
                  new Rectangle2D.Double(Math.random() * graph.getWidth(), Math.random() * graph.getHeight(), 10, 10));
  
          // setting graph type-specific attrs
          postProcessEntity(entity, cell);
  
          insert(cell);
      }
  
      /**
       * Updates relationship labels for specified relationship edge.
       */
      protected void updateRelationshipLabels(DefaultEdge edge, Relationship<E, A, R> rel, Relationship<E, A, R> reverse) {
          DefaultGraphCell sourceCell = entityCells.get(rel.getSourceEntity().getName());
          DefaultGraphCell targetCell = entityCells.get(rel.getTargetEntity().getName());
  
          edge.setSource(sourceCell != null ? sourceCell.getChildAt(0) : null);
          edge.setTarget(targetCell != null ? targetCell.getChildAt(0) : null);
  
          Object[] labels = { rel.getName() + " " + getRelationshipLabel(rel),
                  reverse == null ? "" : reverse.getName() + " " + getRelationshipLabel(reverse) };
          GraphConstants.setExtraLabels(edge.getAttributes(), labels);
  
          Point2D[] labelPositions = {
                  new Point2D.Double(GraphConstants.PERMILLE * (0.1 + 0.2 * Math.random()), 10),
                  new Point2D.Double(GraphConstants.PERMILLE * (0.9 - 0.2 * Math.random()), -10)
          };
          GraphConstants.setExtraLabelPositions(edge.getAttributes(), labelPositions);
      }
  
      @Override
      public JGraph getGraph() {
          return graph;
      }
  
      @Override
      public void dataMapAdded(DataMapEvent e) {
      }
  
      @Override
      public void dataMapChanged(DataMapEvent e) {
      }
  
      @Override
      public void dataMapRemoved(DataMapEvent e) {
          for (E entity : getEntities(e.getDataMap())) {
              removeEntityCell(entity);
          }
      }
  
      public void setProjectController(ProjectController mediator) {
          this.mediator = mediator;
  
          mediator.addDataMapListener(this);
      }
  
      public void setDataDomain(DataChannelDescriptor domain) {
          this.domain = domain;
      }
  
      @Override
      public DataChannelDescriptor getDataDomain() {
          return domain;
      }
  
      @Override
      public void destroy() {
          mediator.removeDataMapListener(this);
      }
  
      /**
       * Checks if entity name has changed, then changes map key
       */
      protected void remapEntity(EntityEvent e) {
          if (e.isNameChange()) {
              entityCells.put(e.getNewName(), entityCells.remove(e.getOldName()));
          }
      }
  
      /**
       * Checks if entity name has changed, then changes map key
       */
      @SuppressWarnings("unchecked")
      protected void remapRelationship(RelationshipEvent e) {
          if (e.isNameChange()) {
              relCells.put(getQualifiedName((Relationship<E, A, R>)e.getRelationship()),
                      relCells.remove(e.getEntity().getName() + "." + e.getOldName()));
          }
      }
  
      /**
       * Returns qualified name (entity name + relationship name) for a
       * relationship
       */
      static <E extends Entity<E, A, R>, A extends Attribute<E, A, R>, R extends Relationship<E, A, R>>
      String getQualifiedName(Relationship<E, A, R> rel) {
          return rel.getSourceEntity().getName() + "." + rel.getName();
      }
  
      @Override
      public void encodeAsXML(XMLEncoder encoder, ConfigurationNodeVisitor delegate) {
          encoder.start("graph")
                  .attribute("type", getType().toString())
                  .attribute("scale", String.valueOf(graph.getScale()));
  
          for (Entry<String, DefaultGraphCell> entry : entityCells.entrySet()) {
              Rectangle2D rect = graph.getCellBounds(entry.getValue());
  
              encoder.start("entity")
                      .attribute("name", entry.getKey())
                      .attribute("x", String.valueOf(Math.round(100 * rect.getX()) / 100.0))
                      .attribute("y", String.valueOf(Math.round(100 * rect.getY()) / 100.0))
                      .attribute("width", String.valueOf(rect.getWidth()))
                      .attribute("height", String.valueOf(rect.getHeight()))
                      .end();
          }
  
          encoder.end();
      }
  
      private void edit(final Map<?,?> map) {
          runWithUndoDisabled(() -> graph.getGraphLayoutCache().edit(map));
      }
  
      private void insert(final Object cell) {
          runWithUndoDisabled(() -> graph.getGraphLayoutCache().insert(cell));
      }
  
      private void runWithUndoDisabled(Runnable r) {
          undoEventsDisabled = true;
          try {
              r.run();
          } finally {
              undoEventsDisabled = false;
          }
      }
  
      @Override
      public void undoableEditHappened(UndoableEditEvent e) {
          if (!undoEventsDisabled) {
              // graph has been modified
              mediator.setDirty(true);
  
              Application.getInstance().getUndoManager().undoableEditHappened(e);
          }
      }
  }
  