package vismon.util;

import java.awt.*;
import java.util.Formatter;

public class TickMarkCalculator {

    private static final double eps = 1e-10;
    public static final int HOR = 0;
    public static final int VER = 1;
    private static final double MIN_VALUE = -1.0e20;

    private static double[] Q = new double[]{1, 5, 2, 2.5, 4, 3};
    private static double[] w = new double[]{0.25, 0.2, 0.5, 0.05};
    private static int[]    fonts = new int[]{8, 9, 10, 11, 12, 13, 14, 15, 16, 17};
    private static int      tickGapPixels = 4;
    private static int[]    pxs = new int[fonts.length];
    //private static int[]    pxs = new int[]{8, 11, 14, 15, 18, 21, 27, 30, 36};
    //private static String[] units = new String[]{"", "thousands", "", "K", "", "M", "", ""};

    private static boolean loose = false;

    static {
        for (int i = 0; i < pxs.length; ++i)
            pxs[i] = tickGapPixels;
    }

    /*
        calculates the number and size of tick marks
        dmin: data min value
        dmax: data max value
        m: number of preferred tick marks
        ft: preferred font size
        width: size of axis
     */
    public static Object[] getTickMarkValues(double dmin, double dmax, int m, int ft, int width, /*int horVer, */int verticalRoom, Graphics2D g) {
        Object[] output = new Object[6];

        if (dmax - dmin < eps) {
            output[0] = dmin;
            output[1] = dmax;
            output[2] = (dmax - dmin) / m;
            return output;
        }

        int n = Q.length;

        double bestLmin = 0;
        double bestLmax = -1;
        double bestLstep = 0;
        double bestScore = -2;

        int bestFormat = 0;
        int bestFontSize = 0;
        int bestOrienation = 0;

        boolean terminate = false;
        for (int j = 1; j < Integer.MAX_VALUE; ++j) {
            for (double q : Q) {
                print("\tq = " + q);
                double sm = simplicityMax(q, j);

                if (calWeight(sm, 1, 1, 1) < bestScore) {
                    terminate = true;
                    print("----------------------------------------\n");
                    break;
                }

                //loop over tick counts
                for (int k = 2; k < Integer.MAX_VALUE; ++k) {
                    print("\t\tk = " + k);
                    double dm = densityMax(k, m);

                    if (calWeight(sm, dm, 1, 1) < bestScore) {
                        print("----------------------------------------\n");
                        break;
                    }

                    double delta = (dmax-dmin) / (k+1) / j / q;
                    for (int z = (int)(Math.ceil(Math.log10(delta))); z < Integer.MAX_VALUE; ++z) {
                        print("\t\t\tz = " + z);
                        double step = j * q * Math.pow(10, z);

                        double cm = coverageMax(dmin, dmax, step*(k-1));
                        if (calWeight(sm, dm, cm, 1) < bestScore) {
                            print("----------------------------------------\n");
                            break;
                        }

                        int minStart = (int) Math.floor(dmax/step) * j - (k-1) * j;
                        int maxStart = (int) Math.ceil(dmin/step) * j;

                        if (minStart > maxStart)
                            continue;

                        for (int start = minStart; start <= maxStart; ++start) {
                            print("\t\t\t\tstart = " + start);
                            double lmin = start * (step / j);
                            double lmax = lmin + step * (k-1);
                            double lstep = step;

                            int lpixel = calRoom(lmin, lmax, dmin, dmax, width, k);

                            double s = simplicity(q, j, lmin, lmax, lstep);
                            double c = coverage(dmin, dmax, lmin, lmax);
                            double d = density(k, m, dmin, dmax, lmin, lmax);
                            Object leg[] = legibility(lmin, lmax, lstep, k, ft, lpixel, /*horVer, */verticalRoom, g);
                            double l = (Double) leg[0];

                            double score = calWeight(s, d, c, l);
                            print(s + " * " + w[0] + " + " + d + " * " + w[1] + " + " + c + " * " + w[2] + " + " + l + " * " + w[3]);
                            print("-->> score = " + score + "");

                            if (bestScore < score && (loose || (lmin <= dmin && lmax >= dmax))) {
                                print("bestScore changing....");
                                print("lmin = " + lmin);
                                print("lmax = " + lmax);
                                print("lstep = " + lstep);
                                bestLmin = lmin;
                                bestLmax = lmax;
                                bestLstep = lstep;
                                bestScore = score;

                                bestFormat = (Integer) leg[1];
                                bestFontSize = (Integer) leg[2];
                                bestOrienation = (Integer) leg[3];
                            }
                        }
                    }
                }
            }
            print("----------------------------------------\n");
            if (terminate)
                break;
        }

        output[0] = bestLmin;
        output[1] = bestLmax;
        output[2] = bestLstep;
        output[3] = bestFormat;
        output[4] = bestFontSize;
        output[5] = bestOrienation;
        return output;
    }

