package edu.csusb.danby.util;

import javax.swing.*;
import java.awt.Graphics;
import java.util.Vector;
import java.awt.Color;
import java.awt.Font;
import java.text.NumberFormat;

/**
 * Panel with appropriately labeled and scaled axes. To be used
 * as a foundation class for graphing
 *
 * @version    Fri Jul 26 09:11:00 PDT 2002
 * @author     Charles S. Stanton
 */
public class AxesPanel extends JPanel {

    
    

    // viewport for graph, with default values
    protected double xRangeLow = -10.0;
    protected double xRangeHigh = 10.0;
    protected double yRangeLow = -10.0;
    protected double yRangeHigh = 10.0;
    
    //current panel width & height
    private int panelWidth, panelHeight;
    //panel dimensions minus offsetts
    private int graphWidth, graphHeight;

    //offsets for borders and axes
    private int leftOffset = 28;
    private int rightOffset = 18;
    private int topOffset = 18;
    private int bottomOffset = 28;
    private int tickDivisions = 5;

    private boolean drawVerticalAtEdges = true;
    private boolean drawHorizontalAtEdges = true;
    // variables used to determine number of decimal places to show
    private double minDataScaled, maxDataScaled;
    private double minScaleValue, maxScaleValue;
    private NumberFormat nf = NumberFormat.getNumberInstance();
    private int xMaxFractionDigits = 2;
    private int yMaxFractionDigits = 2;

    private Color scaleColor = Color.blue;
    private Color fontColor = Color.black;

    //font for labeling axes
    private Font axesFont = new Font("Serif", Font.PLAIN, 12);
    
    final static double log10 = Math.log(10.0);
    final static double log80 = Math.log(80.0) / log10;


    /**
     * constructor for a given viewport
     *
     * @param  axRangeLow   the left boundary
     * @param  axRangeHigh  the right boundary
     * @param  ayRangeLow   the bottom boundary
     * @param  ayRangeHigh  the top boundary
     */
    public AxesPanel(double axRangeLow, double axRangeHigh, double ayRangeLow, double ayRangeHigh) {
        xRangeLow = axRangeLow;
        xRangeHigh = axRangeHigh;
        yRangeLow = ayRangeLow;
        yRangeHigh = ayRangeHigh;
        setBackground(Color.white);
        xMaxFractionDigits = setMaxFractionDigits(xRangeHigh, xRangeLow);
        yMaxFractionDigits = setMaxFractionDigits(yRangeHigh, yRangeLow);
    }


    /**
     * Null argument constructor with a default viewport
     */
    public AxesPanel() {
        this(0, 10, 0, 10);
    }


    /*
     *  Below come the public accessor methods
     */

    /**
     * setViewport sets the ranges of doubles to be plotted
     *
     * @param  axRangeLow   the left boundary
     * @param  axRangeHigh  the right boundary
     * @param  ayRangeLow   the bottom boundary
     * @param  ayRangeHigh  the top boundary
     */
    public void setViewport(double axRangeLow, double axRangeHigh,
            double ayRangeLow, double ayRangeHigh) {
        xRangeLow = axRangeLow;
        xRangeHigh = axRangeHigh;
        yRangeLow = ayRangeLow;
        yRangeHigh = ayRangeHigh;
        xMaxFractionDigits = setMaxFractionDigits(xRangeHigh, xRangeLow);
        yMaxFractionDigits = setMaxFractionDigits(yRangeHigh, yRangeLow);
    }



    /**
     * sets the color of the axes scales
     *
     * @param  color  the desired color
     */
    public void setScaleColor(Color color) {
        scaleColor = color;
    }


    /**
     * sets the color of the fonts
     *
     * @param  color  the desired color
     */
    public void setFontColor(Color color) {
        fontColor = color;
    }


    /**
     *  Sets the TickDivisions attribute of the AxesPanel object
     *
     * @param  value  The new TickDivisions value
     */
    public void setTickDivisions(int value) {
        tickDivisions = value;
    }



    /**
     * Determine whether to draw vertical scale at edges or at x=0.
     *
     * @param  value  The new DrawVerticalScaleAtEdges value
     */
    public void setDrawVerticalScaleAtEdges(boolean value) {
        drawVerticalAtEdges = value;
    }


    /**
     *  Determine whether to draw horizontal scale at edges or at y=0.
     *
     * @param  value  The new DrawHorizontalScaleAtEdges value
     */
    public void setDrawHorizontalScaleAtEdges(boolean value) {
        drawHorizontalAtEdges = value;
    }


