import ij.*;
import ij.plugin.*;
import ij.process.*;
import ij.gui.*;
import ij.measure.Calibration;
import java.awt.event.*;
import java.util.EventListener;
import java.awt.Rectangle;
import java.awt.geom.*;
import java.awt.*;
import ij.measure.ResultsTable;
import javax.swing.Timer;

/**
	This plugin implements the KeyListener interface and listens
	for key events generated by the current image.
*/
public class Distance_Between_Polylines implements PlugIn, KeyListener, ActionListener {

	private static ResultsTable results = new ResultsTable();
	ControlWindow window;
	ImagePlus img;
	int action;
	int x0, y0;
	int num_points;
	double x_step, y_step;
	double dx, dy;
	PolygonRoi polyline1, polyline2;


	public void run(String args) {
	
		this.img = WindowManager.getCurrentImage();
		if ( this.img == null ){
			IJ.noImage();
			return;
		}
		ImageWindow win = this.img.getWindow();
		ImageCanvas canvas = win.getCanvas();
		EventListener[] listeners = canvas.getListeners(KeyListener.class);  // kan bruke getKeyListeners
		for ( int i=0; i < listeners.length; ++i ){
			if ( listeners[i].getClass() == this.getClass() ){
				IJ.error("Distance_Between_Polylines already running for this window");
				return;
			}
		}

		canvas.addKeyListener(this);
		window = new ControlWindow("Plugin Message Window", "Please draw a direction line and press [Enter]", this);

		this.action = 0;
		doNextAction();
	}

	// Methods for handling key presses
	public void keyPressed(KeyEvent e) {
		doNextAction();
	}

	public void keyReleased(KeyEvent e) {}
	public void keyTyped(KeyEvent e) {}

	// Methods for managing the window:
	public void actionPerformed( ActionEvent e ) {

		window.setVisible( false );
		window.dispose();
		this.img.getWindow().getCanvas().removeKeyListener(this);
	}

	void terminatePlugin() {

		Timer timeout = new Timer(2000, this);
		timeout.setRepeats(false);
		timeout.start();   // execute actionPerformed() in 2 seconds
	}

	// The important methods:
	void doNextAction(){
	
		switch (action) {
		case 0:
			window.setMessage("Please draw a direction line and press [Enter]");
			break;
		case 1:
			if ( ! readRefLine() ){
				return;
			}
			window.setMessage("Draw the first line and press [Enter]");
			break;
		case 2:
			if ( ! readFirstLine() ){
				return;
			}
			window.setMessage("Draw the second line and press [Enter]");
			break;
		case 3:
			if ( ! readSecondLine() ){
				return;
			}
			
			if ( window.isShowing() ){  // another small hack
				doCalculations();
			}

			terminatePlugin();  // also removes the KeyListener

			break;
		default:
			window.setMessage("Another time in action switch (reset counter?)");
			break;
		}
	
		++this.action;
	}

	boolean readRefLine(){
		
		Roi roi = this.img.getRoi();

		if ( roi != null && roi.getType() == Roi.POLYLINE ){
			PolygonRoi polyline = (PolygonRoi) roi;
			if ( polyline.getNCoordinates() != 2 ){
				IJ.error("The direction line must have only two points");
				return false;
			} else {
				int[] x_coords = polyline.getXCoordinates();
				int[] y_coords = polyline.getYCoordinates();
				Rectangle offset = polyline.getBounds();
				roi = (Roi) new Line(x_coords[0]+offset.x, y_coords[0]+offset.y,
						     x_coords[1]+offset.x, y_coords[1]+offset.y);
			}
		} else if ( roi == null || roi.getType() != Roi.LINE ){
			IJ.error("The direction line must be a line");
			return false;
		}

		Line line = (Line) roi;

		this.dx = line.x2 - line.x1;
		this.dy = line.y2 - line.y1;

		double length = Math.sqrt( dx*dx + dy*dy );

		this.num_points = (int) Math.floor( length ) + 1;

		this.x_step = dx / (num_points - 1);
		this.y_step = dy / (num_points - 1);

		this.x0 = line.x1;
		this.y0 = line.y1;

		return true;
	}

	boolean readFirstLine(){
		
		Roi roi = this.img.getRoi();
		if ( roi != null && roi.getType() == Roi.LINE ){

			Line line = (Line) roi;
			int[] x_coords = { line.x1, line.x2 };
			int[] y_coords = { line.y1, line.y2 };
			roi = (Roi) new PolygonRoi( x_coords, y_coords, 2, Roi.POLYLINE );

		} else if ( roi == null || !(roi.getType() == Roi.POLYLINE || roi.getType() == Roi.FREELINE) ){

			IJ.error("This plugin only work with polylines");
			return false;
		}

		this.polyline1 = (PolygonRoi) roi;

		return true;
	}

