package ij.plugin;
import ij.*;
import ij.plugin.filter.*;
import ij.process.*;
import ij.gui.*;
import java.awt.*;
import java.awt.image.*;
import java.math.*;
import java.util.*;
import ij.measure.*;


public class SurfacePlotter implements PlugIn {

	static final int fontSize = 14;
	static int plotWidth = 350;
	static int polygonMultiplier = 100;
	static boolean oneToOne;
	static boolean firstTime = true;
	
	static boolean showWireframe=false;
	static boolean showGrayscale=true;
	static boolean showAxis=true;
	static boolean whiteBackground=false;
	static boolean blackFill=false;
	static boolean smooth = true;

	ImagePlus img;
	int[] x,y;
	boolean invertedLut;
	double angleInDegrees = 35;
	double angle = (angleInDegrees/360.0)*2.0*Math.PI;
	double angle2InDegrees = 15.0;
	double angle2 = (angle2InDegrees/360.0)*2.0*Math.PI;
	double yinc2 = Math.sin(angle2);
	double p1x, p1y;  // left bottom corner
	double p2x, p2y;  // center bottom corner
	double p3x, p3y;  // right  bottom corner
	
	LookUpTable lut;
	
	public void run(String arg) {
		img = WindowManager.getCurrentImage();
		if (img==null)
			{IJ.noImage(); return;}
		if (img.getType()==ImagePlus.COLOR_RGB)
			{IJ.error("Surface Plotter", "Grayscale or pseudo-color image required"); return;}
		invertedLut = img.getProcessor().isInvertedLut();
		if (firstTime) {
			if (invertedLut)
				whiteBackground	= true;
			firstTime = false;			
		}
		if (!showDialog())
			return;

		int stackFlags = IJ.setupDialog(img, 0);
		if(stackFlags == PlugInFilter.DONE)
			return;
		Date start = new Date();
		lut = img.createLut();
		
		if (stackFlags==PlugInFilter.DOES_STACKS && img.getStack().getSize()>1){			
			ImageStack stackSource = img.getStack();
			ImageProcessor ip = stackSource.getProcessor(1);
			ImageProcessor plot = makeSurfacePlot(ip);
			ImageStack stack = new ImageStack(plot.getWidth(), plot.getHeight());
			stack.setColorModel(plot.getColorModel());			
			for (int i=1;i<=stackSource.getSize();i++)
				stack.addSlice(null, plot.duplicate().getPixels());
			stack.setPixels(plot.getPixels(), 1);
			ImagePlus plots = new ImagePlus("Surface Plot", stack);
			plots.show();
			for (int i=2;i<=stackSource.getSize();i++) {
				IJ.showStatus("Drawing slice " + i + "..." + " (" + (100*(i-1)/stackSource.getSize()) + "% done)");
				ip = stackSource.getProcessor(i);
				plot = makeSurfacePlot(ip);
				ImageWindow win = plots.getWindow();
				if (win!=null && win.isClosed()) break;
				stack.setPixels(plot.getPixels(), i);
				plots.setSlice(i);
			}
		} else {
			ImageProcessor plot = makeSurfacePlot(img.getProcessor());
			new ImagePlus("Surface Plot", plot).show();
		}
		
		Date end = new Date();
     		long lstart = start.getTime();
      		long lend = end.getTime();
      		long difference = lend - lstart;
		IJ.register(SurfacePlotter.class);
		IJ.showStatus("Done in "+difference+" msec." );
	}
	
	boolean showDialog() {
		GenericDialog gd = new GenericDialog("Surface Plotter");
		//gd.addNumericField("Plot Width (pixels):", plotWidth, 0);
		//gd.addNumericField("Angle (-90-90 degrees):", angleInDegrees, 0);
		gd.addNumericField("Polygon Multiplier (10-200%):", polygonMultiplier, 0);
		gd.addCheckbox("Draw_Wireframe", showWireframe);
		gd.addCheckbox("Shade", showGrayscale);
		gd.addCheckbox("Draw_Axis", showAxis);
		gd.addCheckbox("Source Background is Lighter", whiteBackground);
		gd.addCheckbox("Fill Plot Background with Black", blackFill);
		gd.addCheckbox("One Polygon Per Line", oneToOne);
		gd.addCheckbox("Smooth", smooth);
		gd.showDialog();
		if (gd.wasCanceled())
			return false;
		//plotWidth = (int) gd.getNextNumber();
		//angleInDegrees = gd.getNextNumber();
		polygonMultiplier = (int)gd.getNextNumber();
		showWireframe = gd.getNextBoolean();
		showGrayscale = gd.getNextBoolean();
		showAxis = gd.getNextBoolean();
		whiteBackground = gd.getNextBoolean();
		blackFill = gd.getNextBoolean();
		oneToOne = gd.getNextBoolean();
		smooth = gd.getNextBoolean();
		if (showWireframe && !showGrayscale)
			blackFill = false;
		if (polygonMultiplier>400) polygonMultiplier = 400;
		if (polygonMultiplier<10) polygonMultiplier = 10;
		return true;
	}
	
