/*
 * This file is part of LaTeXDraw.
 * Copyright (c) 2005-2020 Arnaud BLOUIN
 * LaTeXDraw is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later version.
 * LaTeXDraw is distributed without any warranty; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 */
package net.sf.latexdraw.instrument;

import io.github.interacto.jfx.interaction.library.DnD;
import java.net.URL;
import java.util.List;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.Initializable;
import javafx.geometry.Point3D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import net.sf.latexdraw.command.shape.ModifyShapeProperty;
import net.sf.latexdraw.command.shape.MoveCtrlPoint;
import net.sf.latexdraw.command.shape.MovePointShape;
import net.sf.latexdraw.command.shape.RotateShapes;
import net.sf.latexdraw.command.shape.ScaleShapes;
import net.sf.latexdraw.command.shape.ShapeProperties;
import net.sf.latexdraw.handler.ArcAngleHandler;
import net.sf.latexdraw.handler.CtrlPointHandler;
import net.sf.latexdraw.handler.Handler;
import net.sf.latexdraw.handler.MovePtHandler;
import net.sf.latexdraw.handler.RotationHandler;
import net.sf.latexdraw.handler.ScaleHandler;
import net.sf.latexdraw.model.ShapeFactory;
import net.sf.latexdraw.model.api.shape.Arc;
import net.sf.latexdraw.model.api.shape.BezierCurve;
import net.sf.latexdraw.model.api.shape.ControlPointShape;
import net.sf.latexdraw.model.api.shape.Drawing;
import net.sf.latexdraw.model.api.shape.Group;
import net.sf.latexdraw.model.api.shape.ModifiablePointsShape;
import net.sf.latexdraw.model.api.shape.Point;
import net.sf.latexdraw.model.api.shape.Position;
import net.sf.latexdraw.model.api.shape.Shape;
import net.sf.latexdraw.util.Inject;
import net.sf.latexdraw.view.jfx.Canvas;
import net.sf.latexdraw.view.jfx.MagneticGrid;
import org.jetbrains.annotations.NotNull;

/**
 * This instrument manages the selected views.
 * @author Arnaud BLOUIN
 */
public class Border extends CanvasInstrument implements Initializable {
	/** The handlers that scale shapes. */
	final List<ScaleHandler> scaleHandlers;
	/** The handlers that move points. */
	final ObservableList<MovePtHandler> mvPtHandlers;
	/** The handlers that move first control points. */
	final ObservableList<CtrlPointHandler> ctrlPt1Handlers;
	/** The handlers that move second control points. */
	final ObservableList<CtrlPointHandler> ctrlPt2Handlers;
	/** The handler that sets the start angle of an arc. */
	final ArcAngleHandler arcHandlerStart;
	/** The handler that sets the end angle of an arc. */
	final ArcAngleHandler arcHandlerEnd;
	/** The handler that rotates shapes. */
	RotationHandler rotHandler;

	private final @NotNull ShapeCoordDimCustomiser coordDimCustomiser;

	@Inject
	public Border(final Canvas canvas, final MagneticGrid grid, final ShapeCoordDimCustomiser coordDimCustomiser) {
		super(canvas, grid);
		this.coordDimCustomiser = Objects.requireNonNull(coordDimCustomiser);
		mvPtHandlers = FXCollections.observableArrayList();
		ctrlPt1Handlers = FXCollections.observableArrayList();
		ctrlPt2Handlers = FXCollections.observableArrayList();
		arcHandlerStart = new ArcAngleHandler(true);
		arcHandlerEnd = new ArcAngleHandler(false);
		scaleHandlers = FXCollections.observableArrayList();
	}