	boolean readSecondLine(){

		Roi roi = this.img.getRoi();
		if ( roi != null && roi.getType() == Roi.LINE ){

			Line line = (Line) roi;
			int[] x_coords = { line.x1, line.x2 };
			int[] y_coords = { line.y1, line.y2 };
			roi = (Roi) new PolygonRoi( x_coords, y_coords, 2, Roi.POLYLINE );

		} else if ( roi == null || !(roi.getType() == Roi.POLYLINE || roi.getType() == Roi.FREELINE) ){

			IJ.error("This plugin only work with polylines");
			return false;
		}

		this.polyline2 = (PolygonRoi) roi;

		return true;
	}
	
	double scaleX( double x ) {

		Calibration calib = this.img.getCalibration();
		return (x-calib.xOrigin)*calib.pixelWidth;
	}

	double scaleY( double y ) {

		Calibration calib = this.img.getCalibration();
		return (y-calib.yOrigin)*calib.pixelHeight;
	}
	
	int sign( double num ) {
	
		return (num < 0) ? -1 : 1;
	}
	
	void doCalculations(){

		double x, y;
		double l1_x, l1_y;
		double l2_x, l2_y;
		double x_diff, y_diff;
		double[] distances   = new double[num_points];
		int    num_distances = 0;
		double avg_distance  = 0;
		double dist_variance = 0;
		
/*
// debugging:
IJ.write("refline: " + x0 + "," + y0 + " -> " + (x0+dx) + "," + (y0+dy));
IJ.write("");
int[] x_coords, y_coords;
Rectangle offset;
IJ.write("polyline1: " + polyline1.getNCoordinates() + " coordinates");
x_coords = polyline1.getXCoordinates();
y_coords = polyline1.getYCoordinates();
offset = polyline1.getBounds();
for ( int i = 0; i < polyline1.getNCoordinates(); ++i ){
	IJ.write("polyline1["+i+"] = " + (x_coords[i]+offset.x) + "," + (y_coords[i]+offset.y));
}
IJ.write("");
IJ.write("polyline2: " + polyline2.getNCoordinates() + " coordinates");
x_coords = polyline2.getXCoordinates();
y_coords = polyline2.getYCoordinates();
offset = polyline2.getBounds();
for ( int i = 0; i < polyline2.getNCoordinates(); ++i ){
	IJ.write("polyline2["+i+"] = " + (x_coords[i]+offset.x) + "," + (y_coords[i]+offset.y));
}
IJ.write("");
// end debugging
*/

		for ( int point = 0; point < num_points; ++point ){
			// the actual point on the reference/direction line
			x = scaleX(this.x0 + point*this.x_step);
			y = scaleY(this.y0 + point*this.y_step);

			Point2D.Double p1 = getOrthogonalPoint( x, y, polyline1 );
			l1_x = p1.getX();
			l1_y = p1.getY();
			if ( l1_x == 0 && l1_y == 0 ){
				// et lite hack...
				continue;
			}

			Point2D.Double p2 = getOrthogonalPoint( x, y, polyline2 );
			l2_x = p2.getX();
			l2_y = p2.getY();
			if ( l2_x == 0 && l2_y == 0 ){
				// et lite hack her ogs�...
				continue;
			}
		
			// calculate the distance between the lines
			x_diff = l2_x - l1_x;
			y_diff = l2_y - l1_y;

			// store the distance in an array
			distances[num_distances] = Math.sqrt( x_diff*x_diff + y_diff*y_diff );
			++num_distances;

			if ( point % 10 == 0 ) {
				Calibration calib = this.img.getCalibration();
				img.getProcessor().drawLine(
					(int)(l1_x/calib.pixelWidth  + calib.xOrigin),
					(int)(l1_y/calib.pixelHeight + calib.yOrigin), 
					(int)(l2_x/calib.pixelWidth  + calib.xOrigin),
					(int)(l2_y/calib.pixelHeight + calib.yOrigin));
				img.updateAndRepaintWindow();
			}
		}

		// calculate average distance
		for ( int i = 0; i < num_distances; ++i ) {
			avg_distance += distances[i] / num_distances;
		}
		window.setMessage("Average distance: " + avg_distance);

		// calculate standard deviation (variance first)
		for ( int i = 0; i < num_distances; ++i ) {
			dist_variance += (distances[i] - avg_distance) * (distances[i] - avg_distance) / (num_distances - 1);
		}

		results.incrementCounter();
		results.addLabel("Filename", this.img.getTitle());
		results.addValue("Avg distance", avg_distance);
		results.addValue("Std deviation", Math.sqrt(dist_variance));
		results.show("Average distances");
	}
	
