/*
 Violet - A program for editing UML diagrams.

 Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com)

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package com.horstmann.violet.framework;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Point2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JPanel;
import javax.swing.JOptionPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.ResourceBundle;
import java.util.Set;

/**
 * A panel to draw a graph
 */
public class GraphPanel extends JPanel
{
   /**
    * Constructs a graph.
    * @param aToolBar the tool bar with the node and edge tools
    */
   public GraphPanel(ToolBar aToolBar)
   {
      grid = new Grid();
      gridSize = GRID;
      grid.setGrid((int) gridSize, (int) gridSize);
      zoom = 1;
      toolBar = aToolBar;
      setBackground(Color.WHITE);

      selectedItems = new HashSet();

      addMouseListener(new MouseAdapter()
      {
         public void mousePressed(MouseEvent event)
         {
            requestFocus();
            final Point2D mousePoint = new Point2D.Double(event.getX() / zoom,
                  event.getY() / zoom);
            boolean isCtrl = (event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
            Node n = graph.findNode(mousePoint);
            Edge e = graph.findEdge(mousePoint);
            Object tool = toolBar.getSelectedTool();
            if (event.getClickCount() > 1
                  || (event.getModifiers() & InputEvent.BUTTON1_MASK) == 0)
            // double/right-click
            {
               if (e != null)
               {
                  setSelectedItem(e);
                  editSelected();
               }
               else if (n != null)
               {
                  setSelectedItem(n);
                  editSelected();
               }
               else
               {
                  toolBar.showPopup(GraphPanel.this, mousePoint,
                        new ActionListener()
                        {
                           public void actionPerformed(ActionEvent event)
                           {
                              Object tool = toolBar.getSelectedTool();
                              if (tool instanceof Node)
                              {
                                 Node prototype = (Node) tool;
                                 Node newNode = (Node) prototype.clone();
                                 boolean added = graph.add(newNode, mousePoint);
                                 if (added)
                                 {
                                    setModified(true);
                                    setSelectedItem(newNode);
                                 }
                              }
                           }
                        });
               }
            }
            else if (tool == null) // select
            {
               if (e != null)
               {
                  setSelectedItem(e);
               }
               else if (n != null)
               {
                  if (isCtrl)
                     addSelectedItem(n);
                  else if (!selectedItems.contains(n))
                     setSelectedItem(n);
                  dragMode = DRAG_MOVE;
               }
               else
               {
                  if (!isCtrl)
                     clearSelection();
                  dragMode = DRAG_LASSO;
               }
            }
            else if (tool instanceof Node)
            {
               Node prototype = (Node) tool;
               Node newNode = (Node) prototype.clone();
               boolean added = graph.add(newNode, mousePoint);
               if (added)
               {
                  setModified(true);
                  setSelectedItem(newNode);
                  dragMode = DRAG_MOVE;
               }
               else if (n != null)
               {
                  if (isCtrl)
                     addSelectedItem(n);
                  else if (!selectedItems.contains(n))
                     setSelectedItem(n);
                  dragMode = DRAG_MOVE;
               }
            }
            else if (tool instanceof Edge)
            {
               if (n != null) dragMode = DRAG_RUBBERBAND;
            }

            lastMousePoint = mousePoint;
            mouseDownPoint = mousePoint;
            repaint();
         }

         public void mouseReleased(MouseEvent event)
         {
            Point2D mousePoint = new Point2D.Double(event.getX() / zoom,
                  event.getY() / zoom);
            Object tool = toolBar.getSelectedTool();
            if (dragMode == DRAG_RUBBERBAND)
            {
               Edge prototype = (Edge) tool;
               Edge newEdge = (Edge) prototype.clone();
               if (mousePoint.distance(mouseDownPoint) > CONNECT_THRESHOLD
                     && graph.connect(newEdge, mouseDownPoint, mousePoint))
               {
                  setModified(true);
                  setSelectedItem(newEdge);
               }
            }
            else if (dragMode == DRAG_MOVE)
            {
               graph.layout();
               setModified(true);
            }
            dragMode = DRAG_NONE;

            revalidate();
            repaint();
         }
      });

      addMouseMotionListener(new MouseMotionAdapter()
      {
         public void mouseDragged(MouseEvent event)
         {
            Point2D mousePoint = new Point2D.Double(event.getX() / zoom,
                  event.getY() / zoom);
            boolean isCtrl = (event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;

            if (dragMode == DRAG_MOVE && lastSelected instanceof Node)
            {
               Node lastNode = (Node) lastSelected;
               Rectangle2D bounds = lastNode.getBounds();
               double dx = mousePoint.getX() - lastMousePoint.getX();
               double dy = mousePoint.getY() - lastMousePoint.getY();

               // we don't want to drag nodes into negative coordinates
               // particularly with multiple selection, we might never be
               // able to get them back.
               Iterator iter = selectedItems.iterator();
               while (iter.hasNext())
               {
                  Object selected = iter.next();
                  if (selected instanceof Node)
                  {
                     Node n = (Node) selected;
                     bounds.add(n.getBounds());
                  }
               }
               dx = Math.max(dx, -bounds.getX());
               dy = Math.max(dy, -bounds.getY());

               iter = selectedItems.iterator();
               while (iter.hasNext())
               {
                  Object selected = iter.next();
                  if (selected instanceof Node)
                  {
                     Node n = (Node) selected;
                     n.translate(dx, dy);
                  }
               }
               // we don't want continuous layout any more because of multiple selection
               // graph.layout();
            }
            else if (dragMode == DRAG_LASSO)
            {
               double x1 = mouseDownPoint.getX();
               double y1 = mouseDownPoint.getY();
               double x2 = mousePoint.getX();
               double y2 = mousePoint.getY();
               Rectangle2D.Double lasso = new Rectangle2D.Double(Math.min(x1, x2),
                     Math.min(y1, y2), Math.abs(x1 - x2) , Math.abs(y1 - y2));
               Iterator iter = graph.getNodes().iterator();
               while (iter.hasNext())
               {
                  Node n = (Node) iter.next();
                  Rectangle2D bounds = n.getBounds();
                  if (!isCtrl && !lasso.contains(n.getBounds()))
                  {
                     removeSelectedItem(n);
                  }
                  else if (lasso.contains(n.getBounds()))
                  {
                     addSelectedItem(n);
                  }
               }
            }

            lastMousePoint = mousePoint;
            repaint();
         }
      });
   }

   /**
    * Edits the properties of the selected graph element.
    */
   public void editSelected()
   {
   	  if (EditorFrame.editProperties == true) {
	  Object edited = lastSelected;
      if (lastSelected == null)
      {
         if (selectedItems.size() == 1)
            edited = selectedItems.iterator().next();
         else
            return;
      }

      PropertySheet sheet = new PropertySheet(edited, this);
      sheet.addChangeListener(new ChangeListener()
      {
         public void stateChanged(ChangeEvent event)
         {
            graph.layout();
            repaint();
         }
      });
      JOptionPane.showInternalMessageDialog(this, sheet,
            ResourceBundle.getBundle("com.horstmann.violet.framework.EditorStrings").getString("dialog.properties"),
            JOptionPane.QUESTION_MESSAGE);
      setModified(true);
      }
   }

   /**
    * Removes the selected nodes or edges.
    */
   public void removeSelected()
   {
      Iterator iter = selectedItems.iterator();
      while (iter.hasNext())
      {
         Object selected = iter.next();
         if (selected instanceof Node)
         {
            graph.removeNode((Node) selected);
         }
         else if (selected instanceof Edge)
         {
            graph.removeEdge((Edge) selected);
         }
      }
      if (selectedItems.size() > 0) setModified(true);
      repaint();
   }

   /**
    * Set the graph in the panel
    * @param aGraph the graph to be displayed and edited
    */
   public void setGraph(Graph aGraph)
   {
      graph = aGraph;
      setModified(false);
      revalidate();
      repaint();
   }

   public void paintComponent(Graphics g)
   {
      super.paintComponent(g);
      Graphics2D g2 = (Graphics2D) g;
      g2.scale(zoom, zoom);
      Rectangle2D bounds = getBounds();
      Rectangle2D graphBounds = graph.getBounds(g2);
      if (!hideGrid) grid.draw(g2, new Rectangle2D.Double(0, 0,
            Math.max(bounds.getMaxX() / zoom, graphBounds.getMaxX()),
            Math.max(bounds.getMaxY() / zoom, graphBounds.getMaxY())));
      graph.draw(g2, grid);

      Iterator iter = selectedItems.iterator();
      Set toBeRemoved = new HashSet();
      while (iter.hasNext())
      {
         Object selected = iter.next();

         if (!graph.getNodes().contains(selected)
               && !graph.getEdges().contains(selected))
         {
            toBeRemoved.add(selected);
         }
         else if (selected instanceof Node)
         {
            Rectangle2D grabberBounds = ((Node) selected).getBounds();
            drawGrabber(g2, grabberBounds.getMinX(), grabberBounds.getMinY());
            drawGrabber(g2, grabberBounds.getMinX(), grabberBounds.getMaxY());
            drawGrabber(g2, grabberBounds.getMaxX(), grabberBounds.getMinY());
            drawGrabber(g2, grabberBounds.getMaxX(), grabberBounds.getMaxY());
         }
         else if (selected instanceof Edge)
         {
            Line2D line = ((Edge) selected).getConnectionPoints();
            drawGrabber(g2, line.getX1(), line.getY1());
            drawGrabber(g2, line.getX2(), line.getY2());
         }
      }

      iter = toBeRemoved.iterator();
      while (iter.hasNext())
         removeSelectedItem(iter.next());

      if (dragMode == DRAG_RUBBERBAND)
      {
         Color oldColor = g2.getColor();
         g2.setColor(PURPLE);
         g2.draw(new Line2D.Double(mouseDownPoint, lastMousePoint));
         g2.setColor(oldColor);
      }
      else if (dragMode == DRAG_LASSO)
      {
         Color oldColor = g2.getColor();
         g2.setColor(PURPLE);
         double x1 = mouseDownPoint.getX();
         double y1 = mouseDownPoint.getY();
         double x2 = lastMousePoint.getX();
         double y2 = lastMousePoint.getY();
         Rectangle2D.Double lasso = new Rectangle2D.Double(Math.min(x1, x2),
               Math.min(y1, y2), Math.abs(x1 - x2) , Math.abs(y1 - y2));
         g2.draw(lasso);
         g2.setColor(oldColor);
      }
   }

   /**
    * Draws a single "grabber", a filled square
    * @param g2 the graphics context
    * @param x the x coordinate of the center of the grabber
    * @param y the y coordinate of the center of the grabber
    */
   public static void drawGrabber(Graphics2D g2, double x, double y)
   {
      final int SIZE = 5;
      Color oldColor = g2.getColor();
      g2.setColor(PURPLE);
      g2.fill(new Rectangle2D.Double(x - SIZE / 2, y - SIZE / 2, SIZE, SIZE));
      g2.setColor(oldColor);
   }

   public Dimension getPreferredSize()
   {
      Rectangle2D bounds = graph.getBounds((Graphics2D) getGraphics());
      return new Dimension((int) (zoom * bounds.getMaxX()),
            (int) (zoom * bounds.getMaxY()));
   }

   /**
    * Changes the zoom of this panel. The zoom is 1 by default and is multiplied
    * by sqrt(2) for each positive stem or divided by sqrt(2) for each negative
    * step.
    * @param steps the number of steps by which to change the zoom. A positive
    * value zooms in, a negative value zooms out.
    */
   public void changeZoom(int steps)
   {
      final double FACTOR = Math.sqrt(2);
      for (int i = 1; i <= steps; i++)
         zoom *= FACTOR;
      for (int i = 1; i <= -steps; i++)
         zoom /= FACTOR;
      revalidate();
      repaint();
   }

   /**
    * Changes the grid size of this panel. The zoom is 10 by default and is
    * multiplied by sqrt(2) for each positive stem or divided by sqrt(2) for
    * each negative step.
    * @param steps the number of steps by which to change the zoom. A positive
    * value zooms in, a negative value zooms out.
    */
   public void changeGridSize(int steps)
   {
      final double FACTOR = Math.sqrt(2);
      for (int i = 1; i <= steps; i++)
         gridSize *= FACTOR;
      for (int i = 1; i <= -steps; i++)
         gridSize /= FACTOR;
      grid.setGrid((int) gridSize, (int) gridSize);
      graph.layout();
      repaint();
   }

   public void selectNext(int n)
   {
      ArrayList selectables = new ArrayList();
      selectables.addAll(graph.getNodes());
      selectables.addAll(graph.getEdges());
      if (selectables.size() == 0) return;
      java.util.Collections.sort(selectables, new java.util.Comparator()
      {
         public int compare(Object obj1, Object obj2)
         {
            double x1;
            double y1;
            if (obj1 instanceof Node)
            {
               Rectangle2D bounds = ((Node) obj1).getBounds();
               x1 = bounds.getX();
               y1 = bounds.getY();
            }
            else
            {
               Point2D start = ((Edge) obj1).getConnectionPoints().getP1();
               x1 = start.getX();
               y1 = start.getY();
            }
            double x2;
            double y2;
            if (obj2 instanceof Node)
            {
               Rectangle2D bounds = ((Node) obj2).getBounds();
               x2 = bounds.getX();
               y2 = bounds.getY();
            }
            else
            {
               Point2D start = ((Edge) obj2).getConnectionPoints().getP1();
               x2 = start.getX();
               y2 = start.getY();
            }
            if (y1 < y2) return -1;
            if (y1 > y2) return 1;
            if (x1 < x2) return -1;
            if (x1 > x2) return 1;
            return 0;
         }
      });
      int index;
      if (lastSelected == null) index = 0;
      else index = selectables.indexOf(lastSelected) + n;
      while (index < 0)
         index += selectables.size();
      index %= selectables.size();
      setSelectedItem(selectables.get(index));
      repaint();
   }

   /**
    * Checks whether this graph has been modified since it was last saved.
    * @return true if the graph has been modified
    */
   public boolean isModified()
   {
      return modified;
   }

   /**
    * Sets or resets the modified flag for this graph
    * @param newValue true to indicate that the graph has been modified
    */
   public void setModified(boolean newValue)
   {
      modified = newValue;

      if (frame == null)
      {
         Component parentComponent = this;
         do
         {
            parentComponent = parentComponent.getParent();
         }
         while (parentComponent != null && !(parentComponent instanceof GraphFrame));
         if (parentComponent != null) frame = (GraphFrame) parentComponent;
      }
      if (frame != null)
      {
         String title = frame.getFileName();
         if (title != null)
         {
            if (modified)
            {
               if (!frame.getTitle().endsWith("*")) frame.setTitle(title + "*");
            }
            else frame.setTitle(title);
         }
      }
   }

   private void addSelectedItem(Object obj)
   {
      lastSelected = obj;
      selectedItems.add(obj);
   }

   private void removeSelectedItem(Object obj)
   {
      if (obj == lastSelected)
         lastSelected = null;
      selectedItems.remove(obj);
   }

   private void setSelectedItem(Object obj)
   {
      selectedItems.clear();
      lastSelected = obj;
      if (obj != null) selectedItems.add(obj);
   }

   private void clearSelection()
   {
      selectedItems.clear();
      lastSelected = null;
   }

   /**
    * Sets the value of the hideGrid property
    * @param newValue true if the grid is being hidden
    */
   public void setHideGrid(boolean newValue)
   {
      hideGrid = newValue;
      repaint();
   }

   /**
    * Gets the value of the hideGrid property
    * @return true if the grid is being hidden
    */
   public boolean getHideGrid()
   {
      return hideGrid;
   }

   private Graph graph;
   private Grid grid;
   private GraphFrame frame;
   private ToolBar toolBar;

   private double zoom;
   private double gridSize;
   private boolean hideGrid;
   private boolean modified;

   private Object lastSelected;
   private Set selectedItems;

   private Point2D lastMousePoint;
   private Point2D mouseDownPoint;
   private int dragMode;

   private static final int DRAG_NONE = 0;
   private static final int DRAG_MOVE = 1;
   private static final int DRAG_RUBBERBAND = 2;
   private static final int DRAG_LASSO = 3;

   private static final int GRID = 10;

   private static final int CONNECT_THRESHOLD = 8;

   private static final Color PURPLE = new Color(0.7f, 0.4f, 0.7f);
}