/**
 * $RCSfile  $
 * $Revision  $
 * $Date  $
 *
 * Copyright (C) 2008 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution, or a commercial license
 * agreement with Jive.
 */
package org.jivesoftware.openfire.reporting.graph;

import org.jivesoftware.openfire.reporting.stats.StatsViewer;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
import org.jfree.chart.axis.*;
import org.jfree.chart.encoders.KeypointPNGEncoderAdapter;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.plot.PiePlot;
import org.jfree.chart.renderer.xy.XYAreaRenderer;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.data.time.*;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.IntervalXYDataset;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.util.Rotation;
import org.jivesoftware.openfire.stats.Statistic;
import org.jivesoftware.util.JiveGlobals;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.NumberFormat;

/**
 * Builds graphs off of statistics tracked in the <i>StatsEngine</i>.
 *
 * @author Alexander Wenckus
 * @see StatsViewer
 */
public class GraphEngine {
    private StatsViewer statsViewer;

    private static final long YEAR = 31104000000L;

    private static final long MONTH = 2592000000L;

    private static final long WEEK = 604800000L;

    private static final long DAY = 86400000L;

    private TickUnits tickUnits;
    private Locale oldLocale;

    /**
     * Default constructor used by the plugin container to construct the graph engine.
     *
     * @param statsViewer The viewer provides an mechanism to view the data being tracked by the <i>StatsEngine</i>.
     */
    public GraphEngine(StatsViewer statsViewer) {
        this.statsViewer = statsViewer;
    }

    /**
     * Creates a graph in PNG format.  The PNG graph is encoded by the KeypointPNGEncoderAdapter
     * so that the resulting PNG is encoded with alpha transparency.
     *
     * @param key
     * @param width
     * @param height
     * @param startTime
     * @param endTime
     * @param dataPoints
     * @return
     * @throws IOException
     */
    public byte[] generateGraph(String key, int width, int height, String color, long startTime, long endTime,
                                int dataPoints) throws IOException
    {

        JFreeChart chart = generateChart(key, width, height, color, startTime, endTime,dataPoints);
        KeypointPNGEncoderAdapter encoder = new KeypointPNGEncoderAdapter();
        encoder.setEncodingAlpha(true);
        return encoder.encode(chart.createBufferedImage(width, height, BufferedImage.BITMASK, null));
    }

    /**
     * Creates a chart.
     *
     * @param key
     * @param width
     * @param height
     * @param startTime
     * @param endTime
     * @param dataPoints
     * @return
     * @throws IOException
     */
    public JFreeChart generateChart(String key, int width, int height, String color, long startTime, long endTime,
                                    int dataPoints) throws IOException
    {
        Statistic[] def = statsViewer.getStatistic(key);
        if (def == null) {
            return null;
        }

        XYDataset data = populateData(key, def, startTime, endTime, dataPoints);
        if (data == null) {
            return null;
        }
        JFreeChart chart;
        switch(def[0].getStatType()) {
            case count:
                chart = createTimeBarChart(null, color, def[0].getUnits(), data);
                break;
            default:
                chart = createTimeAreaChart(null, color, def[0].getUnits(), data);
        }

        return chart;
    }



    /**
     * Generates a Sparkline type graph. Sparkline graphs
     * are "intense, simple, wordlike graphics" so named by Edward Tufte. The big
     * difference between the graph produced by this method compared to the
     * graph produced by the <code>generateGraph</code> method is that this one
     * produces graphs with no x-axis and no y-axis and is usually smaller in size.
     * @param key
     * @param width
     * @param height
     * @param startTime
     * @param endTime
     * @param dataPoints
     * @return
     * @throws IOException
     */
    public byte[] generateSparklinesGraph(String key, int width, int height, String color, long startTime,
                                          long endTime, int dataPoints) throws IOException
    {
        Statistic[] def = statsViewer.getStatistic(key);
        if (def == null) {
            return null;
        }

        JFreeChart chart;
        switch (def[0].getStatType()) {
            case count:
                chart = generateSparklineBarGraph(key, color, def, startTime, endTime, dataPoints);
                break;
            default:
                chart = generateSparklineAreaChart(key, color, def, startTime, endTime, dataPoints);
        }

        KeypointPNGEncoderAdapter encoder = new KeypointPNGEncoderAdapter();
        encoder.setEncodingAlpha(true);
        return encoder.encode(chart.createBufferedImage(width, height, BufferedImage.BITMASK, null));
    }

