/**
 * Copyright (C) 2001-2016 by RapidMiner and the contributors
 *
 * Complete list of developers available at our web site:
 *
 * http://rapidminer.com
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License along with this program.
 * If not, see http://www.gnu.org/licenses/.
 */
package com.rapidminer.gui.plotter.charts;

import java.awt.Adjustable;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.logging.Level;

import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.plot.SpiderWebPlot;
import org.jfree.chart.title.LegendTitle;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.category.SlidingCategoryDataset;
import org.jfree.ui.HorizontalAlignment;
import org.jfree.ui.RectangleEdge;

import com.rapidminer.datatable.DataTable;
import com.rapidminer.datatable.DataTableRow;
import com.rapidminer.gui.plotter.LabelRotatingPlotterAdapter;
import com.rapidminer.gui.plotter.PlotterConfigurationModel;
import com.rapidminer.gui.plotter.PlotterConfigurationModel.PlotterSettingsChangedListener;
import com.rapidminer.gui.plotter.settings.ListeningJCheckBox;
import com.rapidminer.gui.plotter.settings.ListeningJComboBox;
import com.rapidminer.gui.properties.PropertyPanel;
import com.rapidminer.operator.ports.InputPort;
import com.rapidminer.parameter.ParameterType;
import com.rapidminer.parameter.ParameterTypeBoolean;
import com.rapidminer.parameter.ParameterTypeCategory;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.Tools;
import com.rapidminer.tools.math.function.aggregation.AbstractAggregationFunction;
import com.rapidminer.tools.math.function.aggregation.AggregationFunction;
import com.rapidminer.tools.math.function.aggregation.AverageFunction;


/**
 * Web plotter based on the Spiderweb-plotter of JFreechart.
 *
 * @author David Arnu
 *
 */
public class WebPlotter extends LabelRotatingPlotterAdapter implements ChangeListener {

	public static final String PARAMETER_AGGREGATION = "aggregation";

	public static final String PARAMETER_USE_DISTINCT = "use_distinct";

	private static final long serialVersionUID = 1208210421840512091L;

	private static final int MAX_CATEGORY_VIEW_COUNT = 40;

	/** Indicates which columns will be plotted. */
	private boolean[] columns = new boolean[0];

	/** The maximal number of printable categories. */
	private static final int MAX_CATEGORIES = 200;

	/** The currently used data table object. */
	private DataTable dataTable;

	/** The web data set. */
	private DefaultCategoryDataset categoryDataSet = new DefaultCategoryDataset();

	private SlidingCategoryDataset slidingCategoryDataSet = null;

	/** The column which is used for the group by attribute. */
	private int groupByColumn = -1;

	/** Indicates if only distinct values should be used for aggregation functions. */
	private ListeningJCheckBox useDistinct;

	/** The used aggregation function. */
	private ListeningJComboBox aggregationFunction = null;

	/** Indicates if absolute values should be used. */
	private boolean absolute = false;

	private String aggregationFunctionName;

	private boolean useDistinctFlag = false;

	private JPanel scrollablePlotterPanel = new JPanel(new BorderLayout());

	private ChartPanel panel = null;

	private JScrollBar viewScrollBar = new JScrollBar(JScrollBar.HORIZONTAL, 0, MAX_CATEGORY_VIEW_COUNT, 0, MAX_CATEGORIES);

	private boolean showScrollbar = true;

