import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

import javax.swing.JLabel;

/**
   An edge that is composed of multiple line segments
*/
public abstract class SegmentedLineEdge extends ShapeEdge
{
   /**
      Costructs an edge with no adornments.
   */
   public SegmentedLineEdge()
   {
      lineStyle = LineStyle.SOLID;
      startArrowHead = ArrowHead.NONE;
      endArrowHead = ArrowHead.NONE;
      startLabel = "";
      middleLabel = "";
      endLabel = "";
   }

   /**
      Sets the line style property.
      @param newValue the new value
   */
   public void setLineStyle(LineStyle newValue) { lineStyle = newValue; }

   /**
      Gets the line style property.
      @return the line style
   */
   public LineStyle getLineStyle() { return lineStyle; }

   /**
      Sets the start arrow head property
      @param newValue the new value
   */
   public void setStartArrowHead(ArrowHead newValue) { startArrowHead = newValue; }

   /**
      Gets the start arrow head property
      @return the start arrow head style
   */
   public ArrowHead getStartArrowHead() { return startArrowHead; }

   /**
      Sets the end arrow head property
      @param newValue the new value
   */
   public void setEndArrowHead(ArrowHead newValue) { endArrowHead = newValue; }

   /**
      Gets the end arrow head property
      @return the end arrow head style
   */
   public ArrowHead getEndArrowHead() { return endArrowHead; }

   /**
      Sets the start label property
      @param newValue the new value
   */
   public void setStartLabel(String newValue) { startLabel = newValue; }

   /**
      Gets the start label property
      @return the label at the start of the edge
   */
   public String getStartLabel() { return startLabel; }

   /**
      Sets the middle label property
      @param newValue the new value
   */
   public void setMiddleLabel(String newValue) { middleLabel = newValue; }

   /**
      Gets the middle label property
      @return the label at the middle of the edge
   */
   public String getMiddleLabel() { return middleLabel; }

   /**
      Sets the end label property
      @param newValue the new value
   */
   public void setEndLabel(String newValue) { endLabel = newValue; }

   /**
      Gets the end label property
      @return the label at the end of the edge
   */
   public String getEndLabel() { return endLabel; }

   /**
      Draws the edge.
      @param g2 the graphics context
   */
   public void draw(Graphics2D g2)
   {
      ArrayList points = getPoints();
      
      Stroke oldStroke = g2.getStroke();
      g2.setStroke(lineStyle.getStroke());
      g2.draw(getSegmentPath());
      g2.setStroke(oldStroke);
      startArrowHead.draw(g2, (Point2D)points.get(1),
         (Point2D)points.get(0));
      endArrowHead.draw(g2, (Point2D)points.get(points.size() - 2),
         (Point2D)points.get(points.size() - 1));

      drawString(g2, (Point2D)points.get(1), (Point2D)points.get(0), 
            startArrowHead, startLabel, false);
      drawString(g2, (Point2D)points.get(points.size() / 2 - 1),
            (Point2D)points.get(points.size() / 2),
            null, middleLabel, true);
      drawString(g2, (Point2D)points.get(points.size() - 2),
            (Point2D)points.get(points.size() - 1), 
            endArrowHead, endLabel, false);
   }

   /**
      Draws a string.
      @param g2 the graphics context
      @param p an endpoint of the segment along which to
      draw the string
      @param q the other endpoint of the segment along which to
      draw the string
      @param s the string to draw
      @param center true if the string should be centered
      along the segment
   */
   private static void drawString(Graphics2D g2, 
      Point2D p, Point2D q, ArrowHead arrow, String s, boolean center)
   {
      if (s == null || s.length() == 0) return;
      label.setText("<html>" + s + "</html>");
      label.setFont(g2.getFont());
      Dimension d = label.getPreferredSize();      
      label.setBounds(0, 0, d.width, d.height);

      Rectangle2D b = getStringBounds(g2, p, q, arrow, s, center);
      
      Color oldColor = g2.getColor();
      g2.setColor(g2.getBackground());
      g2.fill(b);
      g2.setColor(oldColor);
      
      g2.translate(b.getX(), b.getY());
      label.paint(g2);
      g2.translate(-b.getX(), -b.getY());        
   }

