/* ChartsModelManager.java

{{IS_NOTE
	Purpose:
		
	Description:
		
	History:
		2014/10/30 , Created by jerrychen
}}IS_NOTE

Copyright (C) 2014 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
}}IS_RIGHT
*/
package org.zkoss.zssex.util;

import java.io.Serializable;

import org.zkoss.chart.model.CategoryModel;
import org.zkoss.chart.model.ChartsModel;
import org.zkoss.chart.model.DefaultCategoryModel;
import org.zkoss.chart.model.DefaultPieModel;
import org.zkoss.chart.model.DefaultXYModel;
import org.zkoss.chart.model.DefaultXYZModel;
import org.zkoss.chart.model.PieModel;
import org.zkoss.chart.model.XYModel;
import org.zkoss.chart.model.XYZModel;
import org.zkoss.json.JSONAware;
import org.zkoss.lang.Objects;
import org.zkoss.zss.model.SChart;
import org.zkoss.zss.model.SChart.ChartGrouping;
import org.zkoss.zss.model.chart.SChartData;
import org.zkoss.zss.model.chart.SGeneralChartData;
import org.zkoss.zss.model.chart.SSeries;

import static org.zkoss.zss.model.SChart.ChartType;

/**
 * Internal use only for ZK charts model.
 * @author Jerry
 *
 */
public class ChartsModelManager {
	
	private ModelSelector[] selectors = {
			new PieModelSelector(),
			new CategoryModelSelector(),
			new XYModelSelector(),
			new XYZModelSelector()
	};
	
	public ChartsModel getChartsModel(SChart schart) {
		for (ModelSelector selector : selectors) {
			if(selector.isSupported(schart.getType()))
				return selector.toCharts(schart.getData());
		}
		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;
		}
	}
	
	static class ChartModelLabel implements Serializable, Comparable<ChartModelLabel>, JSONAware {

		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);
		}

		@Override
		public String toJSONString() {
			return "\"" + toString() + "\"";
		}
	}
	
	interface ModelSelector{
		public boolean isSupported(ChartType type);
		public ChartsModel toCharts(SChartData chartData);
	}
	
	private class PieModelSelector implements ModelSelector {

		@Override
		public boolean isSupported(ChartType type) {
			return ChartType.PIE == type || ChartType.DOUGHNUT == type;
		}

		@Override
		public ChartsModel toCharts(SChartData chartData) {
			if (!(chartData instanceof SGeneralChartData)) {
				return null;
			}
			
			final PieModel model = new DefaultPieModel();

			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 = "."; // highcharts doesn't support empty string as legend
				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;
		}
		
	}

	private class CategoryModelSelector implements ModelSelector {

		@Override
		public boolean isSupported(ChartType type) {
			return ChartType.BAR == type || ChartType.COLUMN == type 
					|| ChartType.LINE == type || ChartType.AREA == type;
		}

		@Override
		public ChartsModel toCharts(SChartData chartData) {
			if (!(chartData instanceof SGeneralChartData)) {
				return null;
			}
			final CategoryModel model = new DefaultCategoryModel();

			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 = "."; // highcharts doesn't support empty string as legend
				if(!(onlyVisible && catData.isCategoryHidden(i)))
					cat = catData.getCategory(i);
				// hightcharts will place the series in order
				// here we need to add it in the reversed order
				if(chartData.getChart().getGrouping() == ChartGrouping.STACKED
						|| chartData.getChart().getGrouping() == ChartGrouping.PERCENT_STACKED) {
					for (int j = serieses.length - 1; j >= 0 ; j--) {
						if(onlyVisible && serieses[j].isXValueFomulaHidden(i))
							continue;
						setModelValue(model, serieses, cat, i, j, onlyVisible);
					}
				} else {
					for (int j = 0; j < serieses.length; j++) {
						if(onlyVisible && serieses[j].isXValueFomulaHidden(i))
							continue;
						setModelValue(model, serieses, cat, i, j, onlyVisible);
					}
				}
			}

			return model;
		}

		private void setModelValue(final CategoryModel model,
				SSeries[] serieses, Object category, int categoryIndex,
				int seriesIndex, boolean onlyVisible) {
			Object series = "";
			
			if(!(onlyVisible && serieses[seriesIndex].isNameFomulaHidden(categoryIndex)))
				series = ChartsModelManager.toSeriesName(serieses[seriesIndex].getName(), seriesIndex);
			Number val = ChartsModelManager.toNumber(serieses[seriesIndex].getValue(categoryIndex), 0);
			model.setValue(new ChartModelLabel(seriesIndex, series), new ChartModelLabel(categoryIndex, category), val);
		}
		
		
		
	}
	
	private class XYModelSelector implements ModelSelector {

		@Override
		public boolean isSupported(ChartType type) {
			return ChartType.SCATTER == type;
		}

		/**
		 * 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.
		 */
		@Override
		public ChartsModel toCharts(SChartData chartData) {
			if (!(chartData instanceof SGeneralChartData)) {
				return null;
			}

			final XYModel model = new DefaultXYModel();
			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;
		}
		
	}

	private class XYZModelSelector implements ModelSelector {

		@Override
		public boolean isSupported(ChartType type) {
			return ChartType.BUBBLE == type;
		}

		/**
		 * 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.
		 */
		@Override
		public ChartsModel toCharts(SChartData chartData) {
			if (!(chartData instanceof SGeneralChartData)) {
				return null;
			}

			final XYZModel model = new DefaultXYZModel();
			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;
		}
		
	}
}