	@Override
	public void initialize(final URL location, final ResourceBundle resources) {
		scaleHandlers.add(new ScaleHandler(Position.NW, canvas.getSelectionBorder()));
		scaleHandlers.add(new ScaleHandler(Position.NORTH, canvas.getSelectionBorder()));
		scaleHandlers.add(new ScaleHandler(Position.NE, canvas.getSelectionBorder()));
		scaleHandlers.add(new ScaleHandler(Position.WEST, canvas.getSelectionBorder()));
		scaleHandlers.add(new ScaleHandler(Position.EAST, canvas.getSelectionBorder()));
		scaleHandlers.add(new ScaleHandler(Position.SW, canvas.getSelectionBorder()));
		scaleHandlers.add(new ScaleHandler(Position.SOUTH, canvas.getSelectionBorder()));
		scaleHandlers.add(new ScaleHandler(Position.SE, canvas.getSelectionBorder()));

		rotHandler = new RotationHandler(canvas.getSelectionBorder());

		scaleHandlers.forEach(handler -> canvas.addToWidgetLayer(handler));
		canvas.addToWidgetLayer(rotHandler);
		canvas.addToWidgetLayer(arcHandlerStart);
		canvas.addToWidgetLayer(arcHandlerEnd);

		canvas.getDrawing().getSelection().getShapes().addListener(
			(ListChangeListener.Change<? extends Shape> evt) -> setActivated(!canvas.getDrawing().getSelection().isEmpty()));

		setActivated(false);
	}


	@Override
	public void setActivated(final boolean activated) {
		super.setActivated(activated);
		scaleHandlers.forEach(handler -> handler.setVisible(activated));
		rotHandler.setVisible(activated);

		if(activated) {
			updatePointsHandlers();
		}else {
			mvPtHandlers.forEach(handler -> handler.setVisible(false));
			ctrlPt1Handlers.forEach(handler -> handler.setVisible(false));
			ctrlPt2Handlers.forEach(handler -> handler.setVisible(false));
			arcHandlerStart.setVisible(false);
			arcHandlerEnd.setVisible(false);
		}
	}


	private void updatePointsHandlers() {
		final Group selection = canvas.getDrawing().getSelection();

		if(selection.size() == 1) {
			selection.getShapeAt(0).ifPresent(sh -> {
				updateMvPtHandlers(sh);
				updateCtrlPtHandlers(sh);
				updateArcHandlers(sh);
			});
		}
	}

	private void updateArcHandlers(final Shape selectedShape) {
		if(selectedShape instanceof Arc) {
			final Arc arc = (Arc) selectedShape;
			arcHandlerStart.setCurrentArc(arc);
			arcHandlerEnd.setCurrentArc(arc);
			arcHandlerStart.setVisible(true);
			arcHandlerEnd.setVisible(true);
		}else {
			arcHandlerStart.setVisible(false);
			arcHandlerEnd.setVisible(false);
		}
	}

	private void updateMvPtHandlers(final Shape selectedShape) {
		if(selectedShape instanceof ModifiablePointsShape) {
			initialisePointHandler(mvPtHandlers, pt -> new MovePtHandler(pt), selectedShape.getPoints());
		}
	}

	private void updateCtrlPtHandlers(final Shape selectedShape) {
		if(selectedShape instanceof BezierCurve) {
			final BezierCurve pts = (BezierCurve) selectedShape;
			initialisePointHandler(ctrlPt1Handlers, pt -> new CtrlPointHandler(pt), pts.getFirstCtrlPts());
			initialisePointHandler(ctrlPt2Handlers, pt -> new CtrlPointHandler(pt), pts.getSecondCtrlPts());
		}
	}

	private <T extends Node & Handler> void initialisePointHandler(final List<T> handlers, final Function<Point, T> supplier, final List<Point> pts) {
		handlers.forEach(handler -> {
			canvas.removeFromWidgetLayer(handler);
			handler.flush();
		});
		handlers.clear();

		pts.forEach(pt -> {
			final T handler = supplier.apply(pt);
			canvas.addToWidgetLayer(handler);
			handlers.add(handler);
		});
	}

	private void configureMovePointBinding() {
		nodeBinder()
			.usingInteraction(DnD::new)
			.toProduce(i -> new MovePointShape((ModifiablePointsShape) canvas.getDrawing().getSelection().getShapeAt(0).orElseThrow(),
				i.getSrcObject().filter(o -> o instanceof MovePtHandler).map(o -> ((MovePtHandler) o).getPoint()).orElseThrow()))
			.on(mvPtHandlers)
			.then((i, c) -> {
				i.getSrcObject().ifPresent(node -> {
					final Point3D startPt = node.localToParent(i.getSrcLocalPoint());
					final Point3D endPt = node.localToParent(i.getTgtLocalPoint());
					final Point ptToMove = ((MovePtHandler) node).getPoint();
					final double x = ptToMove.getX() + endPt.getX() - startPt.getX();
					final double y = ptToMove.getY() + endPt.getY() - startPt.getY();
					c.setNewCoord(grid.getTransformedPointToGrid(new Point3D(x, y, 0d)));
				});
				canvas.update();
			})
			.continuousExecution()
			.when(i -> i.getSrcLocalPoint() != null && i.getTgtLocalPoint() != null && i.getSrcObject().orElse(null) instanceof MovePtHandler &&
				canvas.getDrawing().getSelection().size() == 1 && canvas.getDrawing().getSelection().getShapeAt(0).filter(s -> s instanceof ModifiablePointsShape).isPresent())
			.end(() -> coordDimCustomiser.update())
			.bind();
	}

