/*ChartHelper.java

	Purpose:
		
	Description:
		
	History:
		Oct 18, 2010 2:27:40 PM     2010, Created by ashish

Copyright (C) 2010 Potix Corporation. All Rights Reserved.
 */
package org.zkoss.zssex.util;

import java.awt.Font;
import java.io.Serializable;
import java.text.NumberFormat;
import java.util.Date;

import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.axis.TickUnits;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.zkoss.lang.*;
import org.zkoss.util.logging.Log;
import org.zkoss.zss.model.*;
import org.zkoss.zss.model.SChart.BarDirection;
import org.zkoss.zss.model.SChart.ChartGrouping;
import org.zkoss.zss.model.chart.*;
import org.zkoss.zss.model.impl.sys.FormatEngineImpl;
import org.zkoss.zss.model.sys.EngineFactory;
import org.zkoss.zss.model.sys.format.FormatContext;
import org.zkoss.zss.model.sys.format.FormatEngine;
import org.zkoss.zss.model.sys.format.FormatResult;
import org.zkoss.zss.model.util.Strings;
import org.zkoss.zul.*;

/**
 * Internal Use Only. Helper Utility class for handling Chart.
 * 
 * @author ashish
 * 
 */
public class JFreeChartHelper {

	private static final Log logger = Log.lookup(JFreeChartHelper.class.getName());
	// TODO ZSS 3.5 NO label FONT
	static final String TITLE_FONT_PROPERTY_KEY = "org.zkoss.zss.chart.title.font";
	static final String LEGEND_FONT_PROPERTY_KEY = "org.zkoss.zss.chart.legend.font";
	static final String XAXIS_TICK_FONT_PROPERTY_KEY = "org.zkoss.zss.chart.xAxisTick.font";
	static final String YAXIS_TICK_FONT_PROPERTY_KEY = "org.zkoss.zss.chart.yAxisTick.font";
	
	// TODO ZSS 3.5 draw jfreechart with 3.5 model directly
	// orientation is vertical
	public static JFreeChart drawJFreeChart(SChart chartInfo) {
		final ZssChartExportEngine engine;
		final String type = JFreeChartHelper.getChartType(chartInfo);
		if ("line".equals(type)) {
			engine = new LineChartExportEngine(chartInfo);
		} else if ("pie".equals(type) || "ring".equals(type)) {
			engine = new PieChartExportEngine(chartInfo);
		} else {
			engine = new ZssChartExportEngine(chartInfo);
		}
		
		return engine.drawJFreeChart();
	}

	public static String getChartType(SChart chartInfo) {
		switch (chartInfo.getType()) {
		case AREA:
			return chartInfo.getGrouping() == ChartGrouping.STACKED ? Chart.STACKED_AREA : Chart.AREA;
		case BAR:
		case COLUMN:
			return chartInfo.getGrouping() == ChartGrouping.STACKED ? Chart.STACKED_BAR : Chart.BAR;
		case BUBBLE:
			return Chart.BUBBLE;
		case DOUGHNUT:
			return Chart.RING;
		case LINE:
			return Chart.LINE;
		case OF_PIE:
			return "ofpie";
		case PIE:
			return Chart.PIE;
		case RADAR:
			return "radar";
		case SCATTER:
			return Chart.SCATTER;
		case STOCK:
			return Chart.CANDLESTICK;
		case SURFACE:
			return "surface";
		default:
			return "";
		}

	}

	public static PlotOrientation getOrientation(SChart chartInfo) {
		if(chartInfo.getBarDirection() == null) {
			return PlotOrientation.VERTICAL;
		}
		switch (chartInfo.getBarDirection()) {
		case HORIZONTAL:
			return PlotOrientation.HORIZONTAL;
		default:
			return PlotOrientation.VERTICAL;
		}
	}

	public static void drawChart(org.zkoss.zul.Chart chart, SChart chartInfo) {
		ChartModel model = null;
		switch (chartInfo.getType()) {
		case PIE:
		case DOUGHNUT:
			model = preparePieModel(chartInfo.getData());
			break;
		case BAR:
		case COLUMN:
		case LINE:
		case AREA:
			model = prepareCategoryModel(chartInfo.getData());
			break;
		case SCATTER:
			model = prepareXYModel(chartInfo.getData());
			break;
		case BUBBLE:
			model = prepareXYZModel(chartInfo.getData());
			break;
		case STOCK:
			model = prepareHiLoModel(chartInfo.getData());
			break;
		// TODO other chart types
		case RADAR:
		case SURFACE:
		case OF_PIE:
		}
		if (model != null/* && !drawer.prepareChart(chart, model, xssfChart) */) {
			drawChartInner(chart, model, chartInfo);
		}
	}

