package ij.gui;

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;
import ij.*;
import ij.process.*;
import ij.util.*;
import ij.plugin.filter.Analyzer;
import ij.macro.Interpreter;


/** This class is an image that line graphs can be drawn on. */
public class Plot {

    /** Display points using a circle 5 pixels in diameter. */
    public static final int CIRCLE = 0;
    /** Display points using an X-shaped mark. */
    public static final int X = 1;
    /** Display points using an box-shaped mark. */
    public static final int BOX = 3;
    /** Display points using an tiangular mark. */
    public static final int TRIANGLE = 4;
    /** Display points using an cross-shaped mark. */
    public static final int CROSS = 5;
    /** Display points using a single pixel. */
    public static final int DOT = 6;
    /** Connect points with solid lines. */
    public static final int LINE = 2;
    
    private static final int LEFT_MARGIN = 50;
    private static final int RIGHT_MARGIN = 20;
    private static final int TOP_MARGIN = 20;
    private static final int BOTTOM_MARGIN = 30;
    private static final int WIDTH = 450;
    private static final int HEIGHT = 200;
    
    private int frameWidth;
    private int frameHeight;
    private int xloc;
    private int yloc;
    
    Rectangle frame = null;
    float[] xValues, yValues;
    float[] errorBars;
    int nPoints;
    double xMin, xMax, yMin, yMax;
        
    private double xScale, yScale;
    private Button list, save, copy;
    private Label coordinates;
    private static String defaultDirectory = null;
    private String xLabel;
    private String yLabel;
    private Font font = new Font("Helvetica", Font.PLAIN, 12);
    private boolean fixedYScale;
    private static int options;
    private int lineWidth = 1; // Line.getWidth();
    private boolean realNumbers;
    private int markSize = 5;
    private ImageProcessor ip;
    private String title;
    private boolean initialized;
    private boolean plotDrawn;
    private int plotWidth = PlotWindow.plotWidth;
    private int plotHeight = PlotWindow.plotHeight;
    private boolean multiplePlots;
    private boolean drawPending;
        
    /** Construct a new PlotWindow.
    * @param title          the window title
    * @param xLabel         the x-axis label
    * @param yLabel         the y-axis label
    * @param xValues        the x-coodinates, or null
    * @param yValues        the y-coodinates, or null
    */
    public Plot(String title, String xLabel, String yLabel, float[] xValues, float[] yValues) {
        this.title = title;
        this.xLabel = xLabel;
        this.yLabel = yLabel;
        if (xValues==null || yValues==null) {
            xValues = new float[1];
            yValues = new float[1];
            xValues[0] = -1f;
            yValues[0] = -1f;
        }
        this.xValues = xValues;
        this.yValues = yValues;
        double[] a = Tools.getMinMax(xValues);
        xMin=a[0]; xMax=a[1];
        a = Tools.getMinMax(yValues);
        yMin=a[0]; yMax=a[1];
        fixedYScale = false;
        nPoints = xValues.length;
        drawPending = true;
    }

    /** This version of the constructor excepts double arrays. */
    public Plot(String title, String xLabel, String yLabel, double[] xValues, double[] yValues) {
        this(title, xLabel, yLabel, xValues!=null?Tools.toFloat(xValues):null, yValues!=null?Tools.toFloat(yValues):null);
    }
    
    /** Sets the x-axis and y-axis range. */
    public void setLimits(double xMin, double xMax, double yMin, double yMax) {
        this.xMin = xMin;
        this.xMax = xMax;
        this.yMin = yMin;
        this.yMax = yMax;
        fixedYScale = true;
        if (initialized) setScale();
    }