    private static double calWeight(double s, double d, double c, double l) {
        return s*w[0] + d*w[1] + c*w[2] + l*w[3];
    }

    private static int calRoom(double lmin, double lmax, double dmin, double dmax, int width, int tickNum) {
        int room = 0;
        if (lmin <= dmin && dmax <= lmax)
            return (int)((double) width / (tickNum-1));
        else
            return (int)((width * (lmax - lmin) / (dmax - dmin)) / (tickNum-1));
    }

    private static double simplicity(double q, int j, double lmin, double lmax, double lstep) {
        int n = Q.length;
        int i = indexOfQ(q);

        int v = 0;
        if (lmin % lstep < eps || (lstep - (lmin % lstep) < eps) && lmin <= 0 && lmax >= 0)
            v = 1;

        return (1 - (double) (i-1)/(n-1) - j + v);
    }

    private static double simplicityMax(double q, int j) {
        int n = Q.length;
        int i = indexOfQ(q);
        int v = 1;

        return (1 - (double) (i-1)/(n-1) - j + v);
    }

    private static double coverage(double dmin, double dmax, double lmin, double lmax) {
        double range = dmax - dmin;

        return (1 - 50 * ((dmax-lmax)*(dmax-lmax) + (dmin-lmin)*(dmin-lmin)) / (range*range));
    }

    private static double coverageMax(double dmin, double dmax, double span) {
        double range = dmax - dmin;

        if (span > range) {
            double half = 0.5 * (span - range);
            return (1 - 100 * (half*half) / (range*range));
        }

        return 1;
    }

    private static double density(int k, int m, double dmin, double dmax, double lmin, double lmax) {
        double r = (k - 1) / (lmax - lmin);
        double rt = (m - 1) / (Math.max(lmax, dmax) - Math.min(lmin, dmin));

        return (2 - Math.max(r/rt, rt/r));
    }

    private static double densityMax(int k, int m) {
        if (k >= m)
            return 2 - (double)(k-1)/(m-1);

        return 1;
    }

