package graphicsDomain;

import java.awt.*;
import java.awt.geom.*;
import java.awt.font.*;
import java.text.*;
import java.util.*;

// Encapsulates methods to draw axes and tick marks and labels
public abstract class DrawAxisTicks
{
    private Font tickFont;
    protected double textFac; //(ascent+leading)/(text-height)
    private double maxNormalRange = 10; //  (Do not use sci notation inside
    private double minNormalRange = 1; //   of normal range)
    private int axisLen; //Length in pixels of the axis

//protected variables, will be used by subclasses
    protected double
        currentTickVal, //Current tick value (either X and Y)
        stringWidth, //Witdh of the current tick label
        stringHeight; //Height of the current tick label

    protected int
        borderSize, //Border size in pixels
        xSize, //Graph panel width
        ySize, //Graph panel height
        tickPix, //Tick size in pixels
        axisLabelHeight, //height of the current label
        currentTickPixel; //Pixel position of the current tick

    protected String tickLabel; //The label associated with the current tick

//Nice formater for axis numbers
    private NumberFormat tickFormatter = NumberFormat.getNumberInstance();

    public DrawAxisTicks() {}

//Draw generic axis ticks. The method will call specific methods
//implemented by subclasses for each axis

    public void draw(GraphMetrics metrics,  Graphics2D g2D)
    {
        g2D.setColor(metrics.getGraphOptions().
           selectColor(metrics.getGraphOptions().getLineColor()));
        g2D.setStroke(metrics.getGraphOptions().getAxisStroke());

        //Get information from GraphOptionsInGraphics instance
        tickFont = metrics.getGraphOptions().getTickFont();
        textFac = metrics.getGraphOptions().getTextFac();
        maxNormalRange = metrics.getGraphOptions().getMaxNormalRange();
        minNormalRange = metrics.getGraphOptions().getMinNormalRange();
        int sigFigs = metrics.getGraphOptions().getSigFigs();
        tickPix = metrics.getGraphOptions().getTickPix();
        xSize = metrics.getXSize();
        ySize = metrics.getYSize();
        borderSize = metrics.getBorderSize();
        double[] maxMinLen = getMaxMinLen(metrics);
        double dataMin = maxMinLen[0];
        double dataMax = maxMinLen[1];
        axisLen = (int) maxMinLen[2];

        // set font and find FontRenderContext for label dimensions
        g2D.setFont(tickFont);
        FontRenderContext context = g2D.getFontRenderContext();

        String dummyTickL = "."; // compute width of dummy tick label
        for (int i = 0; i < sigFigs; i++)
        {
            dummyTickL = dummyTickL + "W";
        }
        Rectangle2D bounds = tickFont.getStringBounds(dummyTickL, context);
        int textWidth = (int) bounds.getWidth();

        //find number of ticks
        int numTicks = (int) ( (double) (axisLen - 2 * borderSize) /
                              (double) textWidth);
        if (numTicks < 4) numTicks = 4;
        int nBin = numTicks - 1;

        double dataRange, d;
        if (metrics.getGraphOptions().getNiceNumbers())
        {
            dataRange = nicenum(dataMax - dataMin, true);
            d = nicenum(dataRange / (nBin), false);
        }
        else
        {
            dataRange = dataMax - dataMin;
            d = dataRange / nBin;
        }

        // tick range should be smaller than the data range
        double tickMin = Math.ceil(dataMin / d) * d;
        double tickMax = Math.floor(dataMax / d) * d;

        // Fill in an arraylist of tick values
        ArrayList tickPoints = new ArrayList();
        for (double t = tickMin; t <= tickMax + 0.5 * d; t = t + d)
        {
            tickPoints.add(new Double(t));
        }

        Double dummyMinTick = (Double) tickPoints.get(0); //first
        double minTick = dummyMinTick.doubleValue();
        Double dummyMaxTick = (Double) tickPoints.get(tickPoints.size() - 1); //last
        double maxTick = dummyMaxTick.doubleValue();

        // Determine whether to use scientific notation; find multiplier and sciLabel
        double maxAbsTick = Math.max(Math.abs(maxTick), Math.abs(minTick));
        int powerOfTen = 0;
        double multiplier = 1;
        boolean scientific = false;
        String sciLabel = "";
        if (maxAbsTick >= maxNormalRange || maxAbsTick < minNormalRange)
        {
            if(maxAbsTick == 0)
                powerOfTen = 1;
            else
                powerOfTen = (int) Math.floor(Math.log10(maxAbsTick));
            scientific = true;
            sciLabel = "  (x10^" + powerOfTen + ")";
            multiplier = Math.pow(10, powerOfTen);
        }
        for (int i = 0; i < tickPoints.size(); i++)
        {
            Double currVal = (Double) tickPoints.get(i);
            currentTickVal = currVal.doubleValue();

            // draw little tick mark and grid line. This is done by subclass method
            currentTickPixel = drawTickGrid(g2D, metrics);

            // Find tick label
            tickLabel = "";
            double tickNumber = 0.;
            if (scientific)
            {
                tickNumber = currentTickVal / multiplier;
            } // Divide by exponent to get number
            else
            {
                tickNumber = currentTickVal;
            }
            tickLabel = tickFormatter.format(tickNumber);

            // Calculate tick label width and height
            bounds = tickFont.getStringBounds(tickLabel, context);
            stringWidth = bounds.getWidth();
            stringHeight = bounds.getHeight();
            // draw tick label. Will be carried out by subclass method.
            drawTickLabel(g2D);
        }
        //The label specifying the tick values multipliers needs to be appended to
        //either X or Y labels stored in the GraphDataInGraphics instance.
        //This is done in the concrete subclasses.
        setSciLabel(metrics, sciLabel);
    }

    //Get maximum, minumum avlues and size in pixel (returned in a double array)
    //for either X or Y axis
    public abstract double[] getMaxMinLen(GraphMetrics metrics);

    //Draw the current line in the tick grid (either X or Y), based on the current tick value
    public abstract int drawTickGrid(Graphics2D g2D, GraphMetrics metrics);

    //Draw the current tick label (either X or Y)
    public abstract void drawTickLabel(Graphics2D g2D);

    //Store x or y scientific label suffix in metrics
    public abstract void setSciLabel(GraphMetrics metrics, String label);

    /**
        Converts a given number into a 'nice number'
        @param x The number to convert
        @param round True for rounded to closest; false for rounded up
        @return The nice number near x
     */
    protected double nicenum(double x, boolean round)
    {
        int exp;
        double f, nf;

        if (x <= 0.)
        {
            System.out.println("Illegal value passed to nicenum: " + x);
            System.exit(0);
        }

        exp = (int) Math.floor(Math.log10(x));
        f = x / Math.pow(10.0, exp);
        if (round) // round to closest nice number
        {
            if (f < 1.5)
            {
                nf = 1.0;
            }
            else
            {
                if (f < 3.0)
                {
                    nf = 2.0;
                }
                else
                {
                    if (f < 7.0)
                    {
                        nf = 5.0;
                    }
                    else
                    {
                        nf = 10.0;
                    }
                }
            }
        }
        else // round up to nice number
        {
            if (f <= 1.0)
            {
                nf = 1.0;
            }
            else
            {
                if (f <= 2.0)
                {
                    nf = 2.0;
                }
                else
                {
                    if (f <= 5.0)
                    {
                        nf = 5.0;
                    }
                    else
                    {
                        nf = 10.0;
                    }
                }
            }
        }
        return nf * Math.pow(10.0, exp);
    }
}
