// (c) MIT 2003.  All rights reserved.

package rec.fsm;

///////////////////////////////////////////////////////////////////////////////
//
// FILE:	Edge.java
//
// AUTHOR:	Luke Weisman
//              luke@ai.mit.edu
//
// DATE:	June, 1998
//
// COPYRIGHT:	Massachusetts Institute of Technology
//		All rights reserved.
//
// PURPOSE:	Drawing recognition
//
///////////////////////////////////////////////////////////////////////////////


import java.awt.*;
import rec.util.*;
import rec.sys.*;
import rec.geo.*;
import rec.core.*;
import java.awt.geom.Rectangle2D;

public class Edge extends VisibleWidget implements Registerable, FSM
{
  public static final int HEAD_SIZE = 10;

  transient Surface surf = null;
  
  boolean arrow_head = false;
  boolean arrow_tail = false;

  GPoint head;
  GPoint tail;
  GShape shape;

  Node head_node = null;
  Node tail_node = null;

  String name;


  static {
    Colors.setDefaultColor( "edge", "stroke" );
    Colors.setDefaultColor( "edge-empty", "edge" );
    Colors.setDefaultColor( "edge-full", Color.yellow );
  }


  /**
   * Make an edge with head at p1 and tail at p2
   */
  public Edge( GPoint p1, GPoint p2, Surface surf )
  {
    lineShape( p1, p2 );

    name = null;
    this.surf = surf;
  }


  public Edge( Squiggle s, Surface surf )
  {
    this( s.getFirstPoint(), s.getLastPoint(), surf );
  }

  public Edge( GSegment l, Surface surf )
  {
    this( (GPoint)l.getHeadPoint().copy(), (GPoint)l.getTailPoint().copy(),
	  surf );
  }


  public Edge( WLine l, Surface surf )
  {
    this( l.getHead(), l.getTail(), surf );
  }

  public Edge( Edge old )
  {
    shape = (GShape)old.shape.clone();

    head = new GPoint( old.head );
    tail = new GPoint( old.tail );
    
    surf = old.surf;
    arrow_head = old.arrow_head;
    arrow_tail = old.arrow_tail;
    setNode( old.head_node, true );
    setNode( old.tail_node, false );

    name = old.getName();

    snap();

    //Console.writeln( "clone const of circ: " + shape + ", c = " + circle() );
  }


  /**
   * NOTE: n1 != n2, or we get crash.
   */
  public Edge( Node n1, Node n2, Surface surf )
  {
    name = null;
    this.surf = surf;

    head = n1.getCenter();
    tail = n2.getCenter();

    setNode( n1, true );
    setNode( n2, false );

    snap();
  }

  public Edge( Node n1, Surface surf )
  {
    this( n1, n1, surf );
  }


  void lineShape( GPoint p1, GPoint p2 )
  {
    shape = new GSegment( p1, p2 );

    head = (GPoint) ((GSegment)shape).getHeadPoint().copy();
    tail = (GPoint) ((GSegment)shape).getTailPoint().copy();
  }
  

  void circleShape( GPoint p, double height )
  {
    p = new GPoint( p );
    p.y -= height - 5;
    
    GPoint c = new GPoint( p.x, p.y - 20 );
    shape = new GCircle( new GPoint(c), 20 );
    
    head = new GPoint( p.x + 5, p.y );
    tail = new GPoint( p.x - 5, p.y );
  }



  /////////////////////////////////////////////////////////////////////////////