    private static Object[] legibility(double lmin, double lmax, double lstep, int k, int ft, int lpixel, /*int horVer, */int verticalRoom, Graphics2D g) {
        int v = 0;
        if (lmin % lstep < eps || (lstep - (lmin % lstep) < eps) && lmin <= 0 && lmax >= 0)
            v = 1;

        double bestLegibility = MIN_VALUE;//-2;
        int bestFormat = -1;
        int bestFontSize = -1;
        int bestOrientation = -1;

        double format = 0;
        double fontSize = 0;
        double orientation = 0;
        double overlap = 1e20;
        double outOfVerticalRoom = 0;

        print2("#################################################");
        for (int fmt = 0; fmt < 4; ++fmt) {
            print("\t\t\t\t\t > fmt = " + fmt);
            for (double s = lmin; s <= lmax; s += lstep)
                format += format(fmt, s);

            format = 0.9 * (1.0/k * format) + 0.1 * v;

            for (int findex = 0; findex < fonts.length; ++findex) {
                print2("font size = " + fonts[findex]);
                print("\t\t\t\t\t\t > fontSize = " + fonts[findex]);
                if (fonts[findex] == ft)
                    fontSize = 1;
                else if (fonts[0] <= fonts[findex] && fonts[findex] < ft)
                    fontSize = 0.2 * (fonts[findex] - fonts[0] + 1) / (ft - fonts[0]);
                else
                    fontSize = MIN_VALUE;

                g.setFont(Constants.getPlotFont(fonts[findex]));
                for (int o = 0; o <= 1; ++o) {
                    //if (o == (HOR == horVer ? 0 : 1)) {   //horizontal
                    if (o == HOR) {
                        print("\t\t\t\t\t\t\t > HOR");
                        orientation = 1;

                        // CHECKING IF THE TICKMARKS OVERLAP
                        double labelOverlap = 0;
                        for (int i = 0; i < k-1; ++i) {
                            double label = lmin + i * lstep;
                            double nextLabel = lmin + (i+1) * lstep;

                            String labelFormatted = labelFormatter(label, fmt);
                            String nextLabelFormatted = labelFormatter(nextLabel, fmt);

                            int lwidth = g.getFontMetrics().stringWidth(labelFormatted);
                            int nlwidth = g.getFontMetrics().stringWidth(nextLabelFormatted);

                            int distance = lpixel - (int) (0.5 * (lwidth + nlwidth));
                            print("\t\t\t\t\t\t\t\tlpixel = " + lpixel);
                            print("\t\t\t\t\t\t\t\t<" + labelFormatted + "> -> " + lwidth);
                            print("\t\t\t\t\t\t\t\t<" + nextLabelFormatted + "> -> " + nlwidth);
                            print("\t\t\t\t\t\t\t\tdistance = " + distance);
                            //double labelOverlap;
                            if (distance >= pxs[findex])
                                labelOverlap += 1;
                            else if (0 < distance && distance < pxs[findex])
                                labelOverlap += 2 - (double) pxs[findex] / distance;
                            else
                                labelOverlap += MIN_VALUE;
                        }
                        labelOverlap /= k-1;
                        overlap = labelOverlap;
                        print("\t\t\t\t\t\t\t\toverlap = " + overlap);

                        // CHECKING IF THE TICKMARKS GO BEYOND THE AVAILABLE VERTICAL ROOM
                        int height = g.getFontMetrics().getHeight();
                        if (height < verticalRoom)
                            outOfVerticalRoom = 1;
                        else
                            outOfVerticalRoom = MIN_VALUE;

                        double leg = 0.2 * (format + fontSize + orientation + overlap + outOfVerticalRoom);
                        print2("HOR ---------> leg = " + leg);
                        print("0.2 * (" + format + " + " + fontSize + " + " + orientation + " + " + overlap + " + " + outOfVerticalRoom + ")");
                        //print("-->> leg = " + leg);
                        if (bestLegibility < leg) {
                            print2("bestLegibility changing...");
                            bestLegibility = leg;
                            bestFormat = fmt;
                            bestFontSize = fonts[findex];
                            bestOrientation = o;
                        }
                    } else {        //vertical
                        print("\t\t\t\t\t\t\t > VER");
                        orientation = -0.5;

                        int lheight = g.getFontMetrics().getHeight();

                        int distance = lpixel - lheight;

                        // CHECKING IF THE TICKMARKS OVERLAP
                        double labelOverlap;
                        if (distance >= pxs[findex])
                            labelOverlap = 1;
                        else if (0 < distance && distance < pxs[findex])
                            labelOverlap = 2 - (double) pxs[findex] / distance;
                        else
                            labelOverlap = MIN_VALUE;

                        overlap = labelOverlap;
                        print("\t\t\t\t\t\t\t\toverlap = " + overlap);

                        // CHECKING IF THE TICKMARKS GO OUT OF THE AVAILABLE VERTICAL ROOM
                        double labelOutOfVerticalRoom = 0;
                        for (int i = 0; i < k; ++i) {
                            double label = lmin + i * lstep;

                            String labelFormatted = labelFormatter(label, fmt);
                            int lwidth = g.getFontMetrics().stringWidth(labelFormatted);
                            print2(labelFormatted);
                            print2("\t" + lwidth);

                            if (lwidth < verticalRoom)
                                labelOutOfVerticalRoom += 1;
                            else
                                labelOutOfVerticalRoom += MIN_VALUE;
                        }

                        outOfVerticalRoom = labelOutOfVerticalRoom / k;

                        double leg = 0.2 * (format + fontSize + orientation + overlap + outOfVerticalRoom);
                        print2("VER ---------> leg = " + leg);
                        print("0.2 * (" + format + " + " + fontSize + " + " + orientation + " + " + overlap + " + " + outOfVerticalRoom + ")");
                        //print("-->> leg = " + leg);
                        if (bestLegibility < leg) {
                            print2("bestLegibility changing...");
                            bestLegibility = leg;
                            bestFormat = fmt;
                            bestFontSize = fonts[findex];
                            bestOrientation = o;
                        }
                    }
                }
            }

            format = 0;
            fontSize = 0;
            orientation = 0;
            overlap = 0;
            outOfVerticalRoom = 0;
        }

        print2("bestLeg = " + bestLegibility);

        Object[] output = new Object[4];
        output[0] = bestLegibility;
        output[1] = bestFormat;
        output[2] = bestFontSize;
        output[3] = bestOrientation;
        print("#################################################");
        print("bestLegibility = " + bestLegibility);
        print("bestFormat = " + bestFormat);
        print("bestFontSize = " + bestFontSize);
        print("bestOrientation = " + bestOrientation);

        return output;

    }

