package com.baselet.element.elementnew.plot;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.baselet.control.Matrix;
import com.baselet.control.basics.geom.Dimension;
import com.baselet.control.enums.AlignHorizontal;
import com.baselet.control.enums.ElementId;
import com.baselet.diagram.draw.DrawHandler;
import com.baselet.diagram.draw.helper.theme.Theme;
import com.baselet.diagram.draw.helper.theme.ThemeFactory;
import com.baselet.element.NewGridElement;
import com.baselet.element.elementnew.plot.drawer.PlotGridDrawConfig;
import com.baselet.element.elementnew.plot.elements.AbstractPlot;
import com.baselet.element.elementnew.plot.elements.BarPlot;
import com.baselet.element.elementnew.plot.elements.LinePlot;
import com.baselet.element.elementnew.plot.elements.PiePlot;
import com.baselet.element.elementnew.plot.elements.ScatterPlot;
import com.baselet.element.elementnew.plot.parser.Parser;
import com.baselet.element.elementnew.plot.parser.ParserException;
import com.baselet.element.elementnew.plot.parser.ParserResult;
import com.baselet.element.elementnew.plot.parser.PlotConstants;
import com.baselet.element.elementnew.plot.parser.PlotConstants.PlotType;
import com.baselet.element.elementnew.plot.parser.PlotState;
import com.baselet.element.facet.Facet;
import com.baselet.element.facet.PropertiesParserState;
import com.baselet.element.facet.Settings;
import com.baselet.element.settings.SettingsManualresizeCenter;
import com.baselet.gui.AutocompletionText;

public class PlotGrid extends NewGridElement {

	private static final Logger log = LoggerFactory.getLogger(PlotGrid.class);

	private Matrix<List<AbstractPlot>> matrix;

	private Integer gridWidth;
	private Double minValue;
	private Double maxValue;

	/**
	 * this facet is only here to show autocompletion and include PlotGrid in the new parser logic which uses facets
	 */
	public static final Facet PSEUDO_PLOT_FACET = new Facet() {
		@Override
		public void handleLine(String line, PropertiesParserState propConfig) {
			// do nothing
		}

		@Override
		public List<AutocompletionText> getAutocompletionStrings() {
			return PlotConstants.AUTOCOMPLETION_LIST;
		}

		@Override
		public boolean checkStart(String line, PropertiesParserState propConfig) {
			return true;
		}
	};

	private void fillWithPlots(ParserResult parserState) {
		ArrayList<PlotState> plotStateList = parserState.getPlotStateList();
		DrawHandler drawer = parserState.getDrawer();
		setOverallMinMaxValue(plotStateList);
		for (PlotState plotState : plotStateList) {
			Integer xPos = plotState.getValueAsInt(PlotConstants.KEY_INT_X_POSITION, null);
			Integer yPos = plotState.getValueAsInt(PlotConstants.KEY_INT_Y_POSITION, null);
			// 1 is subtracted from the values because the user counts from 1 to x; java counts from 0 to x-1
			if (xPos != null) {
				xPos -= 1;
			}
			if (yPos != null) {
				yPos -= 1;
			}

			// case1: x and y are specified
			if (xPos != null && yPos != null) {
				setMatrixHeightMinimum(yPos);
				List<List<AbstractPlot>> xCoordinateList = matrix.row(yPos);
				setMinimumListSize(xPos, xCoordinateList);
				xCoordinateList.set(xPos, createPlots(drawer, plotState, xPos, yPos, "x and y are specified"));
			}
			// case2: only x is specified
			else if (xPos != null) {
				putPlotInFirstFreeVerticalSpaceOrAddPlot(drawer, xPos, plotState, "only x is specified -> space replaced");
			}
			// case3: only y is specified
			else if (yPos != null) {
				setMatrixHeightMinimum(yPos);
				List<List<AbstractPlot>> xCoordinateList = matrix.row(yPos);
				putPlotInFirstFreeHorizontalSpaceOrAddPlot(drawer, xCoordinateList, yPos, plotState, "only y specified -> ");
			}
			// case4: no coordinate is specified
			else {
				putPlotInFirstFreeMatrixSpace(drawer, plotState);
			}
		}
		gridWidth = matrix.cols(); // Recalculate grid width
		log.debug("\n" + toString() + "\n");
	}