	public static ChartModel prepareHiLoModel(SChartData chartData) {
		if (!(chartData instanceof SGeneralChartData)) {
			return null;
		}

		final HiLoModel model = new SimpleHiLoModel();
		// currently, we only handle , volumn/open/high/low/close

		SSeries volumn = null;
		SSeries open = null;
		SSeries high = null;
		SSeries low = null;
		SSeries close = null;

		SGeneralChartData catData = (SGeneralChartData) chartData;

		int s = catData.getNumOfSeries();
		if (s <= 0) {
			return model;
		}
		if (s > 4) {
			close = catData.getSeries(4);
		}
		if (s > 3) {
			low = catData.getSeries(3);
		}
		if (s > 2) {
			high = catData.getSeries(2);
		}
		if (s > 1) {
			open = catData.getSeries(1);
		}
		if (s > 0) {
			volumn = catData.getSeries(0);
		}

		s = Math.max(catData.getNumOfCategory(), volumn.getNumOfValue());
		s = open == null ? s : Math.max(volumn.getNumOfValue(), open.getNumOfValue());
		s = high == null ? s : Math.max(open.getNumOfValue(), high.getNumOfValue());
		s = low == null ? s : Math.max(high.getNumOfValue(), low.getNumOfValue());
		s = close == null ? s : Math.max(low.getNumOfValue(), volumn.getNumOfValue());

		for (int i = 0; i < s; i++) {
			Number volumnval = toNumber(volumn == null ? 0 : volumn.getValue(i), 0);
			Number openval = toNumber(open == null ? 0 : open.getValue(i), 0);
			Number highval = toNumber(high == null ? 0 : high.getValue(i), 0);
			Number lowval = toNumber(low == null ? 0 : low.getValue(i), 0);
			Number closeval = toNumber(close == null ? 0 : close.getValue(i), 0);
			Object cat = catData.getCategory(i);
			Date d = toDate(cat);
			if (d != null) {
				model.addValue(d, openval, highval, lowval, closeval, volumnval);
			} else {
				// TODO jfree chart doesn't allow to put a Label on category
				// field.
			}

		}

		return model;
	}

	private static Date toDate(Object obj) {
		if (obj instanceof Date) {
			return (Date) obj;
		} else if (obj instanceof Number) {
			return EngineFactory.getInstance().getCalendarUtil().doubleValueToDate(((Number) obj).doubleValue());
		}
		return null;
	}

	private static Number toNumber(Object obj, int defaultValue) {
		return obj instanceof Number ? (Number) obj : defaultValue;
	}
	
	private static String toSeriesName(String name, int seriesIndex){
		if (name == null || "".equals(name)){
			return "Series"+(seriesIndex+1);
		}else{
			return name;
		}
	}

	/**
	 * We treat those non-numeric data in a series as 0.
	 */
	public static ChartModel prepareCategoryModel(SChartData chartData) {
		if (!(chartData instanceof SGeneralChartData)) {
			return null;
		}
		final CategoryModel model = new SimpleCategoryModel();

		SGeneralChartData catData = (SGeneralChartData) chartData;
		SSeries[] serieses = new SSeries[catData.getNumOfSeries()];

		if (serieses.length == 0) {
			return model;
		}

		for (int i = 0; i < serieses.length; i++) {
			serieses[i] = catData.getSeries(i);
		}

		int s = Math.max(catData.getNumOfCategory(), serieses[0].getNumOfValue());
		for (int i = 1; i < serieses.length; i++) {
			s = Math.max(s, serieses[i].getNumOfValue());
		}

		boolean onlyVisible = chartData.getChart().isPlotOnlyVisibleCells();
		for (int i = 0; i < s; i++) {
			Object cat = "";
			if(!(onlyVisible && catData.isCategoryHidden(i)))
				cat = catData.getCategory(i);

			for (int j = 0; j < serieses.length; j++) {
				if(onlyVisible && serieses[j].isXValueFomulaHidden(i))
					continue;
				Object series = toSeriesName(serieses[j].getName(), j);
				Number val = toNumber(serieses[j].getValue(i), 0);
				model.setValue(new ChartModelLabel(j, series), new ChartModelLabel(i, cat), val);
			}
		}

		return model;
	}

