import com.allen_sauer.gwt.log.client.Log;
import com.smartgwt.client.types.ChartType;
import com.smartgwt.client.widgets.chart.FacetChart;
import com.smartgwt.client.widgets.cube.CellRecord;
import com.smartgwt.client.widgets.cube.Facet;
import com.smartgwt.client.widgets.cube.FacetValue;

import java.util.*;

/**
 * Created with IntelliJ IDEA.
 * User: dzarras
 * Date: 7/12/12
 * Time: 1:37 PM
 */

/**
 * A subclass of FacetChart optimized for time series plotting with support for limiting the number of x-axis labels.
 * When multiple series are added, they are intersected via their 'Key' values that will form x-axis labels.
 * If any given series does not support a value for a given key, it will be skipped, with the series' line connecting
 * the two adjacent points (as per the behavior of the superclass).
 * Two LabelModes are supported: Interval and Count -- see 'setXAxisFormatting'.
 * In a typical usage, the Key class will be a Date and the Value class will be some POJO or data structure that contains
 * both the Date and the value that is being plotted on the y-axis.
 *
 * @param <K> The Key class used to intersect Values.  Natural sort order must correspond to the order in which
 *           labels will be presented along the x-axis.
 * @param <V> The Value class whose data will form the Y values of the chart.   Note that if this is 'Object',
 *           different Extractors can be used with different value classes for each series.
 */
public class TimeSeriesChart<K extends Comparable,V> extends FacetChart
{
    // Strange, couldn't strongly type this because of a binarySearch compilation error:
    // 'cannot find symbol method binarySearch(java.util.ArrayList<K>,K)'

    private ArrayList d_xAxisKeys;

    private HashMap<String,ExtractorStruct<K,V>> d_rawData;

    private LabelMode d_labelMode;
    private int d_labelModeControl;
    private boolean d_labelLastValue = false;
    private XAxisRenderer<K> d_renderer;


    public static enum LabelMode {Interval,Count}

    public static interface SeriesValueExtractor<K,V>
	{
	    /**
	     *
	     * @param o
	     * @return An instance of the Key class for this Value object
	     */
	    public K getXAxisAttribute(V o);

	    /**
	     *
	     * @param o
	     * @return The y-axis value for this Value object, or null if it is to be skipped.
	     */
	    public Double getValue(V o);
	}

    /**
     *
     * @param <K>  The Key class for labels used on the X-Axis
     */
    public static interface XAxisRenderer<K>
    {
	/**
	 * Given a Key instance, return a String representation suitable for use as an x-axis label
	 * @param keyValue  Will not be null
	 * @return
	 */
	public String getLabel(K keyValue);
    }

    /**
     * A temporary holder of the data series added to the chart via 'addSeries', along with a corresponding Extractor
     * @param <K>
     * @param <V>
     */
    private static class ExtractorStruct<K,V>
	{
	    private List<V> d_rawdata;
	    private SeriesValueExtractor<K,V> d_extractor;

	    private ExtractorStruct(List<V> rawdata, SeriesValueExtractor<K,V> extractor)
		{
		    d_rawdata = rawdata;
		    d_extractor = extractor;
		}

	    public List<V> getRawdata()
	    {
		return d_rawdata;
	    }

	    public SeriesValueExtractor<K,V> getExtractor()
		{
		    return d_extractor;
		}
	}

    public TimeSeriesChart()
	{

	    setChartType(ChartType.LINE);
	    setShowDataAxisLabel(true);
	    setShowValueAxisLabel(true);

	    // Note that setting this to 'true' slows down the rendering considerably
	    setShowDataPoints(false);

	    reset();
	}

    /**
     * This may be dubious... re-rendering with new series may not be supported by the superclass.
     */
    public void reset()
    {
	d_xAxisKeys = new ArrayList();
	d_rawData = new HashMap<String, ExtractorStruct<K,V>>();
    }


    /**
     * @param mode    example: LabelMode.Interval,labelModeControl=5 produces a label every 5 points
     *                         LabelModel.Count,labelModeControl=5 produces exactly 5 labels for the x-axis (not counting the
     *                         optional lastLabel)
     * @param labelModeControl  The numerical control for the number of labels as per the mode setting.
     * @param labelLastValue   If true, the last key value for the data will be guaranteed to be labelled
     * @param renderer  If null, the Key class must return a label via toString()
     **/
    public void setXAxisFormatting(LabelMode mode,
				   int labelModeControl,
				   boolean labelLastValue,
				   XAxisRenderer<K> renderer)
    {
	d_labelMode = mode;
	d_labelModeControl = labelModeControl;
	d_labelLastValue = labelLastValue;
	d_renderer = renderer;
    }