	public ImageProcessor makeSurfacePlot(ImageProcessor ip) {
		ip = ip.duplicate();
		Rectangle roi = img.getProcessor().getRoi();
		ip.setRoi(roi);
		if (!(ip instanceof ByteProcessor)) {
			ip.setMinAndMax(img.getProcessor().getMin(), img.getProcessor().getMax());
			ip = ip.convertToByte(true);
			ip.setRoi(roi);
		}
		double angle = (angleInDegrees/360.0)*2.0*Math.PI;
		int polygons = (int)(plotWidth*(polygonMultiplier/100.0)/4);
		if (oneToOne)
			polygons = roi.height;
		double xinc = 0.8*plotWidth*Math.sin(angle)/polygons;
		double yinc = 0.8*plotWidth*Math.cos(angle)/polygons;
		IJ.showProgress(0.01);
		ip.setInterpolate(!oneToOne);
		ip = ip.resize(plotWidth, polygons);
		int width = ip.getWidth();
		int height = ip.getHeight();
		double min = ip.getMin();
		double max = ip.getMax();
				
		if(invertedLut) ip.invert();
		if(whiteBackground) ip.invert();
		if (smooth) ip.smooth();

		x = new int[width+2];
		y = new int[width+2];
		double xstart = 10.0;
		if (xinc<0.0)
			xstart += Math.abs(xinc)*polygons;
		ByteProcessor ipProfile =new ByteProcessor(width, (int)(256+width*yinc2));
		ipProfile.setValue(255);
		ipProfile.fill();
		double ystart =  yinc2*width;
		int ybase = (int)(ystart+0.5);
		int windowWidth =(int)(plotWidth+polygons*Math.abs(xinc) + 20.0);
		int windowHeight = (int)(ipProfile.getHeight()+polygons*yinc + 10.0);
		
		if(showAxis){
			xstart += 50+20;
			ystart += 10;
			windowWidth += 60+20;
			windowHeight += 20;
			p1x = xstart;
			p1y = ystart+255;
			p2x = xstart+xinc*height;;
			p2y = p1y+yinc*height;
			p3x =  p2x+width-1;
			p3y = p2y- yinc2*width;
		}
		
		if(showGrayscale) {
			int v;
			int[] column = new int[255];
			for(int row=0; row<255; row++) {
				if(whiteBackground)
					v = row;
				else
					v = 255-row;
				column[row] = v;
			}
			int base = ipProfile.getHeight()-255;		
			for(int col=0; col<width; col++) {
				ipProfile.putColumn(col, base-(int)(yinc2*col+0.5), column, 255);
			}
		} else {
			ipProfile.setValue(254);
			ipProfile.fill();
		}
		
		ipProfile.snapshot();
		
		ImageProcessor ip2 = new ByteProcessor(windowWidth, windowHeight);		
		if(showGrayscale) {
			ip2.setColorModel(ip.getColorModel());
			if(invertedLut)
				ip2.invertLut();	
			fixLut(ip2);
		}		
		if(!blackFill)
			ip2.setValue(255);
		else
			ip2.setValue(0);
		ip2.fill();
		
		for (int row=0; row<height; row++) {
			double[] profile = ip.getLine(0, row, width-1, row);
			clearAboveProfile(ipProfile, profile, width, yinc2);
			int ixstart = (int)(xstart+0.5);
			int iystart = (int)(ystart+0.5);
			
			ip2.copyBits(ipProfile, ixstart, iystart-ybase, Blitter.COPY_TRANSPARENT);
			ipProfile.reset();

			if (showWireframe) {
				ip2.setValue(0);
				double ydelta = 0.0;
				ip2.moveTo(ixstart, (int)(ystart+255.5 - profile[0]) );
				for(int i=1; i<width; i++) {
					ydelta += yinc2;
					ip2.lineTo( ixstart+i,  (int) (ystart+255.5-(profile[i]+ydelta)));
				}
				ip2.drawLine(ixstart, iystart+255, ixstart + width-1, (int)( ystart+255.5-ydelta) );
				ip2.drawLine( ixstart, iystart+255-(int) (profile[0]+0.5), ixstart, iystart+255 );
				ip2.drawLine( ixstart+width-1, (int) ( ystart+255.5-ydelta),  ixstart+width-1, (int) (ystart+255.5-(profile[width-1]+ydelta)) );
			}
			
			xstart += xinc;
			ystart += yinc;
			if ((row%10)==0) IJ.showProgress((double)row/height);
		}

		IJ.showProgress(1.0);		
		
		if(invertedLut) {
			ip.invert();
			ip.invertLut();
		}
		if(whiteBackground)
			ip.invert();
			
		if (showAxis) {
			if (!lut.isGrayscale() && showGrayscale)
				ip2 = ip2.convertToRGB();			
			drawAndLabelAxis(ip, ip2, roi);
		}

		if (img.getStackSize()==1)
			ip2 = trimPlot(ip2, ybase);
				
		return ip2;
	}