	/**
	 * Scatter chart that uses XYModel has following characteristics: 
	 * 1. It always has two value axes to show one set of numerical data along a horizontal (value) axis and another set of numerical values along a vertical (value) axis.
	 * 2. If X axis value is not numeric, we convert them to a default series with index, e.g. 1, 2, 3, 4...etc like Excel does.
	 * @return data model for scatter chart 
	 */
	public static XYModel prepareXYModel(SChartData chartData) {
		if (!(chartData instanceof SGeneralChartData)) {
			return null;
		}

		final XYModel model = new SimpleXYModel();
		SGeneralChartData catData = (SGeneralChartData) chartData;
		SSeries[] serieses = new SSeries[catData.getNumOfSeries()];

		if (serieses.length == 0) {
			return model;
		}

		for (int i = 0; i < serieses.length; i++) {
			serieses[i] = catData.getSeries(i);
		}

		boolean onlyVisible = chartData.getChart().isPlotOnlyVisibleCells();
		for (int i = 0; i < serieses.length; i++) {
			Object sname = toSeriesName(serieses[i].getName(), i);
			int s = Math.min(serieses[i].getNumOfValue(), serieses[i].getNumOfYValue());
			for (int j = 0; j < s; j++) {
				if(onlyVisible && serieses[i].isYValueFomulaHidden(j)) 
					continue;
				
				Number xval = toNumber(serieses[i].getValue(j), j + 1);
				Number yval = toNumber(serieses[i].getYValue(j), 0);
				model.addValue(new ChartModelLabel(i, sname), xval, yval);
			}
		}

		return model;
	}

	/**
	 * The bubble chart that use XYZModel has similar characteristics as scatter chart.
	 * Its X axis always shows numerical data. If X axis value is not numeric, we convert them to a default series with index, e.g. 1, 2, 3, 4...etc like Excel does.
	 * @see #prepareXYModel(SChartData)
	 */
	public static XYZModel prepareXYZModel(SChartData chartData) {
		if (!(chartData instanceof SGeneralChartData)) {
			return null;
		}

		final XYZModel model = new SimpleXYZModel();
		SGeneralChartData catData = (SGeneralChartData) chartData;
		SSeries[] serieses = new SSeries[catData.getNumOfSeries()];

		if (serieses.length == 0) {
			return model;
		}

		for (int i = 0; i < serieses.length; i++) {
			serieses[i] = catData.getSeries(i);
		}

		boolean onlyVisible = chartData.getChart().isPlotOnlyVisibleCells();
		for (int i = 0; i < serieses.length; i++) {
			Object sname = toSeriesName(serieses[i].getName(), i);
			int s = Math.min(serieses[i].getNumOfValue(), serieses[i].getNumOfYValue());
			for (int j = 0; j < s; j++) {
				if(onlyVisible && (serieses[i].isYValueFomulaHidden(j) || serieses[i].isZValueFomulaHidden(j))) 
					continue;
				
				Number xval = toNumber(serieses[i].getValue(j), j + 1);
				Number yval = toNumber(serieses[i].getYValue(j), 0);
				Number zval = toNumber(serieses[i].getZValue(j), 0);
				model.addValue(new ChartModelLabel(i, sname), xval, yval, zval);
			}
		}

		return model;
	}

	// //PieModel
	public static ChartModel preparePieModel(SChartData chartData) {

		if (!(chartData instanceof SGeneralChartData)) {
			return null;
		}
		final PieModel model = new SimplePieModel();

		SGeneralChartData catData = (SGeneralChartData) chartData;
		SSeries series1 = null;
		if (catData.getNumOfSeries() > 0) {
			series1 = catData.getSeries(0);
		}

		if (series1 == null) {
			return model;
		}

		int s = Math.max(catData.getNumOfCategory(), series1.getNumOfValue());
		boolean onlyVisible = chartData.getChart().isPlotOnlyVisibleCells();

		for (int i = 0; i < s; i++) {
			Object label = "";
			Object val = "";
			
			if(onlyVisible) {
				// if it doesn't have value, then we skip this record
				if(series1.isXValueFomulaHidden(i)) 
					continue;
				
				if(!catData.isCategoryHidden(i))
					label = catData.getCategory(i);
				
				val = series1.getValue(i);
			} else {
				label = catData.getCategory(i);
				val = series1.getValue(i);
			}
			model.setValue(new ChartModelLabel(i, label), val instanceof Number ? (Number) val : 0);
		}

		return model;
	}

	static class ChartModelLabel implements Serializable, Comparable<ChartModelLabel> {

		private static final long serialVersionUID = 1L;

		private final Integer index;
		private final Object label;

		ChartModelLabel(int index, Object label) {
			this.index = index;
			this.label = label;
		}

		public String toString() {
			return label == null ? "" : label.toString();
		}