    /**
     * Registers a time series of data into the chart for later rendering via plotChart()
     * @param seriesName   This will become the legend entry.
     * @param rawData      
     * @param extractor    The extractor that will be used to process this rawData series.
     *                     Note that if 'V' is 'Object', disparate types of extractors can be used
     *                     along with disparate rawData series.  In that case, casting will be
     *                     required within the implementation of the extractor.
     */
    public void addSeries(String seriesName,List<V> rawData, SeriesValueExtractor<K,V> extractor)
    {
        if ((extractor == null) || (rawData == null)){
	    return;
	}
	if (d_rawData == null){
	    d_rawData = new HashMap<String, ExtractorStruct<K,V>>();
	}

	if (d_xAxisKeys == null){
	    d_xAxisKeys = new ArrayList();
	}

	ExtractorStruct<K,V> struct = new ExtractorStruct<K,V>(rawData,extractor);
	d_rawData.put(seriesName, struct);

	for (V o : rawData){
	    K key = extractor.getXAxisAttribute(o);
	    // 'cannot find symbol method binarySearch(java.util.ArrayList<K>,K)' ??
	    Integer index = Collections.binarySearch(d_xAxisKeys,key);
	    if (index < 0){
		int insertion = -(index)-1;
		d_xAxisKeys.add(insertion,key);
	    }
	}

    }

    /**
     * Causes the chart to render via a call to setData
     */
    public void plotChart()
    {
	int numXAxisKeys = d_xAxisKeys.size();
	int labelEvery = d_labelMode == LabelMode.Interval ? d_labelModeControl : numXAxisKeys / d_labelModeControl;

	if (d_labelMode == LabelMode.Count){
	    if ((numXAxisKeys / d_labelModeControl) != 0){
		labelEvery++;
	    }
	}

	int keyIndex = 0;
	TreeMap<K,String> dataIDs = new TreeMap<K, String>();
	ArrayList<FacetValue> xAxisLabels = new ArrayList<FacetValue>(numXAxisKeys);

	boolean debug = Log.isDebugEnabled();  // Avoid unnecessary String construction.
	int lastIndex = numXAxisKeys -1;
	int countSinceLabel = 1;
	for (Object keyObj : d_xAxisKeys){
	    K key = (K) keyObj;
	    String dataID = "id-" + keyIndex;
	    dataIDs.put(key, dataID);

	    if (debug){
		Log.debug("Key: " + key + ", dataID = [" + dataID + "]");
	    }

	    String dataLabel = " ";
	    if ((keyIndex == 0) ||
		(countSinceLabel == labelEvery) ||
		((d_labelLastValue) && (keyIndex == lastIndex))){
		dataLabel = d_renderer != null ? d_renderer.getLabel(key) : key.toString();
		if (debug){
		    Log.debug("Label for " + dataID + " is [" + dataLabel + "]");
		}
		if (keyIndex > 0){
		    countSinceLabel = 1;
		}
	    }
	    else{
		countSinceLabel++;
	    }
	    FacetValue fv = new FacetValue(dataID,dataLabel);
	    xAxisLabels.add(fv);
	    keyIndex++;
	}


	ArrayList<CellRecord> cellRecords = new ArrayList<CellRecord>();

	for (Map.Entry<String,ExtractorStruct<K,V>> entry : d_rawData.entrySet()){
	    String seriesName = entry.getKey();
	    ExtractorStruct<K,V> struct = entry.getValue();

	    CellRecord record = new CellRecord();
	    record.setAttribute("description",seriesName);

	    SeriesValueExtractor<K,V> extractor = struct.getExtractor();

	    List<V> rawData = struct.getRawdata();

	    if (debug){
		Log.debug("Processing " + seriesName + ":");
	    }
	    for (V obj : rawData){
		K xAxisAttribute = extractor.getXAxisAttribute(obj);

		if (xAxisAttribute != null){
		    String dataID = dataIDs.get(xAxisAttribute);

		    if (dataID != null){
			Double value = extractor.getValue(obj);
			if (value != null){
			    record.setAttribute(dataID, value);
			}
		    }
		}

	    }

	    cellRecords.add(record);
	}


	Facet xAxisFacet = new Facet();
	xAxisFacet.setInlinedValues(true);
	xAxisFacet.setValues(xAxisLabels.toArray(new FacetValue[xAxisLabels.size()]));

	setFacets(xAxisFacet, new Facet("description", "thing"));
	setData(cellRecords.toArray(new CellRecord[cellRecords.size()]));
    }

    public void dumpKeys()
    {
	if (d_xAxisKeys == null){
	    Log.error("No xAxisKeys!");
	}

	Log.warn("Number of keys: " + d_xAxisKeys.size());
	int i = 0;
	for (Object obj : d_xAxisKeys){
	    String label = d_renderer != null ? d_renderer.getLabel((K)obj) : obj.toString();
	    Log.warn(i + ": " + label);
	    i++;
	}
    }

    public int getNumberOfKeys()
    {
	return d_xAxisKeys == null ? -1 : d_xAxisKeys.size();
    }

    /**
     * Frees up resources used by this subclass... super() is called, too.
     */
    public void destroy()
    {
	d_rawData = null;
	d_renderer = null;
	d_xAxisKeys = null;

	super.destroy();
    }


}