   /**
      Computes the attachment point for drawing a string.
      @param g2 the graphics context
      @param p an endpoint of the segment along which to
      draw the string
      @param q the other endpoint of the segment along which to
      draw the string
      @param b the bounds of the string to draw
      @param center true if the string should be centered
      along the segment
      @return the point at which to draw the string
   */
   private static Point2D getAttachmentPoint(Graphics2D g2, 
      Point2D p, Point2D q, ArrowHead arrow, Dimension d, boolean center)
   {    
      final int GAP = 3;
      double xoff = GAP;
      double yoff = -GAP - d.getHeight();
      Point2D attach = q;
      if (center)
      {
         if (p.getX() > q.getX()) 
         { 
            return getAttachmentPoint(g2, q, p, arrow, d, center); 
         }
         attach = new Point2D.Double((p.getX() + q.getX()) / 2, 
            (p.getY() + q.getY()) / 2);
         if (p.getY() < q.getY())
            yoff =  - GAP - d.getHeight();
         else if (p.getY() == q.getY())
            xoff = -d.getWidth() / 2;
         else
            yoff = GAP;
      }
      else 
      {
         if (p.getX() < q.getX())
         {
            xoff = -GAP - d.getWidth();
         }
         if (p.getY() > q.getY())
         {
            yoff = GAP;
         }
         if (arrow != null)
         {
            Rectangle2D arrowBounds = arrow.getPath(p, q).getBounds2D();
            if (p.getX() < q.getX())
            {
               xoff -= arrowBounds.getWidth();
            }
            else
            {
               xoff += arrowBounds.getWidth();
            }
         }
      }
      return new Point2D.Double(attach.getX() + xoff, attach.getY() + yoff);
   }

   /**
      Computes the extent of a string that is drawn along a line segment.
      @param g2 the graphics context
      @param p an endpoint of the segment along which to
      draw the string
      @param q the other endpoint of the segment along which to
      draw the string
      @param s the string to draw
      @param center true if the string should be centered
      along the segment
      @return the rectangle enclosing the string
   */
   private static Rectangle2D getStringBounds(Graphics2D g2, 
      Point2D p, Point2D q, ArrowHead arrow, String s, boolean center)
   {
      if (g2 == null) return new Rectangle2D.Double();
      if (s == null || s.equals("")) return new Rectangle2D.Double(q.getX(), q.getY(), 0, 0);
      label.setText("<html>" + s + "</html>");
      label.setFont(g2.getFont());
      Dimension d = label.getPreferredSize();
      Point2D a = getAttachmentPoint(g2, p, q, arrow, d, center);
      return new Rectangle2D.Double(a.getX(), a.getY(), d.getWidth(), d.getHeight());
   }

   public Rectangle2D getBounds(Graphics2D g2)
   {
      ArrayList points = getPoints();
      Rectangle2D r = super.getBounds(g2);
      r.add(getStringBounds(g2, 
               (Point2D)points.get(1), (Point2D)points.get(0), 
               startArrowHead, startLabel, false));
      r.add(getStringBounds(g2, 
               (Point2D)points.get(points.size() / 2 - 1),
               (Point2D)points.get(points.size() / 2), 
               null, middleLabel, true));
      r.add(getStringBounds(g2, 
               (Point2D)points.get(points.size() - 2),
               (Point2D)points.get(points.size() - 1), 
               endArrowHead, endLabel, false));
      return r;
   }

   public Shape getShape()
   {
      GeneralPath path = getSegmentPath();
      ArrayList points = getPoints();
      path.append(startArrowHead.getPath((Point2D)points.get(1),
            (Point2D)points.get(0)), false);
      path.append(endArrowHead.getPath((Point2D)points.get(points.size() - 2),
            (Point2D)points.get(points.size() - 1)), false);
      return path;
   }

   private GeneralPath getSegmentPath()
   {
      ArrayList points = getPoints();
      
      GeneralPath path = new GeneralPath();
      Point2D p = (Point2D) points.get(points.size() - 1);
      path.moveTo((float) p.getX(), (float) p.getY());
      for (int i = points.size() - 2; i >= 0; i--)
      {
         p = (Point2D) points.get(i);
         path.lineTo((float) p.getX(), (float) p.getY());
      }
      return path;
   }
   
   public Line2D getConnectionPoints()
   {
      ArrayList points = getPoints();
      return new Line2D.Double((Point2D) points.get(0),
         (Point2D) points.get(points.size() - 1));
   }

   /**
      Gets the corner points of this segmented line edge
      @return an array list of Point2D objects, containing
      the corner points
   */
   public abstract ArrayList getPoints();

   private LineStyle lineStyle;
   private ArrowHead startArrowHead;
   private ArrowHead endArrowHead;
   private String startLabel;
   private String middleLabel;
   private String endLabel;
   
   private static JLabel label = new JLabel();
}