	void drawAndLabelAxis(ImageProcessor ip, ImageProcessor ip2, Rectangle roi) {			
		ip2.setFont(new Font("SansSerif", Font.PLAIN, fontSize));
		if(!blackFill)	
			ip2.setColor(Color.black);
		else
			ip2.setColor(Color.white);			
		ip2.setAntialiasedText(true);
		String s;
		int w, h;
		Calibration cal = img.getCalibration();

		//z-axis & label
		s = cal.getValueUnit();
		if (s.equals("Gray Value"))
			s = "";
		w =  ip2.getFontMetrics().stringWidth(s);
		drawAxis(ip2, (int) p1x, (int) p1y-255, (int) p1x, (int) p1y , s, 10, -1, 0, 1);
		double min, max;
		if (img.getBitDepth()==8) {
			min = 0;
			max = 255;
		} else {
			min = img.getProcessor().getMin();
			max = img.getProcessor().getMax();
		}
		//IJ.log("");
		//IJ.log(min+"  "+max+"  "+cal.getCValue((int)min)+"  "+cal.getCValue((int)max));
		//ip2.putPixelValue(0,0,0);
		//boolean zeroIsBlack
		//IJ.log(ip2.getPixelValue(+"  "+max+"  "+cal.getCValue((int)min)+"  "+cal.getCValue((int)max));
		if (cal.calibrated()) {
			min = cal.getCValue((int)min);
			max = cal.getCValue((int)max);
		}
		//if (invertedPixelValues)
		//	{double t=max; max=min; min=t;}
		ip2.setAntialiasedText(true);
		s = String.valueOf( (double) Math.round(max*10)/10);
		w =  ip.getFontMetrics().stringWidth(s);
		h =  ip.getFontMetrics().getHeight();
		ip2.drawString(s, (int) p1x-18-w, (int) p1y-255 +h/2);	//ybase+5+h+(int ( yinc2/xinc *10));
		s = String.valueOf( (double) Math.round(min*10)/10);
		w =  ip2.getFontMetrics().stringWidth(s);
		ip2.drawString(s, (int) p1x-18-w, (int) p1y +h/2);
		
		//x-axis
		boolean unitsMatch = cal.getXUnit().equals(cal.getYUnit());
		String xunits = unitsMatch ? cal.getUnits() : cal.getYUnit(); // why swapped?
		s = (double) Math.round(roi.height*cal.pixelHeight*10)/10+" "+xunits;
		w =  ip2.getFontMetrics().stringWidth(s);
		drawAxis(ip2, (int) p1x, (int) p1y, (int) p2x, (int) p2y, s, 10, -1, 1, 1);

		//y-axis
		String yunits = unitsMatch ? cal.getUnits() : cal.getXUnit(); // why swapped?
		s = (double) Math.round(roi.width*cal.pixelWidth*10)/10+" "+yunits;
		w =  ip2.getFontMetrics().stringWidth(s);
		drawAxis(ip2, (int) p2x, (int) p2y, (int) p3x, (int) p3y, s, 10, 1, -1, 1);

	}

	void drawAxis(ImageProcessor ip, int x1, int y1, int x2, int y2, String label, int offset, int offsetXDirection, int offsetYDirection, int labelSide){
		if(blackFill)
			ip.setColor(Color.white);
		else
			ip.setColor(Color.black);		
		
		double m = -(double) (y2-y1)/(double) (x2-x1);
		
		if(m==0)
			m=.0001;
		double mTangent = -1/m;
		double theta = Math.atan(mTangent);
				
		int dy = -offsetXDirection * (int) ( 7*Math.sin(theta) );
		int dx = -offsetXDirection * (int) ( 7*Math.cos(theta) );
		
		x1 += offsetXDirection * (int) ( offset*Math.cos(theta) );
		x2 += offsetXDirection * (int) ( offset*Math.cos(theta) );
		
		y1 += offsetYDirection * (int) ( offset*Math.sin(theta) );
		y2 += offsetYDirection * (int) ( offset*Math.sin(theta) );		
		
		ip.drawLine(x1, y1, x2, y2);
		
		ip.drawLine(x1, y1, x1+dx, y1-dy);
		ip.drawLine(x2, y2, x2+dx, y2-dy);
		ImageProcessor ipText = drawString( ip, label, (int) (Math.atan(m)/2/Math.PI*360) );
		if(blackFill)
			ipText.invert();
				
		Blitter b;
		if (ip instanceof ByteProcessor)
			b = new ByteBlitter((ByteProcessor) ip);
		else
			b = new ColorBlitter((ColorProcessor) ip);
		Color c = blackFill?Color.black:Color.white;
		b.setTransparentColor(c);
		int xloc = (x1+x2)/2-ipText.getWidth()/2  + offsetXDirection*labelSide*(int)(15*Math.cos(theta));
		int yloc = (y1+y2)/2-ipText.getHeight()/2 + offsetYDirection*labelSide*(int)(15*Math.sin(theta));
		b.copyBits(ipText, xloc, yloc, Blitter.COPY_TRANSPARENT);
				
		return;						
	}	
	