	@Override
	protected void configureBindings() {
		configureDnD2ScaleBinding();

		configureMovePointBinding();

		nodeBinder()
			.usingInteraction(DnD::new)
			.toProduce(i -> new MoveCtrlPoint((ControlPointShape) canvas.getDrawing().getSelection().getShapeAt(0).orElseThrow(),
				i.getSrcObject().map(h -> ((CtrlPointHandler) h).getPoint()).orElseThrow(), ctrlPt1Handlers.contains(i.getSrcObject().orElseThrow())))
			.on(ctrlPt1Handlers)
			.on(ctrlPt2Handlers)
			.then((i, c) -> {
				final Point3D startPt = i.getSrcObject().map(n -> n.localToParent(i.getSrcLocalPoint())).orElseGet(() -> new Point3D(0d, 0d, 0d));
				final Point3D endPt = i.getSrcObject().map(n -> n.localToParent(i.getTgtLocalPoint())).orElseGet(() -> new Point3D(0d, 0d, 0d));
				final Point ptToMove = i.getSrcObject().map(n -> ((CtrlPointHandler) n).getPoint()).orElseGet(() -> ShapeFactory.INST.createPoint());
				final double x = ptToMove.getX() + endPt.getX() - startPt.getX();
				final double y = ptToMove.getY() + endPt.getY() - startPt.getY();
				c.setNewCoord(grid.getTransformedPointToGrid(new Point3D(x, y, 0d)));
			})
			.when(() -> canvas.getDrawing().getSelection().size() == 1 && canvas.getDrawing().getSelection().getShapeAt(0).filter(s -> s instanceof ControlPointShape).isPresent())
			.continuousExecution()
			.end(() -> coordDimCustomiser.update())
			.bind();

		nodeBinder()
			.usingInteraction(DnD::new)
			.toProduce(() -> new RotateShapes(canvas.getDrawing().getSelection().getGravityCentre().add(canvas.getOrigin()),
				canvas.getDrawing().getSelection().duplicateDeep(false), 0d))
			.on(rotHandler)
			.then((i, c) -> {
				c.setRotationAngle(c.getGc().computeRotationAngle(
					ShapeFactory.INST.createPoint(canvas.sceneToLocal(i.getSrcScenePoint())),
					ShapeFactory.INST.createPoint(canvas.sceneToLocal(i.getTgtScenePoint()))));
				canvas.update();
			})
			.continuousExecution()
			.bind();

		bindArcHandler();
	}

	private void bindArcHandler() {
		nodeBinder()
			.usingInteraction(DnD::new)
			.toProduce(i -> new ModifyShapeProperty<>(i.getSrcObject().orElse(null) == arcHandlerStart ?
				ShapeProperties.ARC_START_ANGLE : ShapeProperties.ARC_END_ANGLE, canvas.getDrawing().getSelection().duplicateDeep(false), null))
			.on(arcHandlerStart, arcHandlerEnd)
			.then((i, c) -> {
				canvas.getDrawing().getSelection().getShapeAt(0).map(s -> (Arc) s).ifPresent(shape -> {
					final Point gc = c.getShapes().getGravityCentre();
					final Point gap = ShapeFactory.INST.createPoint(i.getSrcObject().map(n -> n.localToParent(i.getSrcLocalPoint())).orElse(null)).
						rotatePoint(shape.getGravityCentre(), -shape.getRotationAngle()).
						substract(i.getSrcObject().orElse(null) == arcHandlerStart ? shape.getStartPoint() : shape.getEndPoint());
					final Point position = ShapeFactory.INST.createPoint(i.getSrcObject().map(n -> n.localToParent(i.getTgtLocalPoint())).orElse(null)).
						rotatePoint(c.getShapes().getGravityCentre(), -c.getShapes().getRotationAngle()).
						substract(gap);
					final double angle = Math.acos((position.getX() - gc.getX()) / position.distance(gc));
					c.setValue(position.getY() > gc.getY() ? 2d * Math.PI - angle : angle);
				});
				canvas.update();
			})
			.when(i -> i.getSrcObject().isPresent() && i.getSrcLocalPoint() != null && i.getTgtLocalPoint() != null && canvas.getDrawing().getSelection().size() == 1)
			.continuousExecution()
			.bind();
	}