    private XYDataset populateData(String key, Statistic[] def, long startTime, long endTime,
                                   int dataPoints)
    {
        double[][] values = statsViewer.getData(key, startTime, endTime, dataPoints);
        long timePeriod = endTime - startTime;
        TimeSeries[] series = new TimeSeries[values.length];
        TimeSeriesCollection dataSet = new TimeSeriesCollection();

        for (int d = 0; d < values.length; d++) {
            series[d] = new TimeSeries(def[d].getName(), getTimePeriodClass(timePeriod));
            Statistic.Type type = def[d].getStatType();

            long interval = timePeriod / values[d].length;
            for (int i = 0; i < values[d].length; i++) {
                series[d].addOrUpdate(
                        getTimePeriod(timePeriod, new Date(startTime + (i * interval)),
                                JiveGlobals.getTimeZone()), cleanData(type, values[d][i]));
            }
            dataSet.addSeries(series[d]);
        }
        return dataSet;
    }

    private Class<? extends RegularTimePeriod> getTimePeriodClass(long timePeriod) {
        if (timePeriod > 86400000) {
            return Day.class;
        } else if (timePeriod > 3600000) {
            return Hour.class;
        } else {
            return Minute.class;
        }
    }

    private RegularTimePeriod getTimePeriod(long timePeriod, Date date, TimeZone zone) {
        if (timePeriod > 86400000) {
            return new Day(date, zone);
        } else if (timePeriod > 3600000) {
            return new Hour(date, zone);
        } else {
            return new Minute(date, zone);
        }
    }


    /**
     * Round up a defined value.
     *
     * @param type  the type of Statistic.
     * @param value the value.
     * @return the rounded up value.
     */
    private double cleanData(Statistic.Type type, double value) {
        if(type == Statistic.Type.count) {
            return Math.round(value);
        }
        return value;
    }

    /**
     * Generates a generic Time Area Chart.
     *
     * @param title      the title of the Chart.
     * @param valueLabel the Y Axis label.
     * @param data       the data to populate with.
     * @return the generated Chart.
     */
    private JFreeChart createTimeAreaChart(String title, String color, String valueLabel, XYDataset data) {
        PlotOrientation orientation = PlotOrientation.VERTICAL;

        DateAxis xAxis = generateTimeAxis();

        NumberAxis yAxis = new NumberAxis(valueLabel);

        NumberFormat formatter = NumberFormat.getNumberInstance(JiveGlobals.getLocale());
        formatter.setMaximumFractionDigits(2);
        formatter.setMinimumFractionDigits(0);
        yAxis.setNumberFormatOverride(formatter);

        XYAreaRenderer renderer = new XYAreaRenderer(XYAreaRenderer.AREA);
        renderer.setOutline(true);

        return createChart(title, data, xAxis, yAxis, orientation, renderer,
                GraphDefinition.getDefinition(color));
    }

    /**
     * Generates a generic Time Bar Chart.
     *
     * @param title      the title of the Chart.
     * @param valueLabel the X Axis Label.
     * @param data       the data to populate with.
     * @return the generated Chart.
     */
    private JFreeChart createTimeBarChart(String title, String color, String valueLabel, XYDataset data) {
        PlotOrientation orientation = PlotOrientation.VERTICAL;
        DateAxis xAxis = generateTimeAxis();

        NumberAxis yAxis = new NumberAxis(valueLabel);
        NumberFormat formatter = NumberFormat.getNumberInstance(JiveGlobals.getLocale());
        formatter.setMaximumFractionDigits(2);
        formatter.setMinimumFractionDigits(0);
        yAxis.setNumberFormatOverride(formatter);
        yAxis.setAutoRangeIncludesZero(true);

        return createChart(title, data, xAxis, yAxis, orientation, new XYBarRenderer(),
                GraphDefinition.getDefinition(color));
    }