  public void draw( Graphics graph )
  {
    Graphics2D g = (Graphics2D)graph;

    Color oc = g.getColor();
    Color lcolor = null; // the line color

    if ( getColor() != null )
      g.setColor( getColor() );
    else if ( !full() )
      lcolor = Colors.getColor( "edge-empty" );
    else
      lcolor = Colors.getColor( "edge-full" );

    //    lcolor = new Color( lcolor.getRed(), lcolor.getGreen(),
    //			lcolor.getBlue(), 100 );
    g.setColor( lcolor );

    if ( circle() )
      {
	drawCircle( g );
	return;
      }

    GPoint h = getHead(), t = getTail();

    int x = h.getIntX(), y = h.getIntY(), x2 = t.getIntX(), y2 = t.getIntY();

    // give the nice thick-line look.
    thickLine( g, x, y, x2, y2 );

    Point d = new Point( x2 - x, y2 - y );
    int len = (int) ( (GSegment)shape ).getLength();
    int dx = d.x * HEAD_SIZE / len;
    int dy = d.y * HEAD_SIZE / len;

    //    Stroke bk_st = g.getStroke();

    //    g.setStroke( new BasicStroke( 2 ) );

    if ( arrow_head )
      {
	thickLine( g, x, y, x + dx + -dy, y + dy + dx );
	thickLine( g, x, y, x + dx - -dy, y + dy - dx );
      }

    if ( arrow_tail )
      {
	thickLine( g, x2, y2, x2 - dx + -dy, y2 - dy + dx );
	thickLine( g, x2, y2, x2 - dx - -dy, y2 - dy - dx );
      }

    //    g.setStroke( bk_st );

    // draw the name
    x = h.getIntX() + d.x / 2;
    y = h.getIntY() + d.y / 2;

    drawNameBubble( g, x, y );

    g.setColor( oc );
  }

  public void drawNameBubble( Graphics2D g, int x, int y )
  {
    Stroke bk_st = g.getStroke();

    Color lcolor = g.getColor();

    int dx, dy;

    String nn = (name != null) ? name : toString();
    //    if ( nn.equals( "0" ) )
    //      nn += "dog";

    Font f = g.getFont();
    Font of = f;
    //    f = f.deriveFont( f.BOLD );
    g.setFont( f );
    FontMetrics fms = g.getFontMetrics();
    //    LineMetrics lms = g.getLineMetrics( nn, g );
    Rectangle2D r2d = fms.getStringBounds( nn, g );

    // width and height of the writing
    int ddx = (int) r2d.getWidth();
    int ddy = (int) r2d.getHeight();

    // x,y is the center point on the line
    // bounding box of writing should be straddling it

    //    g.fillRect( x - ddx / 2, y - 3 * ddy / 2 + fms.getMaxDescent(), 
    //		2 * ddx, 2 * ddy );
    
    // black out background
    g.setColor( Colors.getBackground() );

    if ( nn.equals( "1" ) )
      ddx += 2;

    dx = 0;
    if ( nn.equals( "0" ) || nn.equals( "2" ) )
      dx = 0;
    else if ( nn.equals( "1" ) )
      dx = 0;
    
    g.fillOval( x - dx - 5 - ddx / 2, y - ddy, ddx + 10, 2 * ddy + 1 );

    g.setColor( lcolor );

    g.setStroke( new BasicStroke( 1 ) );
    g.drawOval( x - dx - 5 - ddx / 2, y - ddy, ddx + 10, 2 * ddy + 1 );

    g.setStroke( bk_st );

    g.setColor( Colors.getColor( "text" ) );
    
    // adjust to center
    // x, y in drawString is _baseline_ of the first character.
    g.drawString( nn,
		  x - ddx / 2,
		  y + ddy / 2 );
		  //		  y + ( fms.getAscent() + fms.getDescent() ) / 2 );

    /*    System.err.println( "fm stuff: " + fms.getAscent() + " + " 
			+ fms.getDescent() + " =? " + ddy );
    System.err.println( "     " + fms.getHeight() + " " + fms.getLeading() );
    */

    g.setFont( of );
  }


  public static void thickLine( Graphics g, int x, int y, int x2, int y2 )
  {
    g.drawLine( x, y, x2, y2 );
    //    g.drawLine( x + 2, y + 2, x2 + 2 , y2 + 2 );
    //    g.drawLine( x - 2, y + 2, x2 - 2 , y2 + 2 );
  }

  /////////////////////////////////////////////////////////////////////////////


  public Object clone()
  {
    Edge e = new Edge( getHead(), getTail(), surf );

    return e;
  }


  public boolean hasArrowhead( boolean head )
  {
    return head ? arrow_head : arrow_tail;
  }

  public void removeArrowhead( boolean head )
  {
    if ( head )
      arrow_head = false;
    else
      arrow_tail = false;
  }

  public void addArrowhead( boolean head )
  {
    if ( head )
      arrow_head = true;
    else
      arrow_tail = true;
  }


  boolean headLinked( )
  {
    return head_node != null;
  }

  boolean tailLinked( )
  {
    return tail_node != null;
  }

  boolean linked( boolean head )
  {
    if ( head ) return head_node != null;
    else return tail_node != null;
  }