    /** Adds a set of points to the plot or adds a curve if shape is set to LINE.
    * @param x          the x-coodinates
    * @param y          the y-coodinates
    * @param shape      CIRCLE, X, BOX, TRIANGLE, CROSS, DOT or LINE
    */
    public void addPoints(float[] x, float[] y, int shape) {
        setup();
        switch(shape) {
            case CIRCLE: case X:  case BOX: case TRIANGLE: case CROSS: case DOT:
                for (int i=0; i<x.length; i++) {
                    int xt = LEFT_MARGIN + (int)((x[i]-xMin)*xScale);
                    int yt = TOP_MARGIN + frameHeight - (int)((y[i]-yMin)*yScale);
                    if (xt>=frame.x && yt>=frame.y && xt<=frame.x+frame.width && yt<=frame.y+frame.height)
                        drawShape(shape, xt, yt, markSize);
                }   
                break;
            case LINE:
                ip.setClipRect(frame);                  
                int xts[] = new int[x.length];
                int yts[] = new int[y.length];
                for (int i=0; i<x.length; i++) {
                    xts[i] = LEFT_MARGIN + (int)((x[i]-xMin)*xScale);
                    yts[i] = TOP_MARGIN + frameHeight - (int)((y[i]-yMin)*yScale);
                }
                drawPolyline(ip, xts, yts, x.length);
                ip.setClipRect(null);                   
                break;
        }
        multiplePlots = true;
        if (xValues.length==1) {
            xValues = x;
            yValues = y;
            nPoints = x.length;
            drawPending = false;
        }
    }

    /** Adds a set of points to the plot using double arrays.
        Must be called before the plot is displayed. */
    public void addPoints(double[] x, double[] y, int shape) {
        addPoints(Tools.toFloat(x), Tools.toFloat(y), shape);
    }

    void drawShape(int shape, int x, int y, int size) {
        int xbase = x-size/2;
        int ybase = y-size/2;
        switch(shape) {
            case X:
                ip.drawLine(xbase,ybase,xbase+size,ybase+size);
                ip.drawLine(xbase+size,ybase,xbase,ybase+size);
                break;
            case BOX:
                ip.drawLine(xbase,ybase,xbase+size,ybase);
                ip.drawLine(xbase+size,ybase,xbase+size,ybase+size);
                ip.drawLine(xbase+size,ybase+size,xbase,ybase+size);
                ip.drawLine(xbase,ybase+size,xbase,ybase);
                break;
            case TRIANGLE:
                ip.drawLine(x,ybase,xbase+size,ybase+size);
                ip.drawLine(xbase+size,ybase+size,xbase,ybase+size);
                ip.drawLine(xbase,ybase+size,x,ybase);
                break;
            case CROSS:
                ip.drawLine(xbase,y,xbase+size,y);
                ip.drawLine(x,ybase,x,ybase+size);
                break;
            case DOT:
                ip.drawDot(x, y);
                break;
            default: // 5x5 oval
                ip.drawLine(x-1, y-2, x+1, y-2);
                ip.drawLine(x-1, y+2, x+1, y+2);
                ip.drawLine(x+2, y+1, x+2, y-1);
                ip.drawLine(x-2, y+1, x-2, y-1);
                break;
        }
    }
    
    /** Adds error bars to the plot. */
    public void addErrorBars(float[] errorBars) {
        if (errorBars.length!=nPoints)
            throw new IllegalArgumentException("errorBars.length != npoints");
        this.errorBars = errorBars  ;
    }

    /** Adds error bars to the plot. */
    public void addErrorBars(double[] errorBars) {
        addErrorBars(Tools.toFloat(errorBars));
    }

    /** Draws text at the specified location, where (0,0)
        is the upper left corner of the the plot frame and (1,1) is
        the lower right corner. */
    public void addLabel(double x, double y, String label) {
        setup();
        int xt = LEFT_MARGIN + (int)(x*frameWidth);
        int yt = TOP_MARGIN + (int)(y*frameHeight);
        ip.drawString(label, xt, yt);
    }
    
    /* Draws text at the specified location, using the coordinate system defined
        by setLimits() and the justification specified by setJustification(). */
//  public void addText(String text, double x, double y) {
//      setup();
//      int xt = LEFT_MARGIN + (int)((x-xMin)*xScale);
//      int yt = TOP_MARGIN + frameHeight - (int)((y-yMin)*yScale);
//      if (justification==CENTER)
//          xt -= ip.getStringWidth(text)/2;
//      else if (justification==RIGHT)
//          xt -= ip.getStringWidth(text);
//      ip.drawString(text, xt, yt);
//  }
    
    /** Sets the justification used  by addLabel(), where <code>justification</code>
        is ImageProcessor.LEFT, ImageProcessor.CENTER or ImageProcessor.RIGHT. */
    public void setJustification(int justification) {
        setup();
        ip.setJustification(justification);
    }

