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));
            
        }   
    }
    

}