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

////////////////////////////////////////////////////////////////////////////////
//
// FILE:	Squiggle.java
//
// AUTHOR:	Manoj D. Muzumdar
//		archon@ai.mit.edu
//
//              Adapted from code written by Luke Weisman
//              luke@ai.mit.edu
//
// DATE:	06-XX-98
//
// COPYRIGHT:	Massachusetts Institute of Technology
//		All rights reserved.
//
// PURPOSE:	Drawing recognition
//
// HISTORY:     11-15-98 archon
//              Now aware of new rec.geo package
//
//              January, 1999 archon
//              Added SquiggleCleaner stuff into Squiggle in the form of clean()
//
////////////////////////////////////////////////////////////////////////////////

package rec.sys;

import edu.mit.sketch.geom.Point;
import edu.mit.sketch.geom.Vertex;
import edu.mit.sketch.toolkit.SimpleClassifier;
import edu.mit.sketch.toolkit.SimpleClassifier2;
import edu.mit.sketch.toolkit.SimpleClassifier3;
import edu.mit.sketch.toolkit.StrokeData;
import edu.mit.sketch.toolkit.StrokeDataReader;
import java.awt.*;
import java.io.*;
import java.util.*;
import rec.geo.*;
import rec.util.*;


/**
 * This Widget should not be derived from.
 *
 * It is a special system low-level Widgets.
 */
public final class Squiggle extends VisibleWidget
{
  private double m_PixelVariance;

  private GPoint m_Point;
  private GPolySegment m_PolySegment;
  private boolean m_isPolySegment;

  private StrokeDataReader m_strokeDataReader;
  private SimpleClassifier m_classifier;

  // memoized Squiggle length
  private double m_Length = -1.0;

  // the width of a stroke when drawn to screen
  private int width = 4;

  private double stroke_speed = -1;

  public Squiggle( GPoint gp, long timestamp )
  {
    m_PixelVariance = 5.0;

    m_Point = new GPoint( gp );
    m_strokeDataReader = new StrokeDataReader();
    addStrokeDataPoint( gp, timestamp );
    m_PolySegment = null; 
    m_isPolySegment = false;
    setEnglishName( "Uninterpreted Stroke" );
  }

  public Squiggle( double pixelVariance, GPoint gp, long timestamp )
  {
    m_PixelVariance = pixelVariance;

    m_strokeDataReader = new StrokeDataReader();
    addStrokeDataPoint( gp, timestamp );

    m_Point = new GPoint( gp );
    m_PolySegment = null;
    m_isPolySegment = false;
    setEnglishName( "Uninterpreted Stroke" );
  }

  public Squiggle( Squiggle s )
  {
    this( 5.0, s );
  }

  public Squiggle( double pixelVariance, Squiggle s )
  {
    m_PixelVariance = pixelVariance;
    setEnglishName( "Uninterpreted Stroke" );
    
    if( !s.m_isPolySegment ) {
      m_Point = new GPoint( s.m_Point );
      m_PolySegment = null;
      m_isPolySegment = false;
    }
    else {
      m_Point = null;
      m_PolySegment = new GPolySegment( s.m_PolySegment );
      m_isPolySegment = true;
    }

    setSpeed( getSpeed() );
  }


  private Squiggle( GPolySegment gps )
  {
    setEnglishName( "Uninterpreted Stroke" );
    m_Point = null;
    m_PolySegment = new GPolySegment( gps );
    m_isPolySegment = true;
  }


  public void setSpeed( double speed )
  {
    stroke_speed = speed;
  }

  public double getSpeed()
  {
    return stroke_speed;
  }

  public void setTime(long time)
  {
    m_TimeStamp = time;
  }
  
  /** The average length of the line segments that make up the squiggle. **/
  public double size()
  {
    if( !m_isPolySegment )
      return( m_Point.size() );

    return( m_PolySegment.size() );
  }

  public void resize( double scaleFactor )
  {
    if( !m_isPolySegment ) {
      m_Point.resize( scaleFactor );
      return;
    }

    m_PolySegment.resize( scaleFactor );
  }

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

  public Object clone()
  {
    return( new Squiggle( this ) );
  }

  public Squiggle copy()
  {
    return( new Squiggle( this ) );
  }

  public void reflect()
  {
    if( !m_isPolySegment ) {
      m_Point.reflect();
    }

    m_PolySegment.reflect();
  }

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