  public GPoint getHead( )
  {
    return new GPoint( head );
  }

  public GPoint getTail( )
  {
    return new GPoint( tail );
  }

  public GPoint getPoint( boolean head )
  {
    if ( head ) return getHead();
    else return getTail();
  }

  /**
   * Snap to any nodes connected.
   */
  public void snap( )
  {
    GPoint p1 = headLinked() ? head_node.getCenter() : getHead();
    GPoint p2 = tailLinked() ? tail_node.getCenter() : getTail();
    p1 = (GPoint)p1.copy();
    p2 = (GPoint)p2.copy();

    if ( circle() )
      {
	circleShape( head_node.getCenter(), head_node.getRadius() );
	return;
      }

    // distance from 2 endpoints
    double d = Geometry.distance( p1, p2 );

    if ( headLinked() )
      {
	double r = head_node.getRadius();
	r += 5;
	p1.x += ( r * ( p2.x - p1.x ) ) / d;
	p1.y += ( r * ( p2.y - p1.y ) ) / d;
	
	// fix the distance if we are going to need it.
	if ( tailLinked() )
	  d = Geometry.distance( p1, p2 );
      }
    
    if ( tailLinked() )
      {
	double r = tail_node.getRadius();
	r += 5;
	p2.x += ( r * ( p1.x - p2.x ) ) / d;
	p2.y += ( r * ( p1.y - p2.y ) ) / d;
      }

    lineShape( p1, p2 );
  }

  public void drawCircle( Graphics g )
  {
    GPoint c = ( (GCircle)shape ).getCenter();
    double r =  ( (GCircle)shape ).getRadius();

    int x = (int)(c.x );
    int y = (int)(c.y );

    int xw =(int)( r );
    int yw =(int)( r );

    g.drawOval(  x - xw, y - yw, xw * 2, yw * 2 );
    x += 2;
    y -= 1;
    g.drawOval( x - xw, y - yw, xw * 2, yw * 2 );
    x += 2;
    y += 2;
    g.drawOval( x - xw, y - yw, xw * 2, yw * 2 );

    
    // draw the name
    x = (int)c.x;
    y = (int)c.y;
    g.setColor( Colors.getColor( "text" ) );
    if ( name != null )
      g.drawString( name, x, y );
    else
      g.drawString( toString(), x, y );

    // draw endpoints
    int hx = (int)head.x, hy = (int)head.y;
    g.drawOval( hx - 3, hy - 3, 6, 6 );
    hx = (int)tail.x;
    hy = (int)tail.y;
    g.drawOval( hx - 3, hy - 3, 6, 6 );

  }


  /**
   * Attach one end of the edge to a node. 
   */
  public void setNode( Node n, boolean is_head )
  {
    if ( is_head )
      {
	// we are already connected.
	if ( ( head_node != null ) && ( head_node.equals( n ) ) )
	  return;

	if ( head_node != null )
	  {
	    if (! head_node.equals( tail_node ) )
	      {
		surf.unregister( this, head_node );
		head_node.removeEdge( this );
	      }
	  }

	head_node = n;
	if ( head_node != null )
	  {
	    head = head_node.getCenter();
	    snap();

	    surf.register( this, n );
	    n.addEdge( this );
	  }
      }
    else
      {
	// we are already connected.
	if ( ( tail_node != null ) && ( tail_node.equals( n ) ) )
	  return;

	if ( tail_node != null )
	  {
	    if (! tail_node.equals( head_node ) )
	      {
		surf.unregister( this, tail_node );
		tail_node.removeEdge( this );
	      }
	  }

	tail_node = n;
	if ( tail_node != null )
	  {
	    tail = tail_node.getCenter();
	    snap();

	    surf.register( this, n );
	    n.addEdge( this );
	  }
      }
  }


  public Node getNode( boolean head )
  {
    return head ? head_node : tail_node;
  }

  
  public boolean isHead( Node n )
  {
    if ( head_node != null )
      return ( head_node.equals( n ) );
    else
      return false;
  }

  public boolean isTail( Node n )
  {
    if ( tail_node != null )
      return ( tail_node.equals( n ) );
    else
      return false;
  }

  
  public boolean isConnected( Node n )
  {
    return n.equals( head_node ) || n.equals( tail_node );
  }

  public boolean full( )
  {
    return ( head_node != null ) && ( tail_node != null );
  }