	private void configureDnD2ScaleBinding() {
		final AtomicInteger xgap = new AtomicInteger();
		final AtomicInteger ygap = new AtomicInteger();

		nodeBinder()
			.usingInteraction(DnD::new)
			.toProduce(i -> new ScaleShapes(canvas.getDrawing().getSelection().duplicateDeep(false), canvas.getDrawing(),
					i.getSrcObject().map(h -> ((ScaleHandler) h).getPosition().getOpposite()).orElse(Position.SW)))
			.on(scaleHandlers.toArray(new Node[0]))
			.continuousExecution()
			.first((i, c) -> {
				final Drawing drawing = canvas.getDrawing();
				final Point br = drawing.getSelection().getBottomRightPoint();
				final Point tl = drawing.getSelection().getTopLeftPoint();
				final Point srcPt = ShapeFactory.INST.createPoint(i.getSrcObject()
					.map(n -> n.localToParent(i.getSrcLocalPoint())).orElse(null));

				switch(c.getRefPosition()) {
					case EAST -> {
						xgap.set((int) (tl.getX() - srcPt.getX()));
						canvas.setCursor(Cursor.W_RESIZE);
					}
					case NE -> {
						xgap.set((int) (tl.getX() - srcPt.getX()));
						ygap.set((int) (srcPt.getY() - br.getY()));
						canvas.setCursor(Cursor.SW_RESIZE);
					}
					case NORTH -> {
						ygap.set((int) (srcPt.getY() - br.getY()));
						canvas.setCursor(Cursor.S_RESIZE);
					}
					case NW -> {
						xgap.set((int) (srcPt.getX() - br.getX()));
						ygap.set((int) (srcPt.getY() - br.getY()));
						canvas.setCursor(Cursor.SE_RESIZE);
					}
					case SE -> {
						xgap.set((int) (tl.getX() - srcPt.getX()));
						ygap.set((int) (tl.getY() - srcPt.getY()));
						canvas.setCursor(Cursor.NW_RESIZE);
					}
					case SOUTH -> {
						ygap.set((int) (tl.getY() - srcPt.getY()));
						canvas.setCursor(Cursor.N_RESIZE);
					}
					case SW -> {
						xgap.set((int) (srcPt.getX() - br.getX()));
						ygap.set((int) (tl.getY() - srcPt.getY()));
						canvas.setCursor(Cursor.NE_RESIZE);
					}
					case WEST -> {
						xgap.set((int) (srcPt.getX() - br.getX()));
						canvas.setCursor(Cursor.E_RESIZE);
					}
				}
			})
			.then((i, c) -> {
				final Point pt = ShapeFactory.INST.createPoint(i.getSrcObject()
					.map(n -> n.localToParent(i.getTgtLocalPoint())).orElse(null));
				final Position refPosition = c.getRefPosition();

				if(refPosition.isSouth()) {
					c.setNewY(grid.getTransformedPointToGrid(new Point3D(0, pt.getY() + ygap.get(), 0)).getY());
				}else {
					if(refPosition.isNorth()) {
						c.setNewY(grid.getTransformedPointToGrid(new Point3D(0, pt.getY() - ygap.get(), 0)).getY());
					}
				}

				if(refPosition.isWest()) {
					c.setNewX(grid.getTransformedPointToGrid(new Point3D(pt.getX() - xgap.get(), 0, 0)).getX());
				}else {
					if(refPosition.isEast()) {
						c.setNewX(grid.getTransformedPointToGrid(new Point3D(pt.getX() + xgap.get(), 0, 0)).getX());
					}
				}
				canvas.update();
			})
			.when(i -> i.getSrcObject().isPresent() && i.getSrcLocalPoint() != null && i.getTgtLocalPoint() != null)
			.endOrCancel(i -> canvas.setCursor(Cursor.DEFAULT))
			.end(() -> coordDimCustomiser.update())
			.bind();
	}
}