	ImageProcessor drawString(ImageProcessor ip, String s, int a){
		int w =  ip.getFontMetrics().stringWidth(s);
		int h =  ip.getFontMetrics().getHeight();
		int ipW, ipH;
		
		double r = Math.sqrt( (w/2)*(w/2) + (h/2)*(h/2) );
		double aR = (a/360.0)*2.0*Math.PI;
		double aBaseR = Math.acos( (w/2)/r );
		
		ipW = (int) Math.abs(r*Math.cos(aBaseR+aR));
		ipH = (int) Math.abs(r*Math.sin(aBaseR+aR));
		
		if((int) Math.abs(r*Math.cos(-aBaseR+aR))>ipW)
			ipW = (int) Math.abs(r*Math.cos(-aBaseR+aR));
		if((int) Math.abs(r*Math.sin(-aBaseR+aR))>ipH)
			ipH = (int) Math.abs(r*Math.sin(-aBaseR+aR));
		
		ipW *= 2;
		ipH *= 2;
		
		int tW = w;
		if(ipW>w)
			tW = ipW;
		ImageProcessor ipText = new ByteProcessor(tW, ipH);
		ipText.setFont(new Font("SansSerif", Font.PLAIN, fontSize));
		ipText.setColor(Color.white);
		ipText.fill();
		ipText.setColor(Color.black);
		ipText.setAntialiasedText(true);
		ipText.drawString(s, tW/2-w/2, ipH/2+h/2);
		ipText.setInterpolate(true);
		ipText.rotate(-a);
		ipText.setRoi(tW/2-ipW/2, 0, ipW, ipH);
		ipText = ipText.crop();
		
		//new ImagePlus("test", ipText).show();
		//ip.copyBits(ipText, x, y, Blitter.COPY_TRANSPARENT);

		return ipText;
	}
	
	void clearAboveProfile(ImageProcessor ipProfile, double[] profile, int width, double yinc2) {
		byte[] pixels = (byte[])ipProfile.getPixels();
		double ydelta = 0.0;
		int height = ipProfile.getHeight();
		for(int x=0; x<width; x++) {
			ydelta += yinc2;			
			int top = height - (int)(profile[x]+ydelta);
			for (int y=0,index=x; y<top; y++, index+=width)
				pixels[index] = (byte)255;
		}					
	}

	ImageProcessor trimPlot(ImageProcessor plot, int maxTrim) {
		int background = plot.getPixel(0, 0);
		int width = plot.getWidth();
		int height = plot.getHeight();
		int trim = maxTrim-5;
		a: for (int y=0; y<(maxTrim-5); y++)
			for (int x=0; x<width; x++)
				if (plot.getPixel(x,y)!=background)
					{trim = y-5; break a;}
		if (trim>10) {
			plot.setRoi(0, trim, width, height-trim);
			plot = plot.crop();
		}
		return plot;		
	}


	void fixLut(ImageProcessor ip) {
		if(!lut.isGrayscale() && lut.getMapSize() == 256){
			
			for(int y=0;y<ip.getHeight();y++){
				for(int x=0;x<ip.getWidth();x++){
					if(ip.getPixelValue(x, y)==0){
						ip.putPixelValue(x, y, 1);
					}else if(ip.getPixelValue(x, y)==255){
						ip.putPixelValue(x, y, 254);
					}
			
				}
			}
			
			byte[] rLUT = lut.getReds();			//new byte[256];
			byte[] gLUT = lut.getGreens();			//new byte[256];
			byte[] bLUT = lut.getBlues();			//new byte[256];
			
			rLUT[0] = (byte)0;
			gLUT[0] = (byte)0;
			bLUT[0] = (byte)0;
			rLUT[255] = (byte)255;
			gLUT[255] = (byte)255;
			bLUT[255] = (byte)255;
			
			ip.setColorModel(new IndexColorModel(8, 256, rLUT, gLUT, bLUT));
			
		}	
	}
	

}