    private static double format(int f, double l) {
        if (l == 0)
            return 1;

        switch (f) {
            case 0:
                if ((-1e6 < l && l < -1e-4) || (1e-4 < l && l < 1e6))
                    return 1;
                break;
            case 1:
                return 0.65;
            case 2:
                if ((-1e6 < l && l < -1e3) || (1e3 < l && l < 1e6))
                    return 0.65;
                break;
            case 3:
                if ((-1e6 < l && l < -1e3) || (1e3 < l && l < 1e6))
                    return 0.25;
                break;
            /*case 4:
                if ((-1e9 < l && l < -1e6) || (1e6 < l && l < 1e9))
                    return 0.75;
                break;
            case 5:
                if ((-1e9 < l && l < -1e6) || (1e6 < l && l < 1e9))
                    return 0.4;
                break;
            case 6:
                return 0.25;
            case 7:
                return 0.3;*/
        }

        return -1;
    }

    /*private static double format(int f, double l) {
        if (l == 0)
            return 1;

        switch (f) {
            case 0:
                if ((-1e6 < l && l < -1e-4) || (1e-4 < l && l < 1e6))
                    return 1;
                break;
            case 1:
                return 0.5;
            case 2:
                if ((-1e6 < l && l < -1e3) || (1e3 < l && l < 1e6))
                    return 0.75;
                break;
            case 3:
                if ((-1e6 < l && l < -1e3) || (1e3 < l && l < 1e6))
                    return 0.4;
                break;
            case 4:
                if ((-1e9 < l && l < -1e6) || (1e6 < l && l < 1e9))
                    return 0.75;
                break;
            case 5:
                if ((-1e9 < l && l < -1e6) || (1e6 < l && l < 1e9))
                    return 0.4;
                break;
            case 6:
                return 0.25;
            case 7:
                return 0.3;
        }

        return -1;
    }*/

    private static int indexOfQ(double q) {
        for (int p = 0; p < Q.length; ++p)
            if (Q[p] == q)
                return p;
        return -1;
    }



    private static final int[] tens = {1, 10, 100, 1000, 10000, 100000, 1000000};
    private static String format(double d, int precision) {
        int round = (int)(d * tens[precision] + 0.5);
        while (precision > 0 && round % 10 == 0) {
            --precision;
            round /= 10;
        }
        double dd = (double)round / tens[precision];
        return new Formatter().format("%." + precision + "f", dd).toString();
    }