    /**
     *  Sets the horizontal bounds of the panel
     *
     * @param  xLow   The new left bound
     * @param  xHigh  The new right bound
     */
    public void setXRange(double xLow, double xHigh) {
        xRangeLow = xLow;
        xRangeHigh = xHigh;
    }


    /**
     *  Sets the veritical bounds of the panel
     *
     * @param  yLow   The new lower bound
     * @param  yHigh  The new upper bound
     */
    public void setYRange(double yLow, double yHigh) {
        yRangeLow = yLow;
        yRangeHigh = yHigh;
    }


    /**
     *  Sets the number of digits to show on the horizontal axis
     *
     * @param  n  The new XMaxFractionDigits value
     */
    public void setXMaxFractionDigits(int n) {
        xMaxFractionDigits = n;
    }


    /**
     *  Sets the number of digits to show on the vertical axis
     *
     * @param  n  The new YMaxFractionDigits value
     */
    public void setYMaxFractionDigits(int n) {
        yMaxFractionDigits = n;
    }


    /**
     *  Sets the left offset
     *
     * @param  n  The new left offset
     */
    public void setLeftOffset(int n) {
        leftOffset = n;
    }


    /**
     *  Sets the right offset
     *
     * @param  n  The new right offset
     */
    public void setRightOffset(int n) {
        rightOffset = n;
    }


    /**
     *  Sets the top offset
     *
     * @param  n  The new top offset
     */
    public void setTopOffset(int n) {
        topOffset = n;
    }


    /**
     *  Sets the bottom offset
     *
     * @param  n  The new bottom offset
     */
    public void setBottomOffset(int n) {
        bottomOffset = n;
    }


    /**
     * paint method of panel
     *
     * @param  g  Description of Parameter
     */
    public void paint(Graphics g) {
        super.paint(g);
        //paint background and such
        //Recalculate width & height in case user has
        //changed window size
        panelWidth = getWidth();
        panelHeight = getHeight();
        graphWidth = panelWidth - (leftOffset + rightOffset);
        graphHeight = panelHeight - (topOffset + bottomOffset);
        g.setColor(scaleColor);
        /*
         *  attempt to draw axes at specified locations
         */
        if (drawHorizontalAtEdges) {
            //Draw at bottom edge
            paintHorizontalScale(g, yRangeLow);
        }
        /*
         *  if drawHorizontalAtEdges is false, attempt to paint axes at  y=0
         *  if these are in range. Otherwise draw at bottom edge
         */else {
            if ((yRangeLow < 0) && (yRangeHigh > 0)) {
                paintHorizontalScale(g, 0);
            } else {
                paintHorizontalScale(g, yRangeLow);
            }
        }

        if (drawVerticalAtEdges) {
            //Draw at left edge
            paintVerticalScale(g, xRangeLow);
        }
        /*
         *  if drawVerticalAtEdges is false, attempt to paint axes at x=0
         *  if these are in range. Otherwise draw at left edge.
         */else {

            if ((xRangeLow < 0) && (xRangeHigh > 0)) {
                paintVerticalScale(g, 0);
            } else {
                paintVerticalScale(g, xRangeLow);
            }
        }

    }


    /*
     *  scaling methods
     */
    protected int scaleX(double x) {
        int nx;

        nx = (int) ((x - xRangeLow) * graphWidth / (xRangeHigh - xRangeLow) + leftOffset);
        return nx;
    }


    protected double inverseScaleX(double ix) {
        double x;
        x = (ix - leftOffset) * (xRangeHigh - xRangeLow) / graphWidth + xRangeLow;
        return x;
    }


    protected int scaleY(double y) {
        int ny;

        ny = (int) (graphHeight - ((y - yRangeLow) * graphHeight / (yRangeHigh - yRangeLow)) + topOffset);
        return ny;
    }


    protected double inverseScaleY(double iy) {
        double y;
        double gh = (double) graphHeight;
        y = (yRangeHigh - yRangeLow) * (graphHeight - iy + topOffset) / gh + yRangeLow;
        return y;
    }


//try to determine a reasonable number of decimal places
//to show in labels

    private int setMaxFractionDigits(double max, double min) {
        double spreadData;
        double logSpreadData;
        int maxFractionDigits;
        long spreadPower;

        spreadData = max - min;
        logSpreadData = Math.log(spreadData) / log10;
        spreadPower = Math.round(Math.floor(logSpreadData));
        maxFractionDigits = (int) Math.max(0, 2 - 1 * spreadPower);
        return maxFractionDigits;
    }



