/********************************************************************************
 * Copyright (c) 2019-2020 [Open Lowcode SAS](https://openlowcode.com/)
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0 .
 *
 * SPDX-License-Identifier: EPL-2.0
 ********************************************************************************/

package org.openlowcode.client.graphic.widget.schedule;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.logging.Logger;

import org.openlowcode.client.graphic.widget.schedule.DateUtils.CoordinatesWithFlag;
import org.openlowcode.client.runtime.PageActionManager;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ListCell;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.text.Font;
import javafx.scene.text.Text;

/**
 * the graphical component of a Gantt task. It is implemented as a list cell for
 * performance purposes
 * 
 * @author <a href="https://openlowcode.com/" rel="nofollow">Open Lowcode
 *         SAS</a>
 *
 * @param <E> type of Gantt task
 */
public class GanttTaskCell<E extends GanttTask<E>>
		extends
		ListCell<GanttTaskDisplay<E>> {

	private static Logger logger = Logger.getLogger(GanttTaskCell.class.getName());
	public static Color SCALE_GRAY = Color.rgb(193, 193, 199);
	public static Color SCALE_LIGHT_GRAY = Color.rgb(212, 212, 217);
	private GanttTaskDisplay<E> display;
	private GanttCanvas canvas;
	private double minxforclick;
	private double maxxforclick;
	private double minyforclick;
	private double maxyforclick;

	/**
	 * @return get hte parent gantt display widget
	 */
	public GanttDisplay<E> getParentGanttDisplay() {
		return display.getGanttDisplay();
	}

	/**
	 * creates a gannt task cell with the Open Lowcode client action manager as
	 * parent
	 * 
	 * @param actionmanager parent action manager
	 */
	public GanttTaskCell(PageActionManager actionmanager) {

		setStyle("-fx-padding: 0px;");
		canvas = new GanttCanvas();
		canvas.widthProperty().bind(widthProperty());
		canvas.heightProperty().bind(heightProperty());
		StackPane pane = new StackPane();
		pane.getChildren().addAll(canvas);
		setGraphic(pane);
		setContentDisplay(ContentDisplay.GRAPHIC_ONLY);

		this.setOnMouseClicked(new EventHandler<MouseEvent>() {
			@Override
			public void handle(MouseEvent event) {
				if (display != null) {
					logger.finer("Click On Item " + display.getGantttask().getStarttime());
					Bounds bounds = localToScene(getBoundsInLocal());
					double xcoordinates = event.getSceneX() - bounds.getMinX();
					double ycoordinates = event.getSceneY() - bounds.getMinY();

					logger.finer("X,Y coordinates = " + xcoordinates + " - " + ycoordinates);
					logger.finer("Box X (" + minxforclick + "-" + maxxforclick + ") Box Y (" + minyforclick + "-"
							+ maxyforclick + ")");
					if ((xcoordinates >= minxforclick) && (xcoordinates <= maxxforclick)
							&& (ycoordinates >= minyforclick) && (ycoordinates <= maxyforclick)) {
						logger.finer("Impact");
						GanttTaskMouseEventHandler<
								E> handlerontask = display.getGanttDisplay().getEventHandlerOnGanttTaskMouseClicked();
						if (handlerontask != null)
							handlerontask.handle(event, display.getGantttask());
					} else {
						logger.finer("Shoot outside");
						if (event.getButton().equals(MouseButton.SECONDARY)) {
							display.getGanttDisplay().printGantt(actionmanager, event);
						} else {
							EventHandler<MouseEvent> handleroutside = display.getGanttDisplay()
									.getEventHandlerOnClickOutsideOfGanttTask();
							if (handleroutside != null)
								handleroutside.handle(event);
						}
					}
				}
			}

		});
	}

	@Override
	protected void updateItem(GanttTaskDisplay<E> display, boolean empty) {
		this.display = display;
		canvas.setDisplay(display);
		canvas.draw();
	}

	private class GanttCanvas
			extends
			Canvas {

		@SuppressWarnings("unused")
		private String name;
		private Date startdate;
		private Date enddate;
		private Date displaywindowstart;
		private Date displaywindowend;
		private boolean hasdata;
		private String tasktitle;

		private void setDisplay(GanttTaskDisplay<E> display) {
			if (display == null) {
				hasdata = false;
			} else {
				this.startdate = display.getGantttask().getStarttime();
				this.enddate = display.getGantttask().getEndtime();
				if (display.getGanttDisplay().getAttributefortitle() != null) {
					tasktitle = display.getGantttask().getAttribute(display.getGanttDisplay().getAttributefortitle());
				}
				this.displaywindowstart = display.getGanttDisplay().getStartdatedisplaywindow();
				this.displaywindowend = display.getGanttDisplay().getEnddatedisplaywindow();

				hasdata = true;

			}

		}

		private GanttCanvas() {
			widthProperty().addListener(event -> draw());
			heightProperty().addListener(event -> draw());
			widthProperty().addListener(new ChangeListener<Number>() {

				@Override
				public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
					if (display != null)
						if (display.getGanttDisplay() != null)
							display.getGanttDisplay().redrawTitleIfChanged(newValue);
				}

			});
		}

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

		@Override
		public double prefWidth(double height) {
			return getWidth();
		}

		@Override
		public double prefHeight(double width) {
			return getHeight();
		}

		private CoordinatesWithFlag dateToCoordinates(Date date) {
			return DateUtils.genericDateToCoordinates(date, displaywindowstart, displaywindowend,
					display.getGanttDisplay().getBusinessCalendar());

		}

		private boolean isInDisplayWindow(double startratio, double endratio) {
			if ((startratio >= 0) && (startratio <= 1))
				return true;
			if ((endratio >= 0) && (endratio <= 1))
				return true;
			return false;
		}

		/*
		 * Draw a chart based on the data provided by the model.
		 */
		private void draw() {

			GraphicsContext gc = getGraphicsContext2D();

			gc.setFill(Color.WHITE);

			gc.fillRect(0, 0, getWidth(), getHeight());

			if (hasdata) {

				// Step 1 - strikes here separator

				GanttTaskCell.drawSeparators(gc, displaywindowstart, displaywindowend, 0, 0, getHeight(), getWidth(),
						display.getGanttDisplay().getBusinessCalendar(), 0);

				// Step 2A - strikes first separator on passing dependencies;
				CoordinatesWithFlag startratiowithflag = dateToCoordinates(startdate);
				CoordinatesWithFlag endratiowithflag = dateToCoordinates(enddate);

				gc.setStroke(Color.web("#8A7BBE"));
				gc.setLineWidth(1);
				gc.setLineDashes(0);

				ArrayList<GanttDependency<E>> dependenciesinbetween = display.getGanttDisplay().getPlanningtoshow()
						.getDependenciesInBetween(display.getGantttask().getSequenceInPlanning());

				if (dependenciesinbetween != null)
					for (int i = 0; i < dependenciesinbetween.size(); i++) {
						GanttDependency<E> dependency = dependenciesinbetween.get(i);
						CoordinatesWithFlag dependencyverticalratio = dateToCoordinates(
								dependency.getSuccessor().getStarttime());
						boolean validwithmargin = true;
						if (dependencyverticalratio.getValue() < -0.2)
							validwithmargin = false;
						if (dependencyverticalratio.getValue() > 1.2)
							validwithmargin = false;
						if (validwithmargin) {
							gc.strokeLine((long) (dependencyverticalratio.getValue() * getWidth() + 4), 0,
									(long) (dependencyverticalratio.getValue() * getWidth() + 4), (long) (getHeight()));
						}
					}

				double startratio = startratiowithflag.getValue();
				double endratio = endratiowithflag.getValue();

				// Step 2B - draw dependency on predecessor

				ArrayList<GanttDependency<E>> dependenciesfortaskaspredecessor = display.getGanttDisplay()
						.getPlanningtoshow()
						.getDependenciesByPredecessor(display.getGantttask().getSequenceInPlanning());
				double minpredecessordependratio = endratio;
				double maxpredecessordependratio = endratio;
				boolean predecessordependencyexists = false;
				boolean maxtouched = false;
				boolean mintouched = false;
				if (dependenciesfortaskaspredecessor != null)
					for (int i = 0; i < dependenciesfortaskaspredecessor.size(); i++) {
						GanttDependency<E> dependency = dependenciesfortaskaspredecessor.get(i);
						int thisindex = display.getGantttask().getSequenceInPlanning();
						int successorindex = dependency.getSuccessor().getSequenceInPlanning();
						double successorstarttratio = dateToCoordinates(dependency.getSuccessor().getStarttime())
								.getValue();
						if (successorstarttratio > maxpredecessordependratio) {
							maxpredecessordependratio = successorstarttratio;
							maxtouched = true;
						}
						if (successorstarttratio < minpredecessordependratio) {
							minpredecessordependratio = successorstarttratio;
							mintouched = true;
						}
						predecessordependencyexists = true;
						// arrow will go up
						if (thisindex > successorindex) {
							gc.strokeLine((long) (successorstarttratio * getWidth() + 4), (long) (getHeight() / 2),
									(long) (successorstarttratio * getWidth() + 4), 0);

						}
						// arrow will go down
						if (thisindex < successorindex) {
							gc.strokeLine((long) (successorstarttratio * getWidth() + 4), (long) (getHeight() / 2),
									(long) (successorstarttratio * getWidth() + 4), (long) (getHeight()));

						}
					}
				if (predecessordependencyexists) {

					gc.strokeLine((long) (minpredecessordependratio * getWidth() + (mintouched ? 4 : 0)),
							(long) (getHeight() / 2),
							(long) (maxpredecessordependratio * getWidth() + (maxtouched ? 4 : 0)),
							(long) (getHeight() / 2));

				}

				// Step 2C - draw dependency on successor

				ArrayList<GanttDependency<E>> dependenciesfortaskassuccessor = display.getGanttDisplay()
						.getPlanningtoshow().getDependenciesBySuccessor(display.getGantttask().getSequenceInPlanning());
				boolean hastoparrow = false;
				boolean hasbottomarrow = false;
				if (dependenciesfortaskassuccessor != null)
					for (int i = 0; i < dependenciesfortaskassuccessor.size(); i++) {
						GanttDependency<E> dependency = dependenciesfortaskassuccessor.get(i);
						int thisindex = display.getGantttask().getSequenceInPlanning();
						int predecessorindex = dependency.getPredecessor().getSequenceInPlanning();
						if (thisindex > predecessorindex)
							hastoparrow = true;
						if (thisindex < predecessorindex)
							hasbottomarrow = true;
						if (thisindex == predecessorindex)
							logger.warning("  -- found inconsistent dependency " + thisindex
									+ (display.getGanttDisplay().getAttributefortitle() != null
											? " this = "
													+ display.getGantttask().getAttribute(
															display.getGanttDisplay().getAttributefortitle())
													+ ", precedessor = "
													+ dependency.getPredecessor().getAttribute(
															display.getGanttDisplay().getAttributefortitle())
											: " NO LABEL"));
						logger.fine(" Found dependency for this task as successor");
					}
				if (hastoparrow) {
					logger.fine("  - write top arrow for start arrow " + startratio);
					gc.strokeLine((long) (startratio * getWidth() + 4), 0, (long) (startratio * getWidth() + 4),
							(long) (getHeight() * 0.2));
					gc.strokeLine((long) (startratio * getWidth() + 3), 0, (long) (startratio * getWidth() + 4),
							(long) (getHeight() * 0.2));
					gc.strokeLine((long) (startratio * getWidth() + 5), 0, (long) (startratio * getWidth() + 4),
							(long) (getHeight() * 0.2));
				}
				if (hasbottomarrow) {
					logger.fine("  - write bottom arrow for start arrow " + startratio);

					gc.strokeLine((long) (startratio * getWidth() + 4), (long) (getHeight()),
							(long) (startratio * getWidth() + 4), (long) (getHeight() * 0.8));
					gc.strokeLine((long) (startratio * getWidth() + 3), (long) (getHeight()),
							(long) (startratio * getWidth() + 4), (long) (getHeight() * 0.8));
					gc.strokeLine((long) (startratio * getWidth() + 5), (long) (getHeight()),
							(long) (startratio * getWidth() + 4), (long) (getHeight() * 0.8));
				}

				// Next Step - draw task itself

				boolean valid = true;
				if (startratiowithflag.isOutofrange())
					valid = false;
				if (endratiowithflag.isOutofrange())
					valid = false;
				gc.setLineWidth(0.5);
				Font font = Font.font(12);
				gc.setFont(font);
				gc.setStroke(Color.rgb(255, 255, 255, 0.7));
				gc.setFill(Color.rgb(255, 255, 255, 0.7));
				double maxratio = endratio;
				double minratio = startratio;
				if (startratio > endratio) {
					maxratio = startratio;
					minratio = endratio;
				}

				if (tasktitle != null) {
					if (maxratio > 0.7) {
						Text label = new Text(tasktitle);
						label.setFont(font);
						double labelwidth = label.getBoundsInLocal().getWidth();
						long starttext = (long) (minratio * getWidth() - labelwidth - 5);
						gc.fillText(tasktitle, starttext - 1, 14);
						gc.fillText(tasktitle, starttext + 1, 14);

					} else {
						gc.fillText(tasktitle, (long) (maxratio * getWidth() + 5 - 1), 14);
						gc.fillText(tasktitle, (long) (maxratio * getWidth() + 5 + 1), 14);

					}
				}
				gc.setStroke(Color.rgb(0, 0, 0, 1));
				gc.setFill(Color.rgb(0, 0, 0, 1));

				if (tasktitle != null) {

					if (maxratio > 0.7) {
						Text label = new Text(tasktitle);
						label.setFont(font);
						double labelwidth = label.getBoundsInLocal().getWidth();
						long starttext = (long) (minratio * getWidth() - labelwidth - 5);
						gc.fillText(tasktitle, starttext, 14);

					} else {
						gc.fillText(tasktitle, (long) (maxratio * getWidth() + 5), 14);

					}
				}

				if (isInDisplayWindow(startratio, endratio)) {
					if (startratio < 0)
						startratio = 0;
					if (startratio > 1)
						startratio = 1;
					if (endratio < 0)
						endratio = 0;
					if (endratio > 1)
						endratio = 1;
					Color color = Color.SKYBLUE;
					String attributetomapforcolor = display.getGanttDisplay().getAttributeMappingForColor();
					if (attributetomapforcolor != null) {
						String taskattributevalue = display.getGantttask().getAttribute(attributetomapforcolor);
						Color specialcolor = display.getGanttDisplay().getColorForAttributeValue(taskattributevalue);
						if (specialcolor != null)
							color = specialcolor;
					}

					Stop[] stops = new Stop[] { new Stop(0, color), new Stop(1, color.darker().darker()) };
					LinearGradient gradient = new LinearGradient(0, 0, 0, 300, false, CycleMethod.NO_CYCLE, stops);

					gc.setFill(gradient);

					if (valid) {
						gc.setStroke(Color.BLACK);
					} else {
						gc.setStroke(Color.RED);
					}
					gc.setLineWidth(0.5);

					boolean small = false;
					double lengthinpc = Math.abs(startratio - endratio);
					if (lengthinpc < 0.002)
						small = true;

					if (small) {

						double centerx = startratio * getWidth();
						double centery = getHeight() * 0.5;
						double diamondradius = getHeight() * 0.2;
						minxforclick = centerx - diamondradius;
						maxxforclick = centerx + diamondradius;
						minyforclick = centery - diamondradius;
						maxyforclick = centery + diamondradius;
						double[] polyx = new double[] { centerx - diamondradius, centerx, centerx + diamondradius,
								centerx };
						double[] polyy = new double[] { centery, centery - diamondradius, centery,
								centery + diamondradius };
						gc.fillPolygon(polyx, polyy, 4);
						gc.strokePolygon(polyx, polyy, 4);

					} else {
						if (endratio > startratio) {
							gc.fillRoundRect((long) (startratio * getWidth()), (long) (getHeight() * 0.2),
									(long) ((endratio - startratio) * getWidth()), (long) (getHeight() * 0.6), 5, 5);

							gc.strokeRoundRect((long) (startratio * getWidth()), (long) (getHeight() * 0.2),
									(long) ((endratio - startratio) * getWidth()), (long) (getHeight() * 0.6), 5, 5);
							minxforclick = startratio * getWidth();
							maxxforclick = endratio * getWidth();
							minyforclick = getHeight() * 0.2;
							maxyforclick = getHeight() * 0.8;
						} else {
							double oldendratio = endratio;
							endratio = startratio;
							startratio = oldendratio;
							gc.setStroke(Color.RED);
							gc.fillRoundRect((long) (startratio * getWidth()), (long) (getHeight() * 0.2),
									(long) ((endratio - startratio) * getWidth()), (long) (getHeight() * 0.6), 5, 5);
							gc.strokeRoundRect((long) (startratio * getWidth()), (long) (getHeight() * 0.2),
									(long) ((endratio - startratio) * getWidth()), (long) (getHeight() * 0.6), 5, 5);
							minxforclick = startratio * getWidth();
							maxxforclick = endratio * getWidth();
							minyforclick = getHeight() * 0.2;
							maxyforclick = getHeight() * 0.8;

						}
					}

					// build color dot if required
					String attributetomapfordot = display.getGanttDisplay().getAttributeMappingForDot();
					if (attributetomapfordot != null) {
						String taskattributevalue = display.getGantttask().getAttribute(attributetomapfordot);
						Color specialcolor = display.getGanttDisplay().getColorForDot(taskattributevalue);
						if (specialcolor != null) {
							long circlecenterx = (long) (startratio * getWidth());
							long circlecentery = (long) (getHeight() * 0.2);
							gc.setFill(specialcolor);
							gc.setStroke(Color.BLACK);
							gc.fillOval(circlecenterx - 2, circlecentery - 2, 5, 5);
							gc.strokeOval(circlecenterx - 2, circlecentery - 2, 5, 5);

						}
					}
				}

			}
		}
	}

	/**
	 * true if the date is a monday
	 * 
	 * @param date date to analyze
	 * @return true if the date is a monday
	 */
	public static boolean isMonday(Date date) {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);

		if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.MONDAY)
			return true;
		return false;
	}

	/**
	 * return true if lines should not be drawn for hours in the scales of the
	 * planning
	 * 
	 * @param startsofdates days of start of days
	 * @return true id reduced display (less than 30 days shown)
	 */
	public static boolean isReducedDisplay(Date[] startsofdates) {
		if (startsofdates.length > 30)
			return true;
		return false;
	}

	/**
	 * draw all separators in the gantt task cell
	 * 
	 * @param gc                     graphics context
	 * @param startdatedisplaywindow start date (first day) of the display window
	 * @param enddatedisplaywindow   end date (last day) of the display window
	 * @param ystarthour             start hour of the planning
	 * @param ystartday              start day of the planning
	 * @param yend                   end of the cell in pixel
	 * @param totalwidth             total width in pixel
	 * @param businesscalendar       business calendar to use for displaying of
	 *                               opening hours
	 * @param extraxoffset           extra offset in the display
	 */
	public static void drawSeparators(
			GraphicsContext gc,
			Date startdatedisplaywindow,
			Date enddatedisplaywindow,
			double ystarthour,
			double ystartday,
			double yend,
			double totalwidth,
			BusinessCalendar businesscalendar,
			float extraxoffset) {
		Date[] separatorstoconsider = DateUtils.getAllStartOfDays(startdatedisplaywindow, enddatedisplaywindow,
				businesscalendar);
		boolean isreduceddisplay = isReducedDisplay(separatorstoconsider);

		for (int i = 0; i < separatorstoconsider.length; i++) {
			if (isreduceddisplay) {
				gc.setLineWidth(0.5);

				gc.setStroke(GanttTaskCell.SCALE_LIGHT_GRAY);
			}
			if (!isreduceddisplay) {
				gc.setLineWidth(1);

				gc.setStroke(GanttTaskCell.SCALE_GRAY);
			}

			gc.setEffect(null);
			Date separatortoprint = separatorstoconsider[i];
			if (isMonday(separatortoprint)) {
				gc.setStroke(GanttTaskCell.SCALE_GRAY);
				if (!isreduceddisplay)
					gc.setLineWidth(2);
				if (isreduceddisplay)
					gc.setLineWidth(1);

			}
			double separatorratio = DateUtils.genericDateToCoordinates(separatortoprint, startdatedisplaywindow,
					enddatedisplaywindow, businesscalendar).getValue();
			gc.strokeLine((long) (separatorratio * totalwidth + extraxoffset), (long) ystartday,
					(long) (separatorratio * totalwidth + extraxoffset), (long) (yend));
			if (!isreduceddisplay)
				if (separatorstoconsider.length < 30) {
					for (int j = businesscalendar.getDaywindowhourstart() + 1; j < businesscalendar
							.getDaywindowhourend(); j++) {
						Date hour = new Date(separatortoprint.getTime()
								+ (j - businesscalendar.getDaywindowhourstart()) * 3600 * 1000);
						double hourratio = DateUtils.genericDateToCoordinates(hour, startdatedisplaywindow,
								enddatedisplaywindow, businesscalendar).getValue();
						gc.setStroke(GanttTaskCell.SCALE_LIGHT_GRAY);
						gc.setLineWidth(0.5);

						gc.setEffect(null);
						gc.strokeLine((long) (hourratio * totalwidth + extraxoffset), (long) (ystarthour),
								(long) (hourratio * totalwidth + extraxoffset), (long) (yend));

					}
				}
		}

	}
}