  public void translate( double dx, double dy )
  {
    if( !m_isPolySegment ) {
      m_Point.translate( dx, dy );
      return;
    }

    m_strokeDataReader.translate(dx,dy);
    m_classifier = null;

    m_PolySegment.translate( dx, dy );
  }

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

  // cleans up the stroke points to reduce noise
  public void clean()
  {
    int size = getNumberOfPoints();

    if( size == 0 || size == 1 )
      return;

    boolean changeMade = false;

    Vector newPoints = new Vector();

    int i = 0;

    while( i < size ) {

      if( i == size - 1 ) {
	newPoints.addElement( m_PolySegment.getPointAt( i++ ) );
	continue;
      }

      GPoint first = getPointAt( i );
      GPoint current = null;

      Vector tmp = new Vector();
      int j = i + 1;

      while( j < size ) {
	current = m_PolySegment.getPointAt( j );
	if( Geometry.distance( first, current ) <= m_PixelVariance ) {
	  tmp.addElement( current );
	  changeMade = true;
	  ++i;
	  ++j;
	  continue;
	}
	break;
      }

      if( tmp.size() == 0 ) {
	newPoints.addElement( first );
      }
      else {
	double totalX = first.x;
	double totalY = first.y;

	for( int k = 0; k < tmp.size(); ++k ) {
	  current = (GPoint)tmp.elementAt( k );
	  totalX += current.x;
	  totalY += current.y;
	}

	newPoints.addElement( new GPoint( totalX / (tmp.size() + 1),
					  totalY / (tmp.size() + 1) ) );
      }

      ++i;
    }

    if( changeMade && ( newPoints.size() > 1 ) )
      m_PolySegment = new GPolySegment( newPoints );
  }

  public double getPixelVariance()
  {
    return( m_PixelVariance );
  }

  public void setPixelVariance( double pixelVariance )
  {
    m_PixelVariance = pixelVariance;
  }

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

  public void addPoint( GPoint p, long timestamp )
  {
    addStrokeDataPoint( p, timestamp );

    GPoint gp = new GPoint( p );
    if( !m_isPolySegment ) {
      m_PolySegment = new GPolySegment( new GSegment( m_Point, gp ) );
      m_isPolySegment = true;
      return;
    }
    
    m_PolySegment.addPointToTail( gp );
  }

  /**
   * This is just for the stroke data stuff for the new fangled
   * toolkit recognizers.  Unless you are dealing directly with the
   * toolkit this is useless.
   **/
  private void addStrokeDataPoint( GPoint p, long timestamp )
  {
    Point point = new Point( p.getIntX(), p.getIntY() );
    point.setTimeStamp( timestamp );
    m_strokeDataReader.addPoint( point );
  }

  public SimpleClassifier getClassifier() 
  {
    if( m_classifier == null && m_strokeDataReader.hasSufficientData() ) {
      StrokeData stroke = m_strokeDataReader.getStrokeData();
      m_classifier = new SimpleClassifier3( stroke );
      // tell it to ignore general paths and always choose a polyline
      //m_classifier.polyline_vs_general_path_bias = 0;
      m_classifier.classify();
    }
    return m_classifier;
  }

  // ada: added to get the start and end times of the squiggle
  // uese the actual time stamps of the points set to the toolkit
  // recognizers
  public long getStartTime()
  {
    StrokeData stroke = m_strokeDataReader.getStrokeData();
    return stroke.getStartTime();
  }

  // ada: like getStartTime above but returns the end time of
  // the squiggle.
  public long getEndTime()
  {
    StrokeData stroke = m_strokeDataReader.getStrokeData();
    return stroke.getEndTime();
  }
  
  public int getNumberOfPoints()
  {
    if( !m_isPolySegment )
      return( 1 );

    return( m_PolySegment.getNumberOfPoints() );
  }

  public GPoint getFirstPoint()
  {
    if( !m_isPolySegment )
      return( m_Point );

    return( m_PolySegment.getHeadPoint() );
  }

  public GPoint getLastPoint()
  {
    if( !m_isPolySegment )
      return( m_Point );

    return( m_PolySegment.getTailPoint() );
  }

  public GPoint getPointAt( int i )
  {
    Assert.check( i >= 0 && i < getNumberOfPoints(), "index out of bounds" );

    if( !m_isPolySegment )
      return( m_Point );

    return( m_PolySegment.getPointAt( i ) );
  }

//    public Squiggle getSub( int first, int last )
//    {
//      if( !m_isPolySegment ) {
//        Assert.check( first == 0 && last == 0, "index out of bounds" );
//        return( new Squiggle( m_Point ) );
//      }

//      return( new Squiggle( m_PolySegment.getMiddleSub( first, last ) ) );
//    }
  