    /**
     * Generates a Chart.
     *
     * @param title        the title of the chart.
     * @param data         the data to use in the chart.
     * @param xAxis        the variables to use on the xAxis.
     * @param yAxis        the variables to use on the yAxis.
     * @param orientation  the orientation
     * @param itemRenderer the type of renderer to use.
     * @return the generated chart.
     */
    private JFreeChart createChart(String title, XYDataset data, ValueAxis xAxis, ValueAxis yAxis,
                                   PlotOrientation orientation, XYItemRenderer itemRenderer, GraphDefinition def)
    {
        int seriesCount = data.getSeriesCount();
        for(int i = 0; i < seriesCount; i++) {
            itemRenderer.setSeriesPaint(i, def.getInlineColor(i));
            itemRenderer.setSeriesOutlinePaint(i, def.getOutlineColor(i));
        }

        XYPlot plot = new XYPlot(data, xAxis, yAxis, null);
        plot.setOrientation(orientation);
        plot.setRenderer(itemRenderer);

        return createChart(title, plot);
    }

    private JFreeChart createChart(String title, XYPlot plot) {
        JFreeChart chart = new JFreeChart(title, JFreeChart.DEFAULT_TITLE_FONT, plot, false);
        chart.setBackgroundPaint(Color.white);
        return chart;
    }

    /**
     * Generates a simple Time Axis.
     *
     * @return the generated Time Axis.
     */
    private DateAxis generateTimeAxis() {
        DateAxis xAxis = new DateAxis("");
        xAxis.setLowerMargin(0.05);
        xAxis.setUpperMargin(0.02);
        xAxis.setLabel(null);
        xAxis.setTickLabelsVisible(true);
        xAxis.setTickMarksVisible(true);

        xAxis.setAxisLineVisible(true);
        xAxis.setNegativeArrowVisible(false);
        xAxis.setPositiveArrowVisible(false);
        xAxis.setVisible(true);
        xAxis.setTickMarkPosition(DateTickMarkPosition.MIDDLE);
        Locale locale = JiveGlobals.getLocale();
        // If the tick units have not yet been setup or the locale has changed
        if(tickUnits == null || !locale.equals(oldLocale)) {
            tickUnits = createTickUnits(locale, JiveGlobals.getTimeZone());
            oldLocale = locale;
        }
        xAxis.setStandardTickUnits(tickUnits);

        return xAxis;
    }