  /**
   * @return true if single arrowhead.
   */
  public boolean directed( )
  {
    return ( arrow_tail || arrow_head ) && !( arrow_tail && arrow_head );
  }

  /**
   * Directed edge with arrowhead linked to node n
   */
  public boolean pointingTo( Node n )
  {
    return ( ( isHead(n) && arrow_head && !arrow_tail )
	     || ( isTail(n) && arrow_tail && !arrow_head ) );
  }

  public boolean circle( )
  {
    return ( full() && head_node.equals( tail_node ) );
  }

  public void setName( String the_name )
  {
    final String new_name = the_name;
    final String old_name = name;

    AlterTransition tr =  new FlipAlterTransition( surf, this ) {
      
	  String old_name = null;

	  public void doUndo( )
	    {
	      name = new_name;
	      //Console.writeln( "Changing the name for the edge." );
	    }
	  public void doDo()
	    {
	      ((Edge)getTargetWidget()).name = old_name;
	      //Console.writeln( "Reset the name to old value." );
	    }
	  public String name()
	    {
	      return "edge-name";
	    }
	}; // end internal AlterTransition declaration

    tr = (AlterTransition)tr.undo();
    surf.alterWidget( this, tr );
  }

  public String getName()
  {
    return (name != null) ? name.toString() : null;
  }

  
  public String toString( )
  {
    return super.toString() + "-" + name;
  }


  // the register stuff
  public void removed( Widget w )
  {
    //Console.writeln( toString() + " caught a " + w + " dying on me." );
    if ( w.equals( head_node ) )
      head_node = null;

    if ( w.equals( tail_node ) )
      tail_node = null;

    snap();
  }

  /**
   * Called when ow is replaced by nw.
   */
  public void replaced( Widget ow, Widget nw )
  {
    System.err.println( toString() + " examining " + ow + " turning into a " + nw );
    if ( nw instanceof rec.fsm.Node )
      {
	setNode( (Node)nw, ow.equals( head_node ) );
      }
  }

  /***
   * Called when w has been altered.
   *
   * @param w  The Widget after it has been altered.
   * @param t  The Transition that affected that Widget.
   */
  public void altered( Widget w, Transition t )
  {
    if ( t instanceof rec.core.MoveTransition )
      {
	//Console.writeln( toString() + " caught a node moving--readjustment phase." );
	snap();
      }
  }
  



  /****************** STOLEN FROM WLINE ******************/


  /////////////////////////////////////////////////////////////////////////////

  public boolean includes( GPoint p, double offset )
  {
    return( shape.includes( new GPoint( p ), offset ) );
  }

  /////////////////////////////////////////////////////////////////////////////

  public void move( GPoint p )
  {
    //Console.writeln( "Moving by " + p );

    GPoint d = getLocation().getCenter();    
    d.x = p.x - d.x;
    d.y = p.y - d.y;

    shape.translate( d.x, d.y );
    head.translate( d.x, d.y );
    tail.translate( d.x, d.y );

    snap();
  }

  public void rotate( GPoint p, double angle )
  {
    Assert.note( "rotate in rec.fsm.Edge Not implemented" );
    shape.rotate( p, angle );
    if ( shape instanceof GSegment )
      {
	head = ((GSegment)shape).getHeadPoint();
	tail = ((GSegment)shape).getTailPoint();
      }

    snap();
  }

  public double size() 
  {
    return getLength();
  }

  public void resize( double d )
  {
    Assert.note( "resize in rec.fsm.Edge Not implemented" );
  }


  /////////////////////////////////////////////////////////////////////////////

  public Location getLocation()
  {
    return( new Location( shape.getBounds() ) );
  }

  public Rectangle getBounds()
  {
    return( shape.getBounds() );
  }

 
  public double getLength()
  {
    if ( circle() )
      return Math.PI * 2.0 * ( (GCircle)shape ).getRadius();
    else
      return ( (GSegment)shape ).getLength();
  }


  public void setHead( GPoint p )
  {
    p =(GPoint) p.copy();
    if ( circle() )
      circleShape( p, head_node.getRadius() );
    else
      lineShape( p, tail );
  }

  public void setTail( GPoint p )
  {
    p = (GPoint) p.copy();
    if ( circle() )
      circleShape( p, head_node.getRadius() );
    else
      lineShape( head, p );
  }


}  