		@Override
		public int compareTo(ChartModelLabel o) {
			int indexComp = index.compareTo(o.index);
			return indexComp != 0 ? indexComp : label.toString().compareTo(o.toString());
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((index == null) ? 0 : index.hashCode()) + (label == null ? 0 : label.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ChartModelLabel other = (ChartModelLabel) obj;
			return Objects.equals(this.index, other.index) && Objects.equals(this.label, other.label);
		}
	}

	//
	/**
	 * chart's font property value format: [NAME], [WEIGHT], [SIZE] e.g.
	 * SansSerif, bold, 20
	 * 
	 * @param key
	 * @return
	 */
	public static Font getCustomFont(String key) {
		String value = Library.getProperty(key);
		Font font = null;
		if (value != null) {
			try {
				String[] token = value.trim().split(",");
				if (token.length == 3) {
					String weightString = token[1].trim();
					int weight = 0;
					if ("bold".equalsIgnoreCase(weightString)) {
						weight = Font.BOLD;
					} else if ("plain".equalsIgnoreCase(weightString)) {
						weight = Font.PLAIN;
					} else if ("italic".equalsIgnoreCase(weightString)) {
						weight = Font.ITALIC;
					} else {
						throw new Exception("Unsupprted chart font weight: " + weightString);
					}

					font = new Font(token[0].trim(), weight, Integer.parseInt(token[2].trim()));
				}
			} catch (Exception x) {
				logger.warning(x.getMessage());
			}
		}
		return font;
	}

	/**
	 * Pre-set fonts of {@link Chart} upon configuration.
	 * 
	 * @param chart
	 */
	private static void setFont(org.zkoss.zul.Chart chart) {
		Font font = getCustomFont(TITLE_FONT_PROPERTY_KEY);
		if (font != null) {
			chart.setTitleFont(font);
		}

		font = getCustomFont(LEGEND_FONT_PROPERTY_KEY);
		if (font != null) {
			chart.setLegendFont(font);
		}

		font = getCustomFont(XAXIS_TICK_FONT_PROPERTY_KEY);
		if (font != null) {
			chart.setXAxisTickFont(font);
		}

		font = getCustomFont(YAXIS_TICK_FONT_PROPERTY_KEY);
		if (font != null) {
			chart.setYAxisTickFont(font);
		}
	}

	private static void drawChartInner(org.zkoss.zul.Chart chart, ChartModel model, SChart chartInfo) {
		setFont(chart);
		chart.setType(JFreeChartHelper.getChartType(chartInfo));
		chart.setTitle(getChartTitle(chartInfo));
		chart.setThreeD(chartInfo.isThreeD());
		chart.setFgAlpha(128);
		if (chartInfo.getBarDirection() != null) {
			chart.setOrient(chartInfo.getBarDirection() == BarDirection.HORIZONTAL ? "horizontal" : "vertical");
		}
		chart.setModel(model);
	}

	private static String getChartTitle(SChart chartInfo) {
		// final XSSFChart xssfChart = (XSSFChart) poiChart;
		// final boolean autoTitleDeleted = xssfChart.isAutoTitleDeleted();
		// if (!autoTitleDeleted) {
		final String title = chartInfo.getTitle();
//		final String type = ChartHelper.getChartType(chartInfo);
//		return !Strings.isEmpty(title) ? title : "";
		// }
		// return "";

		//ZSS-821
		return title == null ? "" : !Strings.isEmpty(title) ? title : getFirstSeriesTitle(chartInfo);
		
	}

	private static String getFirstSeriesTitle(SChart chartInfo) {
		//ZSS-821
		SChartData scd = chartInfo.getData();
		if (scd instanceof SGeneralChartData) {
			SGeneralChartData cd = (SGeneralChartData) chartInfo.getData();
			if (cd.getNumOfSeries() > 0) {
				if (cd.getNumOfSeries() == 1) {
					return cd.getSeries(0).getName();
				} else {
					switch (chartInfo.getType()) {
					case PIE:
					case DOUGHNUT:
						return cd.getSeries(0).getName();
					default:
						return "";
					}
				}
			}
		}
		return "";
	}