	public WebPlotter(final PlotterConfigurationModel settings) {
		super(settings);
		setBackground(Color.white);

		String[] allFunctions = new String[AbstractAggregationFunction.KNOWN_AGGREGATION_FUNCTION_NAMES.length + 1];
		allFunctions[0] = "none";
		System.arraycopy(AbstractAggregationFunction.KNOWN_AGGREGATION_FUNCTION_NAMES, 0, allFunctions, 1,
				AbstractAggregationFunction.KNOWN_AGGREGATION_FUNCTION_NAMES.length);
		aggregationFunction = new ListeningJComboBox(settings, PARAMETER_AGGREGATION, allFunctions);
		aggregationFunction.setPreferredSize(new Dimension(aggregationFunction.getPreferredSize().width,
				PropertyPanel.VALUE_CELL_EDITOR_HEIGHT));
		aggregationFunction
				.setToolTipText("Select the type of the aggregation function which should be used for grouped values.");
		aggregationFunction.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				settings.setParameterAsString(PARAMETER_AGGREGATION, aggregationFunction.getSelectedItem().toString());
			}
		});

		useDistinct = new ListeningJCheckBox(PARAMETER_USE_DISTINCT, "Use Only Distinct", false);
		useDistinct.setToolTipText("Indicates if only distinct values should be used for aggregation functions.");
		useDistinct.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				settings.setParameterAsBoolean(PARAMETER_USE_DISTINCT, useDistinct.isSelected());
			}
		});

		viewScrollBar.getModel().addChangeListener(this);

	}

	public WebPlotter(PlotterConfigurationModel settings, DataTable dataTable) {
		this(settings);
		setDataTable(dataTable);
	}

	@Override
	public void stateChanged(ChangeEvent e) {
		if (slidingCategoryDataSet != null) {
			slidingCategoryDataSet.setFirstCategoryIndex(viewScrollBar.getValue());
		}
	}

	@Override
	public void setDataTable(DataTable dataTable) {
		super.setDataTable(dataTable);
		this.dataTable = dataTable;
		this.columns = new boolean[this.dataTable.getNumberOfColumns()];
		updatePlotter();
	}

	@Override
	public void setAbsolute(boolean absolute) {
		this.absolute = absolute;
		updatePlotter();
	}

	@Override
	public boolean isSupportingAbsoluteValues() {
		return true;
	}

	@Override
	public void setPlotColumn(int index, boolean plot) {
		columns[index] = plot;
		updatePlotter();
		super.repaint();
		revalidate();
	}

	@Override
	public boolean getPlotColumn(int index) {
		return columns[index];
	}

	@Override
	public int getNumberOfAxes() {
		return 1;
	}

	@Override
	public int getValuePlotSelectionType() {
		return MULTIPLE_SELECTION;
	}

	@Override
	public void setAxis(int index, int dimension) {
		if (index == 0) {
			groupByColumn = dimension;
		}
		updatePlotter();
	}

	@Override
	public int getAxis(int index) {
		if (index == 0) {
			return groupByColumn;
		} else {
			return -1;
		}
	}

	@Override
	public String getAxisName(int index) {
		if (index == 0) {
			return "Group-By Column";
		} else {
			return "Unknown";
		}
	}

	protected int getNumberOfPlots() {
		int counter = 0;
		for (boolean column : columns) {
			if (column) {
				counter++;
			}
		}
		return counter;
	}

	private int prepareData() {

		if (getNumberOfPlots() == 0) {
			return -1;
		}
		int categoryValuesSize = -1;

		categoryDataSet.clear();
		synchronized (dataTable) {
			for (int col = 0; col < this.dataTable.getNumberOfColumns(); col++) {
				if (this.columns[col] && this.dataTable.isNumerical(col)) {
					AggregationFunction aggregation = null;
					if (aggregationFunctionName != null && !aggregationFunctionName.equals("none")) {
						try {
							aggregation = AbstractAggregationFunction.createAggregationFunction(aggregationFunctionName);
						} catch (Exception e) {
							LogService
									.getRoot()
									.log(Level.WARNING,
											"com.rapidminer.gui.plotter.charts.BarChartPlotter.instantiating_aggregation_function_error",
											aggregationFunctionName);
							aggregation = new AverageFunction();
						}
					}
					Iterator<DataTableRow> i = this.dataTable.iterator();
					Map<String, Collection<Double>> categoryValues = new LinkedHashMap<>();

					if (groupByColumn >= 0 && dataTable.isNumerical(groupByColumn)) {
						return 0;
					}

					while (i.hasNext()) {
						DataTableRow row = i.next();

						double value = row.getValue(col);

						if (!Double.isNaN(value)) {
							if (absolute) {
								value = Math.abs(value);
							}

							String groupByName = "";
							if (groupByColumn >= 0) {
								double nameValue = row.getValue(groupByColumn);
								if (dataTable.isDate(groupByColumn)) {
									groupByName = Tools.createDateAndFormat(nameValue);
								} else if (dataTable.isTime(groupByColumn)) {
									groupByName = Tools.createTimeAndFormat(nameValue);
								} else if (dataTable.isDateTime(groupByColumn)) {
									groupByName = Tools.createDateTimeAndFormat(nameValue);
								} else if (dataTable.isNominal(groupByColumn)) {
									groupByName = dataTable.mapIndex(groupByColumn, (int) nameValue);
								} else {
									groupByName = Tools.formatIntegerIfPossible(nameValue) + "";
								}
							}

							// increment values
							Collection<Double> values = categoryValues.get(groupByName);
							if (values == null) {
								if (useDistinctFlag) {
									values = new TreeSet<>();
								} else {
									values = new LinkedList<>();
								}
								categoryValues.put(groupByName, values);
							}
							values.add(value);
						}
					}

					// calculate aggregation and set values
					if (col >= 0) {
						if (aggregation != null) {
							Iterator<Map.Entry<String, Collection<Double>>> c = categoryValues.entrySet().iterator();
							while (c.hasNext()) {
								Map.Entry<String, Collection<Double>> entry = c.next();
								String name = entry.getKey();
								Collection<Double> values = entry.getValue();
								double[] valueArray = new double[values.size()];
								Iterator<Double> v = values.iterator();
								int valueIndex = 0;
								while (v.hasNext()) {
									valueArray[valueIndex++] = v.next();
								}
								double value = aggregation.calculate(valueArray);

								categoryDataSet.setValue(value, name, dataTable.getColumnName(col));
							}
						} else {
							Iterator<Map.Entry<String, Collection<Double>>> c = categoryValues.entrySet().iterator();
							while (c.hasNext()) {
								Map.Entry<String, Collection<Double>> entry = c.next();
								String name = entry.getKey();
								Collection<Double> values = entry.getValue();
								Iterator<Double> v = values.iterator();
								while (v.hasNext()) {
									double value = v.next();
									categoryDataSet.setValue(value, name, dataTable.getColumnName(col));
								}
							}
						}
					}
					categoryValuesSize = categoryValues.size();
				}
			}
		}
		return categoryValuesSize;
	}

	@Override
	public JComponent getPlotter() {
		return scrollablePlotterPanel;
	}

	@Override
	public JComponent getRenderComponent() {
		return panel;
	}

	@Override
	public void prepareRendering() {
		super.prepareRendering();
		this.showScrollbar = false;
		updatePlotter();
	}

	@Override
	public void finishRendering() {
		super.finishRendering();
		this.showScrollbar = true;
		updatePlotter();
	}

	@Override
	public void updatePlotter() {
		final int categoryCount = prepareData();

		SwingUtilities.invokeLater(new Runnable() {

			@Override
			public void run() {
				scrollablePlotterPanel.remove(viewScrollBar);
			}
		});

		if (categoryCount > MAX_CATEGORY_VIEW_COUNT && showScrollbar) {
			SwingUtilities.invokeLater(new Runnable() {

				@Override
				public void run() {
					viewScrollBar.setOrientation(Adjustable.HORIZONTAL);
					scrollablePlotterPanel.add(viewScrollBar, BorderLayout.SOUTH);
				}
			});

			this.slidingCategoryDataSet = new SlidingCategoryDataset(categoryDataSet, 0, MAX_CATEGORY_VIEW_COUNT);
			viewScrollBar.setMaximum(categoryCount);
			viewScrollBar.setValue(0);

		} else {
			this.slidingCategoryDataSet = null;
		}

		if (categoryCount <= MAX_CATEGORIES) {

			SpiderWebPlot plot = new SpiderWebPlot(categoryDataSet);

			plot.setAxisLinePaint(Color.LIGHT_GRAY);
			plot.setOutlinePaint(Color.WHITE);

			plot.setLabelGenerator(new StandardCategoryItemLabelGenerator());

			JFreeChart chart = new JFreeChart("", TextTitle.DEFAULT_FONT, plot, true);

			double[] colorValues = null;
			if (groupByColumn >= 0 && this.dataTable.isNominal(groupByColumn)) {
				colorValues = new double[this.dataTable.getNumberOfValues(groupByColumn)];
			} else {
				colorValues = new double[categoryDataSet.getColumnCount()];
			}
			for (int i = 0; i < colorValues.length; i++) {
				colorValues[i] = i;
			}

			if (panel != null) {
				panel.setChart(chart);
			} else {
				panel = new AbstractChartPanel(chart, getWidth(), getHeight() - MARGIN);
				scrollablePlotterPanel.add(panel, BorderLayout.CENTER);
				final ChartPanelShiftController controller = new ChartPanelShiftController(panel);
				panel.addMouseListener(controller);
				panel.addMouseMotionListener(controller);
			}

			// set the background color for the chart...
			chart.setBackgroundPaint(Color.white);

			// legend settings
			LegendTitle legend = chart.getLegend();
			if (legend != null) {
				legend.setPosition(RectangleEdge.TOP);
				legend.setFrame(BlockBorder.NONE);
				legend.setHorizontalAlignment(HorizontalAlignment.LEFT);
				legend.setItemFont(LABEL_FONT);
			}
			if (groupByColumn < 0) {
				// no legend is needed when there is no group-by selection
				chart.removeLegend();
			}
			// ATTENTION: WITHOUT THIS WE GET SEVERE MEMORY LEAKS!!!
			panel.getChartRenderingInfo().setEntityCollection(null);
		} else {
			LogService.getRoot().log(Level.INFO, "com.rapidminer.gui.plotter.charts.BarChartPlotter.too_many_columns",
					new Object[] { categoryCount, MAX_CATEGORIES });
		}
	}

	@Override
	public JComponent getOptionsComponent(int index) {
		switch (index) {
			case 0:
				JLabel label = new JLabel("Aggregation:");
				label.setToolTipText("Select the type of the aggregation function which should be used for grouped values.");
				return label;
			case 1:
				return aggregationFunction;
			case 2:
				return useDistinct;
			default:
				return null;
		}
	}

	/** The default implementation delivers an empty set. */
	@Override
	public List<ParameterType> getAdditionalParameterKeys(InputPort inputPort) {
		List<ParameterType> types = super.getAdditionalParameterKeys(inputPort);

		String[] allFunctions = new String[AbstractAggregationFunction.KNOWN_AGGREGATION_FUNCTION_NAMES.length + 1];
		allFunctions[0] = "none";
		System.arraycopy(AbstractAggregationFunction.KNOWN_AGGREGATION_FUNCTION_NAMES, 0, allFunctions, 1,
				AbstractAggregationFunction.KNOWN_AGGREGATION_FUNCTION_NAMES.length);

		types.add(new ParameterTypeCategory(PARAMETER_AGGREGATION,
				"The function used for aggregating the values grouped by the specified column.", allFunctions, 0));
		types.add(new ParameterTypeBoolean(PARAMETER_USE_DISTINCT,
				"Indicates if only distinct values should be regarded for aggregation.", false));
		return types;
	}

	@Override
	public List<PlotterSettingsChangedListener> getListeningObjects() {
		List<PlotterSettingsChangedListener> list = super.getListeningObjects();
		list.add(aggregationFunction);
		list.add(useDistinct);
		return list;
	}

	/** The default implementation does nothing. */
	@Override
	public void setAdditionalParameter(String key, String value) {
		super.setAdditionalParameter(key, value);
		if (PARAMETER_AGGREGATION.equals(key)) {
			try {
				int index = Integer.valueOf(value);
				if (index > 0) {
					aggregationFunctionName = AbstractAggregationFunction.KNOWN_AGGREGATION_FUNCTION_NAMES[index - 1];
				} else {
					aggregationFunctionName = "none";
				}
			} catch (NumberFormatException e) {
				aggregationFunctionName = value;
			}
			updatePlotter();
		} else if (PARAMETER_USE_DISTINCT.equals(key)) {
			useDistinctFlag = Boolean.parseBoolean(value);
			updatePlotter();
		}
	}

	@Override
	public Icon getIcon(int index) {
		return null;
	}

	@Override
	public String getPlotterName() {
		return PlotterConfigurationModel.WEB_PLOT;
	}

}