    public static String labelFormatter(double label, int f) {
        if (label == 0)
            return "0";

        switch (f) {
            case 0:
                if (Util.equals(label % 1.0, 0))
                    return (int) label + "";
                return format(label, 3) + "";
            case 1:
                double l1 = (int)(label * 1.0e3) * 1.0e-6;
                if (Util.equals(l1 % 1.0, 0))
                    return (int) l1 + "K";
                return format(l1, 3) + "K";
                //return label/1000.0 + "";
            /*case 2:
                double l2 = (int) (label * 1.0e3) * 1.0e-6;
                if (Util.equals(l2 % 1.0, 0))
                    return (int) l2 + "K";
                return format(l2, 3) + "K";
                //return label/1000.0 + "K";
            case 3:
                double l3 = (int) (label * 1.0e3) * 1.0e-6;
                if (Util.equals(l3 % 1.0, 0))
                    return (int) l3 + "K";
                return format(l3, 3) + "K";
                //return label/1000.0 + "";*/
            case 2:
                double l4 = (int) (label * 1.0e6) * 1.0e-9;
                if (Util.equals(l4 % 1.0, 0))
                    return (int) l4 + "M";
                return format(l4, 3) + "M";
                //return label/1000000.0 + "M";
            /*case 5:
                double l5 = (int) (label * 1.0e6) * 1.0e-9;
                if (Util.equals(l5 % 1.0, 0))
                    return (int) l5 + "M";
                return format(l5, 3) + "M";
                //return label/1000000.0 + "";*/
            case 3:
                int power = (int) (Math.log(label) / Math.log(10));
                if (power < 0)
                    --power;
                double fraction = label / Math.pow(10, power);
                String sign = "";
                fraction = Math.round(fraction*100) * 0.01;
                if (power > 0)
                    sign = "+";
                return format(fraction, 2) + "e" + sign + power;
            case 7:
                power = (int)(Math.log(label) / Math.log(10));
                if (power < 0)
                    --power;
                fraction = label / Math.pow(10, power);
                return format(fraction, 2);
            //return (Math.round(fraction*100) * 0.01) + "";
        }

        return "-1";
    }

    /*public static String labelFormatter(double label, int f) {
        if (label == 0)
            return "0";

        switch (f) {
            case 0:
                if (Util.equals(label % 1.0, 0))
                    return (int) label + "";
                return format(label, 3) + "";
            case 1:
                double l1 = (int)(label * 1.0e3) * 1.0e-6;
                if (Util.equals(l1 % 1.0, 0))
                    return (int) l1 + "";
                return format(l1, 3);
                //return label/1000.0 + "";
            case 2:
                double l2 = (int) (label * 1.0e3) * 1.0e-6;
                if (Util.equals(l2 % 1.0, 0))
                    return (int) l2 + "K";
                return format(l2, 3) + "K";
                //return label/1000.0 + "K";
            case 3:
                double l3 = (int) (label * 1.0e3) * 1.0e-6;
                if (Util.equals(l3 % 1.0, 0))
                    return (int) l3 + "";
                return format(l3, 3) + "";
                //return label/1000.0 + "";
            case 4:
                double l4 = (int) (label * 1.0e6) * 1.0e-9;
                if (Util.equals(l4 % 1.0, 0))
                    return (int) l4 + "M";
                return format(l4, 3) + "M";
                //return label/1000000.0 + "M";
            case 5:
                double l5 = (int) (label * 1.0e6) * 1.0e-9;
                if (Util.equals(l5 % 1.0, 0))
                    return (int) l5 + "";
                return format(l5, 3) + "";
                //return label/1000000.0 + "";
            case 6:
                int power = (int) (Math.log(label) / Math.log(10));
                if (power < 0)
                    --power;
                double fraction = label / Math.pow(10, power);
                String sign = "";
                fraction = Math.round(fraction*100) * 0.01;
                if (power > 0)
                    sign = "+";
                return format(fraction, 2) + "e" + sign + power;
            case 7:
                power = (int)(Math.log(label) / Math.log(10));
                if (power < 0)
                    --power;
                fraction = label / Math.pow(10, power);
                return format(fraction, 2);
            //return (Math.round(fraction*100) * 0.01) + "";
        }

        return "-1";
    }*/

    private static void print(String s) {
        //System.out.println(s);
    }

    private static void print2(String s) {
        //System.out.println(s);
    }
}