  public void setWidth( int width )
  {
    this.width = width;
  }

  public int getWidth( )
  {
    return width;
  }

  public void drawPlain( Graphics graph ) 
  {
    Graphics2D g = (Graphics2D)graph;
    Stroke st_bk = g.getStroke();
    g.setStroke( new BasicStroke( width ) );
    
    if( !m_isPolySegment )
      return;

    int i = 0;
    for( i = 0; i < m_PolySegment.getNumberOfPoints() - 1; ++i ) {
      g.drawLine( m_PolySegment.getPointAt( i ).getIntX(),
		  m_PolySegment.getPointAt( i ).getIntY(),
		  m_PolySegment.getPointAt( i + 1 ).getIntX(),
		  m_PolySegment.getPointAt( i + 1 ).getIntY() );
    }

    g.setStroke( st_bk );
  }
    


  public void draw( Graphics graph )
  {
    Graphics2D g = (Graphics2D)graph;
    Stroke st_bk = g.getStroke();
    g.setStroke( new BasicStroke( width ) );
    
    if( !m_isPolySegment )
      return;

    Color currColor = g.getColor();
    Color[] c = new Color[3];

//      if( getColor() != null ) 
//        currColor = getColor();
    
    c[0] = Colors.fade( currColor );
    c[1] = currColor;
    c[2] = Colors.fade( currColor, 1 );
    
    int i = 0;
    for( i = 0; i < m_PolySegment.getNumberOfPoints() - 1; ++i ) {
     
      g.setColor( c[i % 3] );
      g.drawLine( m_PolySegment.getPointAt( i ).getIntX(),
		  m_PolySegment.getPointAt( i ).getIntY(),
		  m_PolySegment.getPointAt( i + 1 ).getIntX(),
		  m_PolySegment.getPointAt( i + 1 ).getIntY() );
    }

    g.setStroke( st_bk );
  }

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

  public boolean includes( GPoint p, double offset )
  {
    if( !m_isPolySegment )
      return( m_Point.includes( p, offset ) );

    return( m_PolySegment.includes( p, offset ) );
  }

  public boolean contains( GPoint p )
  {
    if( !m_isPolySegment )
      return m_Point.includes( p );

    return m_PolySegment.contains( p );
  }

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

  public void move( GPoint p )
  {
    GPoint curr = getLocation().getCenter();

    double dx = p.x - curr.x;
    double dy = p.y - curr.y;

    translate( dx, dy );
  }

  public void rotate( GPoint p, double angle )
  {
    if( !m_isPolySegment ) {
      m_Point.rotate( p, angle );
      return;
    }

    m_PolySegment.rotate( p, angle );
  }

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

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

  public Rectangle getBounds()
  {
    if( !m_isPolySegment )
      return( m_Point.getBounds() );

    return( m_PolySegment.getBounds() );
  }

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

  public double getLength()
  {
    if( !m_isPolySegment )
      return( 0.0 );

    // this looks fishy since we can change polysegment when we add
    // points...  MO
    if( m_Length < 0 )
      m_Length = m_PolySegment.getLength();
    
    return( m_Length );
  }

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

  public GPolySegment getPolygon()
  {
    if( m_isPolySegment )
      return( m_PolySegment );

    return( null );
  }

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

//    public void saveSquiggle(ObjectOutputStream out)
//    {
//      if (m_isPolySegment) {
//        try {
//  	out.writeObject(m_PolySegment);
//        }
//        catch (IOException e){
//  	e.printStackTrace();
//        }
//      }
//    }

//    public static Squiggle restoreSquiggle(ObjectInputStream input)
//    {
//      GPolySegment segment = (GPolySegment)input.readObject();
//      return new Squiggle(segment);    
//    }



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

  /**
   * Method to save all the stroke data.
   */
  public Vertex[] getVertices() {
      StrokeData stroke = m_strokeDataReader.getStrokeData();
      return stroke.getVertices();
  }
  
  /**
   * Get the stroke data object for this squiggle
   * @return A StrokeData object containing the points and timestamps
   */
  public StrokeData getStrokeData() {
    return m_strokeDataReader.getStrokeData();
  }

}