	private void setOverallMinMaxValue(List<PlotState> plotStateList) {
		minValue = Double.MAX_VALUE;
		maxValue = Double.MIN_VALUE;
		Double[][] data;
		for (PlotState state : plotStateList) {
			data = state.getDataSet().data();
			for (Double[] dArray : data) {
				for (Double d : dArray) {
					if (d > maxValue) {
						maxValue = d;
					}
					if (d < minValue) {
						minValue = d;
					}
				}
			}
		}
	}

	private void setMatrixHeightMinimum(Integer minHeight) {
		while (minHeight > matrix.rows() - 1) {
			matrix.addLine(new ArrayList<List<AbstractPlot>>());
		}
	}

	private void setMinimumListSize(Integer minWidth, List<List<AbstractPlot>> lineToSet) {
		while (minWidth > lineToSet.size() - 1) {
			lineToSet.add(null);
		}
	}

	private void putPlotInFirstFreeVerticalSpaceOrAddPlot(DrawHandler drawer, Integer xFix, PlotState plotState, String info) {
		boolean plotFilledInFreeSpace = false;
		for (int ySeq = 0; ySeq < matrix.rows(); ySeq++) {
			List<List<AbstractPlot>> xCoordinateList = matrix.row(ySeq);
			if (xFix >= xCoordinateList.size()) {
				setMinimumListSize(xFix, xCoordinateList);
			}
			if (xCoordinateList.get(xFix) == null) {
				xCoordinateList.set(xFix, createPlots(drawer, plotState, xFix, ySeq, info));
				plotFilledInFreeSpace = true;
				break;
			}
		}
		// If there is no free space available for the plot, a new line must be added
		if (!plotFilledInFreeSpace) {
			ArrayList<List<AbstractPlot>> newColumn = new ArrayList<List<AbstractPlot>>();
			setMinimumListSize(xFix, newColumn);
			newColumn.set(xFix, createPlots(drawer, plotState, xFix, matrix.rows(), "only x is specified -> expanded y-list"));
			matrix.addLine(newColumn);
		}
	}

	private void putPlotInFirstFreeHorizontalSpaceOrAddPlot(DrawHandler drawer, List<List<AbstractPlot>> xCoordinateList, Integer yFix, PlotState plotState, String info) {
		for (int xSeq = 0; true; xSeq++) {
			if (xSeq == xCoordinateList.size()) {
				xCoordinateList.add(createPlots(drawer, plotState, xSeq, yFix, info + "added new x-entry"));
				return;
			}
			if (xCoordinateList.get(xSeq) == null) {
				xCoordinateList.set(xSeq, createPlots(drawer, plotState, xSeq, yFix, info + "replaced x-entry"));
				return;
			}
		}
	}

	private void putPlotInFirstFreeMatrixSpace(DrawHandler drawer, PlotState plotState) {
		// Go through all lines and all values in each line
		for (int ySeq = 0; ySeq < matrix.rows(); ySeq++) {
			List<List<AbstractPlot>> oneLine = matrix.row(ySeq);

			for (int xSeq = 0; xSeq < oneLine.size(); xSeq++) {
				List<AbstractPlot> oneValue = oneLine.get(xSeq);
				// If a free space is found use it
				if (oneValue == null) {
					oneLine.set(xSeq, createPlots(drawer, plotState, xSeq, ySeq, "no coordinate specified -> free space found"));
					return;
				}
			}
			// If the actual x-coordinates line is < than the default grid width add a new value
			if (oneLine.size() < gridWidth) {
				oneLine.add(createPlots(drawer, plotState, oneLine.size(), ySeq, "no coordinate specified -> expanded x-list"));
				return;
			}
		}
		// If every space in the matrix is occupied and the position is still not found add a new line and fill its first place
		List<List<AbstractPlot>> newLine = new ArrayList<List<AbstractPlot>>();
		newLine.add(createPlots(drawer, plotState, 0, matrix.rows(), "no coordinate specified -> every matrix space occupied, expanded y-list"));
		matrix.addLine(newLine);
	}