	//ZSS-822
	public static void prepareAxis(JFreeChart jfchart, SChart chartInfo) {
		
		//ZSS-822
		switch (chartInfo.getType()) {
		case PIE:
		case DOUGHNUT:
		case BAR:
		case COLUMN:
		case LINE:
		case AREA:
			if (!chartInfo.getValueAxises().isEmpty()) {
				final NumberAxis jfaxis = (NumberAxis) jfchart.getCategoryPlot().getRangeAxis();
				if (jfaxis != null) {
					SChartAxis saxis = chartInfo.getValueAxises().get(0); 
					setupAxis(jfaxis, saxis);
				}
			}
			break;
		case SCATTER:
			final int size = chartInfo.getValueAxises().size(); 
			final NumberAxis jfxaxis = (NumberAxis) jfchart.getXYPlot().getRangeAxis(0);
			final NumberAxis jfyaxis = (NumberAxis) jfchart.getXYPlot().getRangeAxis(1);
			if (chartInfo.getBarDirection() == BarDirection.HORIZONTAL) {
				if (size > 0 && jfyaxis != null) {
					SChartAxis yaxis = chartInfo.getValueAxises().get(0);
					setupAxis(jfyaxis, yaxis);
				}
				if (size > 1 && jfxaxis != null) {
					SChartAxis xaxis = chartInfo.getValueAxises().get(1);
					setupAxis(jfxaxis, xaxis);
				}
			} else {
				if (size > 0 && jfxaxis != null) {
					SChartAxis xaxis = chartInfo.getValueAxises().get(0);
					setupAxis(jfxaxis, xaxis);
				}
				if (size > 1 && jfyaxis != null) {
					SChartAxis yaxis = chartInfo.getValueAxises().get(1);
					setupAxis(jfyaxis, yaxis);
				}
			}
			break;
		case BUBBLE:
			final int size0 = chartInfo.getValueAxises().size();
			final NumberAxis jfxaxis0 = (NumberAxis) jfchart.getXYPlot().getRangeAxis(0);
			final NumberAxis jfyaxis0 = (NumberAxis) jfchart.getXYPlot().getRangeAxis(1);
			final NumberAxis jfzaxis0 = (NumberAxis) jfchart.getXYPlot().getRangeAxis(2);
			if (size0 > 0 && jfxaxis0 != null) {
				SChartAxis xaxis = chartInfo.getValueAxises().get(0);
				setupAxis(jfxaxis0, xaxis);
			}
			if (size0 > 1 && jfyaxis0 != null) {
				SChartAxis yaxis = chartInfo.getValueAxises().get(1);
				setupAxis(jfyaxis0, yaxis);
			}
			if (size0 > 2 && jfzaxis0 != null) {
				SChartAxis zaxis = chartInfo.getValueAxises().get(2);
				setupAxis(jfzaxis0, zaxis);
			}
			break;
		// TODO other chart types
		case STOCK:
			final int size1 = chartInfo.getValueAxises().size();
			final NumberAxis jfxaxis1 = (NumberAxis) jfchart.getXYPlot().getRangeAxis(0);
			final NumberAxis jfyaxis1 = (NumberAxis) jfchart.getXYPlot().getRangeAxis(1);
			if (size1 > 0 && jfxaxis1 != null) {
				SChartAxis xaxis = chartInfo.getValueAxises().get(0);
				setupAxis(jfxaxis1, xaxis);
			}
			if (size1 > 1 && jfyaxis1 != null) {
				SChartAxis yaxis = chartInfo.getValueAxises().get(1);
				setupAxis(jfyaxis1,  yaxis);
			}
			break;
		case RADAR:
		case SURFACE:
		case OF_PIE:
		}

	}
	//ZSS-822
	private static void setupAxis(ValueAxis axis, SChartAxis saxis) {
		if (saxis != null && !Strings.isBlank(saxis.getFormat())) {
			FormatContext formatContext = new FormatContext();
			FormatEngine formatEngine = new FormatEngineImpl();
			FormatResult result = formatEngine.format(saxis.getFormat(), 1d, formatContext, 12);
			NumberFormat format = (NumberFormat) result.getFormater();
			double low = axis.getLowerBound();
			double high = axis.getUpperBound();
			double size = (high - low) / 10;
			size = normalize(size);
			TickUnits units = new TickUnits();
			if (format != null) {
				units.add(new NumberTickUnit(size, format));
			} else {
				units.add(new NumberTickUnit(size));
			}
			axis.setStandardTickUnits(units);
		}
	}
	//ZSS-822
	private static double normalize(double size) {
		double exp10 = Math.log10(size);
		int exp = (int) Math.floor(exp10);
		double tens = Math.pow(10d, exp);
		int num = (int) Math.floor(size / tens);
		switch(num) {
		case 0:
		case 1:
		case 2:
			num = tens < 1 ? 5 : 2;
			break;
		case 3:
		case 4:
		case 5:
			num = 5;
			break;
		case 6:
		case 7:
		case 8:
		case 9:
			num = 10;
			break;
		}
		return  num * tens;
	}
}