    /** Changes the drawing color. The frame and labels are
        always drawn in black. */
    public void setColor(Color c) {
        setup();
        if (!(ip instanceof ColorProcessor)) {
            ip = ip.convertToRGB();
            ip.setLineWidth(lineWidth);
            ip.setFont(font);
            ip.setAntialiasedText(true);
        }
        ip.setColor(c);
    }

    /** Changes the line width. */
    public void setLineWidth(int lineWidth) {
        if (lineWidth<1) lineWidth = 1;
        setup();
        ip.setLineWidth(lineWidth);
        this.lineWidth = lineWidth;
        markSize = lineWidth==1?5:7;
    }

    /** Changes the font. */
    public void changeFont(Font font) {
        setup();
        ip.setFont(font);
        this.font = font;
    }

    void setup() {
        if (initialized)
            return;
        initialized = true;
        createImage();
        ip.setColor(Color.black);
        if (lineWidth>3)
            lineWidth = 3;
        ip.setLineWidth(lineWidth);
        ip.setFont(font);
        ip.setAntialiasedText(true);
        if (frameWidth==0) {
            frameWidth = plotWidth;
            frameHeight = plotHeight;
        }
        frame = new Rectangle(LEFT_MARGIN, TOP_MARGIN, frameWidth, frameHeight);
        setScale();
    }

    void setScale() {
        if ((xMax-xMin)==0.0)
            xScale = 1.0;
        else
            xScale = frame.width/(xMax-xMin);
        if ((yMax-yMin)==0.0)
            yScale = 1.0;
        else
            yScale = frame.height/(yMax-yMin);
    }

    void createImage() {
        if (ip!=null)
            return;
        int width = plotWidth+LEFT_MARGIN+RIGHT_MARGIN;
        int height = plotHeight+TOP_MARGIN+BOTTOM_MARGIN;
        byte[] pixels = new byte[width*height];
        for (int i=0; i<width*height; i++)
            pixels[i] = (byte)255;
        ip = new ByteProcessor(width, height, pixels, null);
    }

    int getDigits(double n1, double n2) {
        if (Math.round(n1)==n1 && Math.round(n2)==n2)
            return 0;
        else {
            n1 = Math.abs(n1);
            n2 = Math.abs(n2);
            double n = n1<n2&&n1>0.0?n1:n2;
            double diff = Math.abs(n2-n1);
            if (diff>0.0 && diff<n) n = diff;
            int digits = 1;
            if (n<10.0) digits = 2;
            if (n<0.01) digits = 3;
            if (n<0.001) digits = 4;
            if (n<0.0001) digits = 5;       
            return digits;
        }
    }
    
    /** Draws the plot specified in the constructor. */
    public void draw() {
        int x, y;
        double v;
        
        if (plotDrawn)
            return;
        plotDrawn = true;
        createImage();
        setup();
        
        if (drawPending) {
            ip.setClipRect(frame);                  
            int xpoints[] = new int[nPoints];
            int ypoints[] = new int[nPoints];
            for (int i=0; i<nPoints; i++) {
                xpoints[i] = LEFT_MARGIN + (int)((xValues[i]-xMin)*xScale);
                ypoints[i] = TOP_MARGIN + frame.height - (int)((yValues[i]-yMin)*yScale);
            }
            drawPolyline(ip, xpoints, ypoints, nPoints); 
            ip.setClipRect(null);   
            if (this.errorBars != null) {
                xpoints = new int[2];
                ypoints = new int[2];
                for (int i=0; i<nPoints; i++) {
                    xpoints[0] = xpoints[1] = LEFT_MARGIN + (int)((xValues[i]-xMin)*xScale);
                    ypoints[0] = TOP_MARGIN + frame.height - (int)((yValues[i]-yMin-errorBars[i])*yScale);
                    ypoints[1] = TOP_MARGIN + frame.height - (int)((yValues[i]-yMin+errorBars[i])*yScale);
                    drawPolyline(ip, xpoints,ypoints, 2);
                }       
            }
        }               

        if (ip instanceof ColorProcessor)
            ip.setColor(Color.black);
        ip.drawRect(frame.x, frame.y, frame.width+1, frame.height+1);
        int digits = getDigits(yMin, yMax);
        String s = IJ.d2s(yMax, digits);
        int sw = ip.getStringWidth(s);
        if ((sw+4)>LEFT_MARGIN)
            ip.drawString(s, 4, TOP_MARGIN-4);
        else
            ip.drawString(s, LEFT_MARGIN-ip.getStringWidth(s)-4, TOP_MARGIN+10);
        s = IJ.d2s(yMin, digits);
        sw = ip.getStringWidth(s);
        if ((sw+4)>LEFT_MARGIN)
            ip.drawString(s, 4, TOP_MARGIN+frame.height);
        else
            ip.drawString(s, LEFT_MARGIN-ip.getStringWidth(s)-4, TOP_MARGIN+frame.height);
        FontMetrics fm = ip.getFontMetrics();
        x = LEFT_MARGIN;
        y = TOP_MARGIN + frame.height + fm.getAscent() + 6;
        digits = getDigits(xMin, xMax);
        ip.drawString(IJ.d2s(xMin,digits), x, y);
        s = IJ.d2s(xMax,digits);
        ip.drawString(s, x + frame.width-ip.getStringWidth(s)+6, y);
        ip.drawString(xLabel, LEFT_MARGIN+(frame.width-ip.getStringWidth(xLabel))/2, y+3);
        drawYLabel(yLabel,LEFT_MARGIN,TOP_MARGIN,frame.height, fm);
    }
    