	private List<AbstractPlot> createPlots(DrawHandler drawer, PlotState plotState, Integer xPos, Integer yPos, String info) {
		List<AbstractPlot> plotList = new ArrayList<AbstractPlot>();

		// create and add base plot
		plotList.add(createPlot(drawer, plotState, xPos, yPos, info));

		// create and add sub plots
		for (PlotState subPlotState : plotState.getSubplots()) {
			plotList.add(createPlot(drawer, subPlotState, xPos, yPos, info));
		}

		return plotList;
	}

	private AbstractPlot createPlot(DrawHandler drawer, PlotState plotState, int xPos, int yPos, String info) {
		String type = plotState.getValueValidated(PlotType.getKey(), PlotType.Bar.getValue(), PlotConstants.toStringList(PlotType.values()));
		log.debug("PlotGrid insert : " + type + " (" + xPos + ";" + yPos + ") " + info);
		PlotGridDrawConfig plotDrawConfig = new PlotGridDrawConfig(getRealSize(), new Dimension(getRectangle().width, getRectangle().height), minValue, maxValue);
		if (PlotType.Pie.getValue().equals(type)) {
			return new PiePlot(drawer, plotDrawConfig, plotState, xPos, yPos);
		}
		else if (PlotType.Line.getValue().equals(type)) {
			return new LinePlot(drawer, plotDrawConfig, plotState, xPos, yPos);
		}
		else if (PlotType.Scatter.getValue().equals(type)) {
			return new ScatterPlot(drawer, plotDrawConfig, plotState, xPos, yPos);
		}
		else {
			return new BarPlot(drawer, plotDrawConfig, plotState, xPos, yPos);
		}
	}

	public void drawPlots() {
		for (int row = 0; row < matrix.rows(); row++) {
			for (int col = 0; col < matrix.row(row).size(); col++) {
				List<AbstractPlot> oneCell = matrix.cell(row, col);
				for (AbstractPlot onePlot : oneCell) {
					if (onePlot != null) {
						if (col != onePlot.getXPosition()) {
							log.error("Plot contains wrong coordinates: " + col + " != " + onePlot.getXPosition());
						}
						if (row != onePlot.getYPosition()) {
							log.error("Plot contains wrong coordinates: " + row + " != " + onePlot.getYPosition());
						}
						onePlot.plot(matrix.cols(), matrix.rows());
					}
				}
			}
		}
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder("------------------------------\n");
		for (int i = 0; i < matrix.rows(); i++) {
			List<List<AbstractPlot>> row = matrix.row(i);
			for (List<AbstractPlot> oneCell : row) {
				for (AbstractPlot onePlot : oneCell) {
					if (onePlot == null) {
						sb.append("null\t");
					}
					else {
						sb.append(onePlot.getPlotLineNr()).append("\t");
					}
				}
			}
			sb.append("\n");
		}
		return sb.append("------------------------------").toString();
	}

	@Override
	protected void drawCommonContent(PropertiesParserState state) {
		DrawHandler drawer = state.getDrawer();
		try {
			matrix = new Matrix<List<AbstractPlot>>();
			ParserResult parserState = new Parser().parse(getPanelAttributes());
			parserState.setDrawer(drawer);
			log.debug(parserState.toString());

			gridWidth = Integer.parseInt(parserState.getPlotGridValue(PlotConstants.KEY_INT_GRID_WIDTH, PlotConstants.GRID_WIDTH_DEFAULT));

			fillWithPlots(parserState);

			drawPlots();

		} catch (ParserException e) {
			Theme currentTheme = ThemeFactory.getCurrentTheme();
			drawer.setForegroundColor(currentTheme.getColor(Theme.PredefinedColors.RED));
			drawer.setBackgroundColor(currentTheme.getColor(Theme.PredefinedColors.WHITE));
			drawer.drawRectangle(0, 0, getRectangle().width - 1, getRectangle().height - 1);
			float x = getRectangle().getWidth() / 2.0f;
			drawer.print(e.getMessage(), x, getRealSize().height / 2.0, AlignHorizontal.CENTER);
		}
	}

	@Override
	protected Settings createSettings() {
		return new SettingsManualresizeCenter() {

			@Override
			protected List<Facet> createFacets() {
				return listOf(PSEUDO_PLOT_FACET); // no real facets should be used
			}
		};
	}

	@Override
	public ElementId getId() {
		return ElementId.PlotGrid;
	}

}