    /*
     *  draw horiziontal scale at bottom
     *  of graph
     */
    private void paintHorizontalScale(Graphics g, double hScalePosition) {
        float[] grid = findGrid(xRangeLow, xRangeHigh, graphWidth);
        int scalePosition = scaleY(hScalePosition);
        double tickPos;
        int iTickPos;
        double tickSpace = (xRangeHigh - xRangeLow) / tickDivisions;
        String scaleLabel;

        g.setFont(axesFont);
        nf.setMaximumFractionDigits(xMaxFractionDigits);
        g.setColor(scaleColor);
        g.drawLine(leftOffset, scalePosition, leftOffset + graphWidth, scalePosition);
        for (int i = 0; i < grid.length; i++) {
            g.setColor(scaleColor);
            //tickPos = xRangeLow+i*tickSpace;
            iTickPos = scaleX(grid[i]);
            g.drawLine(iTickPos, scalePosition - 5, iTickPos, scalePosition + 5);
            g.setColor(fontColor);
            scaleLabel = nf.format(grid[i]);
            g.setColor(fontColor);
            g.drawString(scaleLabel, iTickPos - 3, scalePosition + 12);
        }
    }


    /*
     *  draw vertical scale at indicated position
     *
     */
    private void paintVerticalScale(Graphics g, double vScalePosition) {
        g.setFont(axesFont);
        float[] grid = findGrid(yRangeLow, yRangeHigh, graphHeight);
        int scalePosition = scaleX(vScalePosition);
        int iTickPos;
        String scaleLabel;

        nf.setMaximumFractionDigits(yMaxFractionDigits);
        g.setColor(scaleColor);
        g.drawLine(scalePosition, topOffset, scalePosition, panelHeight - bottomOffset);
        for (int i = 0; i < grid.length; i++) {
            g.setColor(scaleColor);
            iTickPos = scaleY(grid[i]);
            g.drawLine(scalePosition - 5, iTickPos, scalePosition + 5, iTickPos);
            scaleLabel = nf.format(grid[i]);
            g.setColor(fontColor);
            g.drawString(scaleLabel, scalePosition - 16, iTickPos - 4);
        }
    }


    /*
     *  Find good grid values for scales and grid lines
     *  @lowValue is the lower endpoint of the scale
     *  @highValue is the upper endpoint of the scale
     *  @graphPixels is the dimension of the scale in pixel space
     *  @return an array of floats to draw the grid at
     */
    private float[] findGrid(double lowValue, double highValue,
            int graphPixels) {
        int increment;
        int incrementFactor = 1;
        // increase increment for small graphs
        if (graphPixels < 180) {
            incrementFactor = 2;
        }
        if (graphPixels < 80) {
            incrementFactor = 5;
        }
        //first we scale so that there are between 8 and 80 integer values
        //in the graphing range
        int pow10 = (int) Math.floor(log80 - Math.log(highValue - lowValue) / log10);
        double scale10 = Math.exp(log10 * pow10);
        int iScaledLow = (int) (lowValue * scale10);
        int iScaledHigh = (int) (highValue * scale10);
        //Now we reduce the lines, keeping multiples of 2, 5, or 10
        int numberLines = iScaledHigh - iScaledLow;
        if (numberLines >= 50) {
            increment = 10 * incrementFactor;
        } else if (numberLines >= 20) {
            increment = 5 * incrementFactor;
        } else if (numberLines >= 10) {
            increment = 2 * incrementFactor;
        } else {
            increment = 1 * incrementFactor;
        }
        //adjust the starting value to be a multiple of the increment
        //We want a positive remainder, so the grid point is in the range
        //adjust the starting value to be a multiple of the increment
        //We want a positive remainder, so the grid point is in the range
        int iAdj = (iScaledLow < 0) ? iScaledLow % increment + increment : iScaledLow % increment;
        int initPosition = iScaledLow - iAdj;
        iAdj = (iScaledHigh < 0) ? iScaledHigh % increment : iScaledHigh % increment - increment;
        int finalPosition = iScaledHigh + iAdj;

        if ((highValue - initPosition) > 1.02 * (highValue - lowValue)) {
            initPosition = initPosition + increment;
        }

        if ((finalPosition - lowValue) > 1.02 * (highValue - lowValue)) {
            finalPosition = finalPosition - increment;
        }
        //recount the number of grid points
        //numberLines = (int)Math.max(1,(finalPosition -initPosition)+1);
        numberLines = 12;

        //now calculate the grid points
        int iPosition = initPosition;
        float[] grid = new float[numberLines];
        for (int i = 0; i < numberLines; i++) {
            grid[i] = (float) (iPosition / scale10);
            iPosition += increment;
        }
        return grid;
    }
}