    void drawPolyline(ImageProcessor ip, int[] x, int[] y, int n) {
        ip.moveTo(x[0], y[0]);
        for (int i=0; i<n; i++)
            ip.lineTo(x[i], y[i]);
    }
        
    void drawYLabel(String yLabel, int x, int y, int height, FontMetrics fm) {
        if (yLabel.equals(""))
            return;
        int w =  fm.stringWidth(yLabel) + 5;
        int h =  fm.getHeight() + 5;
        ImageProcessor label = new ByteProcessor(w, h);
        label.setColor(Color.white);
        label.fill();
        label.setColor(Color.black);
        label.setFont(font);
        label.setAntialiasedText(true);
        int descent = fm.getDescent();
        label.drawString(yLabel, 0, h-descent);
        label = label.rotateLeft();
        int y2 = y+(height-ip.getStringWidth(yLabel))/2;
        if (y2<y) y2 = y;
        int x2 = x-h-2;
        //new ImagePlus("after", label).show();
        ip.insert(label, x2, y2);
    }
    
    ImageProcessor getBlankProcessor() {
        createImage();
        return ip;
    }

    String getCoordinates(int x, int y) {
        String text = "";
        if (!frame.contains(x, y))
            return text;
        if (fixedYScale || multiplePlots) { // display cursor location
            double xv = (x-LEFT_MARGIN)/xScale + xMin;
            double yv = (TOP_MARGIN+frameHeight-y)/yScale +yMin;
            text =  "X=" + IJ.d2s(xv,getDigits(xv,xv))+", Y=" + IJ.d2s(yv,getDigits(yv,yv));
        } else { // display x and f(x)
            int index = (int)((x-frame.x)/((double)frame.width/nPoints));
            if (index>0 && index<nPoints) {
                double xv = xValues[index];
                double yv = yValues[index];
                text = "X=" + IJ.d2s(xv,getDigits(xv,xv))+", Y=" + IJ.d2s(yv,getDigits(yv,yv));
            }
        }
        return text;
    }

    /** Returns the plot as an ImageProcessor. */
    public ImageProcessor getProcessor() {
        draw();
        return ip;
    }
    
    /** Returns the plot as an ImagePlus. */
    public ImagePlus getImagePlus() {
        draw();
        return new ImagePlus(title, ip);
    }

    /** Displays the plot in a PlotWindow and returns a reference to the PlotWindow. */
    public PlotWindow show() {
        draw();
        if (Prefs.useInvertingLut && (ip instanceof ByteProcessor) && !Interpreter.isBatchMode() && IJ.getInstance()!=null) {
            ip.invertLut();
            ip.invert();
        }
        if ((IJ.macroRunning() && IJ.getInstance()==null) || Interpreter.isBatchMode()) {
            ImagePlus imp = new ImagePlus(title, ip);
            WindowManager.setTempCurrentImage(imp);
            Interpreter.addBatchModeImage(imp);
            return null;
        }
        return new PlotWindow(this);
    }
    
}