	public Point2D.Double getOrthogonalPoint( double x, double y, PolygonRoi polyline ) {
	
		// first find the approximately orthogonal point

		int[] x_coords = polyline.getXCoordinates();
		int[] y_coords = polyline.getYCoordinates();
		Rectangle offset = polyline.getBounds();

		int i, lo_i, hi_i;
		double val, lo_val, hi_val;
		lo_i = 0;
		hi_i = polyline.getNCoordinates() - 1;
		lo_val = this.dx*(x-scaleX(x_coords[lo_i]+offset.x))
		       + this.dy*(y-scaleY(y_coords[lo_i]+offset.y));
		hi_val = this.dx*(x-scaleX(x_coords[hi_i]+offset.x))
		       + this.dy*(y-scaleY(y_coords[hi_i]+offset.y));

		if ( sign(lo_val) == sign(hi_val) ){
			return new Point2D.Double();    // 0,0 indikerer feil :-}
		}

		for ( int diff = (hi_i-lo_i)/2; diff >= 1; diff = (hi_i-lo_i)/2 ){
			i = lo_i + diff;
			val = this.dx*(x-scaleX(x_coords[i]+offset.x)) 
			    + this.dy*(y-scaleY(y_coords[i]+offset.y));

			if ( sign(val) == sign(lo_val) ){
				lo_i = i;
				lo_val = val;
			} else {
				hi_i = i;
				hi_val = val;
			}
		}
		
// debuggin:
if ( hi_i - lo_i != 1 ){
	IJ.error("hi_i - lo_i != 1");
}

		// then find the exact orthogonal point
		double lo_x = scaleX(x_coords[lo_i]+offset.x);
		double lo_y = scaleY(y_coords[lo_i]+offset.y);
		double hi_x = scaleX(x_coords[hi_i]+offset.x);
		double hi_y = scaleY(y_coords[hi_i]+offset.y);

		double a1; // slope for the polyline between lo_x,lo_y and hi_x,hi_y
		if ( lo_x == hi_x ){
			// vertical line
			a1 = 1e14; // use a really large number
		} else {
			a1 = ( hi_y - lo_y )/( hi_x - lo_x );
		}
// debugging:
if ( Math.abs(a1) > 1e14 ){
	IJ.error("a1 is a really big number: "+a1);
}
		

		double a2; // slope for the orthogonal to the direction line at x,y
		if ( this.dy != 0 ){
			a2 = -( this.dx/this.dy ); // the orthogonal line has coordinates (x,y)+a2*(-dy,dx)
		} else {
			// direction line horizontal, and thus the orthogonal is vertical
			//a2 = 1e14; // use a really large number
			return new Point2D.Double( x, lo_y + a1*(x-lo_x) );
		}
// debugging:
if ( Math.abs(a2) > 1e14 ){
	IJ.error("a2 is a really big number: "+a2);
}

		if ( a1 == a2 ){  // do I need some fuzziness here?
			// the line segment between lo_x,lo_y and hi_x,hi_y is
			// perpendicular to the direction line. Just use the middle point.
			return new Point2D.Double( (hi_x+lo_x)/2, (hi_y+lo_y)/2 );
		} else {
			double xn = ( (y-a2*x) - (lo_y-a1*lo_x) )/(a1-a2);
			return new Point2D.Double( xn, y+a2*(xn-x) );
		}

	}

}

class ControlWindow extends Dialog {
	Label label;

	public ControlWindow(String title, String message, ActionListener listener) {
		super( IJ.getInstance(), title, false );

		setLayout( new BorderLayout() );
		if ( message==null ){
			message = "";
		}
		
		Panel center = new Panel();
		center.setLayout( new FlowLayout( FlowLayout.CENTER, 15, 15 ) );
		add( "Center", center );

		this.label = new Label();
		center.add( this.label );
		setMessage( message );

		Button button = new Button( "  End plugin  " );
		button.addActionListener( listener );
		Panel panel = new Panel();
		panel.setLayout( new FlowLayout() );
		panel.add( button );
		add( "South", panel );
		
		if ( ij.IJ.isMacintosh() ){
//			setResizable( false );
		}
		
		pack();
		placeUpperRight( this );
		show();	
	}

	public void setMessage( String new_message ) {

		label.setText( new_message );
		if ( label.getMinimumSize().getWidth() > label.getSize().getWidth() ){
			// how do I make the label and window larger?
			this.validate();
		}
	}
	
	static void placeUpperRight(Window win) {
		Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
		Dimension window = win.getSize();
		
		if (window.width==0){
			return;
		}
		
		win.setLocation( screen.width-window.width, 0 );
	}

}