    private TickUnits createTickUnits(Locale locale, TimeZone zone) {
        TickUnits units = new TickUnits();

        // date formatters
        DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS", locale);
        DateFormat f2 = new SimpleDateFormat("HH:mm:ss", locale);
        DateFormat f3 = DateFormat.getTimeInstance(DateFormat.SHORT, locale);
        DateFormat f4 = DateFormat.getDateTimeInstance(DateFormat.SHORT,
                DateFormat.SHORT, locale);
        DateFormat f5 = new SimpleDateFormat("d-MMM", locale);
        DateFormat f6 = new SimpleDateFormat("MMM-yyyy", locale);
        DateFormat f7 = new SimpleDateFormat("yyyy", locale);

        // NOTE: timezone not needed on date formatters because dates have already been converted
        // to the appropriate timezone by the respective RegularTimePeriod (Minute, Hour, Day, etc)
        // see:
        //   http://www.jfree.org/jfreechart/api/gjdoc/org/jfree/data/time/Hour.html#Hour:Date:TimeZone
        //
        // If you do use a timezone on the formatters and the Jive TimeZone has been set to something
        // other than the system timezone, time specific charts will show incorrect values.
        /*
        f1.setTimeZone(zone);
        f2.setTimeZone(zone);
        f3.setTimeZone(zone);
        f4.setTimeZone(zone);
        f5.setTimeZone(zone);
        f6.setTimeZone(zone);
        f7.setTimeZone(zone);
        */

        // milliseconds
        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 1, f1));
        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 5, DateTickUnit.MILLISECOND, 1, f1));
        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 10, DateTickUnit.MILLISECOND, 1, f1));
        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 25, DateTickUnit.MILLISECOND, 5, f1));
        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 50, DateTickUnit.MILLISECOND, 10, f1));
        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 100, DateTickUnit.MILLISECOND, 10, f1));
        units.add(new DateTickUnit( DateTickUnit.MILLISECOND, 250, DateTickUnit.MILLISECOND, 10, f1));
        units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 500, DateTickUnit.MILLISECOND, 50, f1));

        // seconds
        units.add(new DateTickUnit(DateTickUnit.SECOND, 1, DateTickUnit.MILLISECOND, 50, f2));
        units.add(new DateTickUnit(DateTickUnit.SECOND, 5, DateTickUnit.SECOND, 1, f2));
        units.add(new DateTickUnit(DateTickUnit.SECOND, 10, DateTickUnit.SECOND, 1, f2));
        units.add(new DateTickUnit(DateTickUnit.SECOND, 30, DateTickUnit.SECOND, 5, f2));

        // minutes
        units.add(new DateTickUnit(DateTickUnit.MINUTE, 1, DateTickUnit.SECOND, 5, f3));
        units.add( new DateTickUnit(DateTickUnit.MINUTE, 2, DateTickUnit.SECOND, 10, f3));
        units.add(new DateTickUnit(DateTickUnit.MINUTE, 5, DateTickUnit.MINUTE, 1, f3));
        units.add(new DateTickUnit(DateTickUnit.MINUTE, 10, DateTickUnit.MINUTE, 1, f3));
        units.add(new DateTickUnit(DateTickUnit.MINUTE, 15, DateTickUnit.MINUTE, 5, f3));
        units.add(new DateTickUnit(DateTickUnit.MINUTE, 20, DateTickUnit.MINUTE, 5, f3));
        units.add(new DateTickUnit(DateTickUnit.MINUTE, 30, DateTickUnit.MINUTE, 5, f3));

        // hours
        units.add(new DateTickUnit(DateTickUnit.HOUR, 1, DateTickUnit.MINUTE, 5, f3));
        units.add(new DateTickUnit(DateTickUnit.HOUR, 2, DateTickUnit.MINUTE, 10, f3));
        units.add(new DateTickUnit(DateTickUnit.HOUR, 4, DateTickUnit.MINUTE, 30, f3) );
        units.add(new DateTickUnit(DateTickUnit.HOUR, 6, DateTickUnit.HOUR, 1, f3));
        units.add(new DateTickUnit(DateTickUnit.HOUR, 12, DateTickUnit.HOUR, 1, f4));

        // days
        units.add(new DateTickUnit(DateTickUnit.DAY, 1, DateTickUnit.HOUR, 1, f5));
        units.add(new DateTickUnit(DateTickUnit.DAY, 2, DateTickUnit.HOUR, 1, f5));
        units.add(new DateTickUnit(DateTickUnit.DAY, 7, DateTickUnit.DAY, 1, f5));
        units.add(new DateTickUnit(DateTickUnit.DAY, 15, DateTickUnit.DAY, 1, f5));

        // months
        units.add(new DateTickUnit(DateTickUnit.MONTH, 1, DateTickUnit.DAY, 1, f6));
        units.add(new DateTickUnit(DateTickUnit.MONTH, 2, DateTickUnit.DAY, 1, f6));
        units.add(new DateTickUnit(DateTickUnit.MONTH, 3, DateTickUnit.MONTH, 1, f6));
        units.add(new DateTickUnit(DateTickUnit.MONTH, 4, DateTickUnit.MONTH, 1, f6));
        units.add(new DateTickUnit(DateTickUnit.MONTH, 6, DateTickUnit.MONTH, 1, f6));

        // years
        units.add(new DateTickUnit(DateTickUnit.YEAR, 1, DateTickUnit.MONTH, 1, f7));
        units.add(new DateTickUnit(DateTickUnit.YEAR, 2, DateTickUnit.MONTH, 3, f7));
        units.add(new DateTickUnit(DateTickUnit.YEAR, 5, DateTickUnit.YEAR, 1, f7));
        units.add(new DateTickUnit(DateTickUnit.YEAR, 10, DateTickUnit.YEAR, 1, f7));
        units.add(new DateTickUnit(DateTickUnit.YEAR, 25, DateTickUnit.YEAR, 5, f7));
        units.add(new DateTickUnit(DateTickUnit.YEAR, 50, DateTickUnit.YEAR, 10, f7));
        units.add(new DateTickUnit(DateTickUnit.YEAR, 100, DateTickUnit.YEAR, 20, f7));

        return units;
    }

    /**
     * Generates a SparkLine Time Area Chart.
     * @param key
     * @param stats
     * @param startTime
     * @param endTime
     * @return chart
     */
    private JFreeChart generateSparklineAreaChart(String key, String color, Statistic [] stats, long startTime, long endTime, int dataPoints) {
        Color backgroundColor = getBackgroundColor();

        XYDataset dataset = populateData(key, stats, startTime, endTime, dataPoints);

        JFreeChart chart = ChartFactory.createXYAreaChart(
                null, // chart title
                null, // xaxis label
                null, // yaxis label
                dataset, // data
                PlotOrientation.VERTICAL,
                false, // include legend
                false, // tooltips?
                false // URLs?
        );

        chart.setBackgroundPaint(backgroundColor);
        chart.setBorderVisible(false);
        chart.setBorderPaint(null);

        XYPlot plot = (XYPlot)chart.getPlot();
        plot.setForegroundAlpha(1.0f);
        plot.setDomainGridlinesVisible(false);
        plot.setDomainCrosshairVisible(false);
        plot.setRangeCrosshairVisible(false);
        plot.setBackgroundPaint(backgroundColor);
        plot.setRangeGridlinesVisible(false);

        GraphDefinition graphDef = GraphDefinition.getDefinition(color);
        Color plotColor = graphDef.getInlineColor(0);
        plot.getRenderer().setSeriesPaint(0, plotColor);
        plot.getRenderer().setBaseItemLabelsVisible(false);
        plot.getRenderer().setBaseOutlinePaint(backgroundColor);
        plot.setOutlineStroke(null);
        plot.setDomainGridlinePaint(null);

        NumberAxis xAxis = (NumberAxis)chart.getXYPlot().getDomainAxis();

        xAxis.setLabel(null);
        xAxis.setTickLabelsVisible(true);
        xAxis.setTickMarksVisible(true);
        xAxis.setAxisLineVisible(false);
        xAxis.setNegativeArrowVisible(false);
        xAxis.setPositiveArrowVisible(false);
        xAxis.setVisible(false);

        NumberAxis yAxis = (NumberAxis)chart.getXYPlot().getRangeAxis();
        yAxis.setTickLabelsVisible(false);
        yAxis.setTickMarksVisible(false);
        yAxis.setAxisLineVisible(false);
        yAxis.setNegativeArrowVisible(false);
        yAxis.setPositiveArrowVisible(false);
        yAxis.setVisible(false);

        return chart;
    }

    /**
     * Creates a Pie Chart based on map.
     *
     * @return the Pie Chart generated.
     */
    public JFreeChart getPieChart(Map<String, Double> pieValues) {
        DefaultPieDataset dataset = new DefaultPieDataset();

        for (String key : pieValues.keySet()) {
            dataset.setValue(key, pieValues.get(key));
        }

        JFreeChart chart = ChartFactory.createPieChart3D(
                null,  // chart title
                dataset, // data
                true,    // include legend
                true,
                false
        );

        chart.setBackgroundPaint(Color.white);
        chart.setBorderVisible(false);
        chart.setBorderPaint(null);


        PiePlot plot = (PiePlot)chart.getPlot();
        plot.setSectionOutlinesVisible(false);
        plot.setLabelFont(new Font("SansSerif", Font.BOLD, 12));
        plot.setNoDataMessage("No data available");
        plot.setCircular(true);
        plot.setLabelGap(0.02);
        plot.setOutlinePaint(null);
        plot.setLabelLinksVisible(false);

        plot.setLabelGenerator(null);

        plot.setLegendLabelGenerator(new StandardPieSectionLabelGenerator("{0}"));


        plot.setStartAngle(270);
        plot.setDirection(Rotation.ANTICLOCKWISE);
        plot.setForegroundAlpha(0.60f);
        plot.setInteriorGap(0.33);

        return chart;
    }

    /**
     * Generates a Sparkline Bar Graph.
     *
     * @param def the key of the statistic object.
     * @return the generated chart.
     */
    public JFreeChart generateSparklineBarGraph(String key, String color, Statistic [] def, long startTime,
                                                long endTime, int dataPoints)
    {
        Color backgroundColor = getBackgroundColor();

        IntervalXYDataset dataset = (IntervalXYDataset) populateData(key, def, startTime, endTime, dataPoints);
        JFreeChart chart = ChartFactory.createXYBarChart(
                null, // chart title
                null, // domain axis label
                true,
                null, // range axis label
                dataset, // data
                PlotOrientation.VERTICAL,
                false, // include legend
                false, // tooltips?
                false // URLs?
        );

        chart.setBackgroundPaint(backgroundColor);
        chart.setBorderVisible(false);
        chart.setBorderPaint(null);

        XYPlot plot = (XYPlot)chart.getPlot();
        plot.setDomainGridlinesVisible(false);
        plot.setDomainCrosshairVisible(false);
        plot.setRangeCrosshairVisible(false);
        plot.setBackgroundPaint(backgroundColor);
        plot.setRangeGridlinesVisible(false);


        GraphDefinition graphDef = GraphDefinition.getDefinition(color);
        Color plotColor = graphDef.getInlineColor(0);
        plot.getRenderer().setSeriesPaint(0, plotColor);
        plot.getRenderer().setBaseItemLabelsVisible(false);
        plot.getRenderer().setBaseOutlinePaint(backgroundColor);
        plot.setOutlineStroke(null);
        plot.setDomainGridlinePaint(null);

        ValueAxis xAxis = chart.getXYPlot().getDomainAxis();

        xAxis.setLabel(null);
        xAxis.setTickLabelsVisible(true);
        xAxis.setTickMarksVisible(true);
        xAxis.setAxisLineVisible(false);
        xAxis.setNegativeArrowVisible(false);
        xAxis.setPositiveArrowVisible(false);
        xAxis.setVisible(false);

        ValueAxis yAxis = chart.getXYPlot().getRangeAxis();
        yAxis.setTickLabelsVisible(false);
        yAxis.setTickMarksVisible(false);
        yAxis.setAxisLineVisible(false);
        yAxis.setNegativeArrowVisible(false);
        yAxis.setPositiveArrowVisible(false);
        yAxis.setVisible(false);

        return chart;
    }

    /**
     * Takes a hexidecimel color value and returns its color equivelent.
     *
     * @param hexColor The hex color to be parsed
     * @return The java color object
     */
    private static Color getColor(String hexColor) {
        return new Color(Integer.valueOf(hexColor.substring(0, 2), 16),
                Integer.valueOf(hexColor.substring(2, 4), 16),
                Integer.valueOf(hexColor.substring(4, 6), 16));
    }

    /**
     * Returns a color that can be used as a background.
     * @return Color
     */
    private static Color getBackgroundColor() {
        return new Color(255,255,255);
    }

    public static long[] parseTimePeriod(String timeperiod) {

        if (null == timeperiod)
            timeperiod = "last60minutes";

        Date fromDate = null;
        Date toDate = null;
        long dataPoints = 60;

        Calendar cal = Calendar.getInstance();
        Date now = cal.getTime();
        // Reset the day fields so we're at the beginning of the day.
        cal.set(Calendar.HOUR, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        // Compute "this week" by resetting the day of the week to the first day of the week
        cal.set(Calendar.DAY_OF_WEEK, cal.getFirstDayOfWeek());
        Date thisWeekStart = cal.getTime();
        Date thisWeekEnd = now;
        // Compute last week - start with the end boundary which is 1 millisecond before the start of this week
        cal.add(Calendar.MILLISECOND, -1);
        Date lastWeekEnd = cal.getTime();
        // Add that millisecond back, subtract 7 days for the start boundary of "last week"
        cal.add(Calendar.MILLISECOND, 1);
        cal.add(Calendar.DAY_OF_YEAR, -7);
        Date lastWeekStart = cal.getTime();
        // Reset the time
        cal.setTime(now);
        cal.set(Calendar.HOUR, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        // Reset to the 1st day of the month, make the the start boundary for "this month"
        cal.set(Calendar.DAY_OF_MONTH, cal.getMinimum(Calendar.DAY_OF_MONTH));
        Date thisMonthStart = cal.getTime();
        Date thisMonthEnd = now;
        // Compute last month
        cal.add(Calendar.MILLISECOND, -1);
        Date lastMonthEnd = cal.getTime();
        cal.add(Calendar.MILLISECOND, 1);
        cal.add(Calendar.MONTH, -1);
        Date lastMonthStart = cal.getTime();
        // Compute last 3 months
        cal.setTime(now);
        cal.add(Calendar.MONTH, -2);
        cal.set(Calendar.HOUR, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        Date last3MonthsStart = cal.getTime();
        Date last3MonthsEnd = now;
        // Compute last 7 days:
        cal.setTime(now);
        cal.add(Calendar.DAY_OF_YEAR, -6);
        cal.set(Calendar.HOUR, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        Date last7DaysStart = cal.getTime();
        Date last7DaysEnd = now;
        // Compute last 60 minutes;
        cal.setTime(now);
        cal.add(Calendar.MINUTE, -60);
        Date last60MinutesStart = cal.getTime();
        Date last60MinutesEnd = now;
        // Compute last 24 hours;
        cal.setTime(now);
        cal.add(Calendar.HOUR, -23);
        Date last24HoursStart = cal.getTime();
        Date last24HoursEnd = now;
        // Done, reset the cal internal date to now
        cal.setTime(now);

        if ("thisweek".equals(timeperiod)) {
            fromDate = thisWeekStart;
            toDate = thisWeekEnd;
            dataPoints = 7;
        } else if ("last7days".equals(timeperiod)) {
            fromDate = last7DaysStart;
            toDate = last7DaysEnd;
            dataPoints = 7;
        } else if ("lastweek".equals(timeperiod)) {
            fromDate = lastWeekStart;
            toDate = lastWeekEnd;
            dataPoints = 7;
        } else if ("thismonth".equals(timeperiod)) {
            fromDate = thisMonthStart;
            toDate = thisMonthEnd;
            dataPoints = 30;
        } else if ("lastmonth".equals(timeperiod)) {
            fromDate = lastMonthStart;
            toDate = lastMonthEnd;
            dataPoints = 30;
        } else if ("last3months".equals(timeperiod)) {
            fromDate = last3MonthsStart;
            toDate = last3MonthsEnd;
            dataPoints = (long)Math.ceil((toDate.getTime() - fromDate.getTime()) / WEEK);
        } else if ("last60minutes".equals(timeperiod)) {
            fromDate = last60MinutesStart;
            toDate = last60MinutesEnd;
            dataPoints = 60;
        } else if ("last24hours".equals(timeperiod)) {
            fromDate = last24HoursStart;
            toDate = last24HoursEnd;
            dataPoints = 48;
        } else {
            String[] dates = timeperiod.split("to");
            if (dates.length > 0) {
                DateFormat formDateFormatter = new SimpleDateFormat("MM/dd/yy");
                String fromDateParam = dates[0];
                String toDateParam = dates[1];
                if (fromDateParam != null) {
                    try {
                        fromDate = formDateFormatter.parse(fromDateParam);
                    }
                    catch (Exception e) {
                        // ignore formatting exception
                    }
                }
                if (toDateParam != null) {
                    try {
                        toDate = formDateFormatter.parse(toDateParam);
                        // Make this date be the end of the day (so it's the day *inclusive*, not *exclusive*)
                        Calendar adjusted = Calendar.getInstance();
                        adjusted.setTime(toDate);
                        adjusted.set(Calendar.HOUR_OF_DAY, 23);
                        adjusted.set(Calendar.MINUTE, 59);
                        adjusted.set(Calendar.SECOND, 59);
                        adjusted.set(Calendar.MILLISECOND, 999);
                        toDate = adjusted.getTime();
                    }
                    catch (Exception e) {
                        // ignore formatting exception
                    }
                }
                dataPoints = discoverDataPoints(fromDate, toDate);
            }
        }

        // default to last 60 minutes
        if (null == fromDate && null==toDate) {
            return new long[] {last60MinutesStart.getTime(), last60MinutesEnd.getTime(), dataPoints};
        } else if (null == fromDate) {
            return new long[] {0, toDate.getTime(), dataPoints};
        } else if (null == toDate) {
            return new long[] {fromDate.getTime(), now.getTime(), dataPoints};
        } else {
            return new long[] {fromDate.getTime(), toDate.getTime(), dataPoints};
        }
    }

    private static int discoverDataPoints(Date fromDate, Date toDate) {
        long delta = toDate.getTime() - fromDate.getTime();
        if(delta > YEAR) {
            return (int)(delta / MONTH);
        }
        else if (delta > 2 * MONTH) {
            return (int)(delta / WEEK);
        }
        else {
            return (int)(delta / DAY);
        }
    }

    public static class GraphDefinition {
        public static final GraphDefinition standard_light;
        public static final GraphDefinition standard_dark;
        private Color[] inlineColors;
        private Color[] outlineColors;

        static {
            standard_light = new GraphDefinition(
                    new Color[]{new Color(246, 171, 77), getColor("B1C3D9")},
                    new Color[]{new Color(217, 126, 12), getColor("17518C")}
                    );

            standard_dark = new GraphDefinition(
                    new Color[]{new Color(116, 128, 141), getColor("74808D")},
                    new Color[]{new Color(116, 128, 141), getColor("74808D")}
                    );
        }

        public static GraphDefinition getDefinition(String colorscheme) {
            GraphDefinition graphDef = GraphDefinition.standard_light;
            if (colorscheme != null && colorscheme.equalsIgnoreCase("dark")) {
                graphDef = GraphDefinition.standard_dark;
            }
            return graphDef;
        }

        public GraphDefinition(Color[] inlineColors, Color[] outlineColors) {
            this.inlineColors = inlineColors;
            this.outlineColors = outlineColors;
        }

        public Color getInlineColor(int index) {
            return inlineColors[index];
        }

        public Color[] getInlineColors() {
            return inlineColors;
        }

        public Color getOutlineColor(int index) {
            return outlineColors[index];
        }

        public Color[] getOutlineColors() {
            return outlineColors;
        }
    }
}
