package be.iminds.ilabt.jfed.experimenter_gui.canvas;

import be.iminds.ilabt.jfed.experimenter_gui.editor.ToolboxItem;
import be.iminds.ilabt.jfed.experimenter_gui.editor.views.DraggedItemProvider;
import be.iminds.ilabt.jfed.ui.javafx.dialogs.JFDialogs;
import be.iminds.ilabt.jfed.util.ExecuteOnNotNull;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.MouseDragEvent;
import javafx.scene.input.MouseEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

/**
 * User: twalcari
 * Date: 5/21/2015
 * Time: 10:27
 */
public abstract class AbstractEditableCanvas extends ExperimentCanvas {
    private static final Logger LOG = LoggerFactory.getLogger(AbstractEditableCanvas.class);

    protected static final double EDITOR_LOCATION_SHIFT = 15;

    protected final ObservableList<CanvasNode> clipboard = FXCollections.observableArrayList();
    private final StringProperty status = new SimpleStringProperty();

    private final NewCanvasLink newCanvasLink;
    private final DraggedItemProvider draggedItemProvider;

    public AbstractEditableCanvas(DraggedItemProvider draggedItemProvider) {
        this.draggedItemProvider = draggedItemProvider;

        this.newCanvasLink = new NewCanvasLink(this);

        this.setOnMouseDragReleased(this::onMouseDragReleased);

        //make sure that the initialize-function is called when the scene-property is set
        new ExecuteOnNotNull<Scene>(sceneProperty()) {
            @Override
            public void run() {
                initialize();
            }
        };
    }

    private void initialize() {
        //add mnemorics
        getScene().getAccelerators().put(new KeyCodeCombination(KeyCode.DELETE), this::deleteSelectedItem);
    }

    protected abstract void deleteSelectedItem();


    @Override
    protected void canvasNodeOnDragDetected(MouseEvent mouseEvent) {
        super.canvasNodeOnDragDetected(mouseEvent);

        //make canvas link invisible when starting to drag
        newCanvasLink.setVisible(false);
    }

    @Override
    protected void canvasNodeOnMouseDragged(MouseEvent mouseEvent) {
        CanvasNode node = (CanvasNode) mouseEvent.getSource();
        Bounds canvasOrigin = getCanvas().localToScene(getCanvas().getBoundsInLocal());
        double newX = mouseEvent.getSceneX()
                - canvasOrigin.getMinX()
                - node.getBoundsInParent().getWidth() / 2;
        double newY = mouseEvent.getSceneY()
                - canvasOrigin.getMinY()
                - node.getBoundsInParent().getHeight() / 2;

        //setSecondaryStatus("Moving to " + newX + ", " + newY);

        node.relocate(newX, newY);

        mouseEvent.consume();
    }

    /**
     * Performs panning of the canvas when the newCanvasLink is not visible, or when the shift-button is down.
     * Otherwise, it tries to connect two nodes
     */
    protected void onCanvasMouseDragged(MouseEvent mouseEvent) {
        if (mouseEvent.isShiftDown() || !newCanvasLink.isVisible()) {
            super.onCanvasMouseDragged(mouseEvent);
        } else {

            //try to connect this node if canvas isn't fixed
            NewLinkTarget newLinkTarget = findNearbyTarget(mouseEvent.getX(), mouseEvent.getY());

            if (newLinkTarget != null && newLinkTarget != newCanvasLink.getOrigin() /* self links are never allowed*/) {
                newCanvasLink.setTarget(newLinkTarget);
                newCanvasLink.setValid(isLegalLink(newCanvasLink));
            } else {
                newCanvasLink.setNewEnd(mouseEvent.getX(), mouseEvent.getY());
                newCanvasLink.setValid(true);
            }
        }
    }

    @Override
    protected void onCanvasMouseReleased(MouseEvent mouseEvent) {
        super.onCanvasMouseReleased(mouseEvent);

        //have we made a new connection?
        if (newCanvasLink.isLinkComplete()) {
            //check if new link may be created

            if (!isLegalLink(newCanvasLink)) {
                setStatus("The requested link is illegal and was ignored.");
                LOG.trace("Link is not legal, and is thus rejected.");
            } else {
                processNewLinkRequest(newCanvasLink);
            }
        }
        newCanvasLink.clear();
        newCanvasLink.setVisible(false);
    }

    protected abstract boolean isLegalLink(NewCanvasLink newCanvasLink);

    @Override
    protected void onCanvasMouseMoved(MouseEvent mouseEvent) {
        if (!mouseEvent.isPrimaryButtonDown()) {

            //if the mouse is close nearby a node, show a "newCanvasLink"
            NewLinkTarget newLinkTarget = findNearbyTarget(mouseEvent.getX(), mouseEvent.getY());

            if (newLinkTarget != null) {
                newCanvasLink.setOrigin(newLinkTarget);

                newCanvasLink.setNewEnd(mouseEvent.getX(), mouseEvent.getY());
                newCanvasLink.setVisible(true);

                setStatus("Click and drag the circle to another item on the canvas to create a new connection");
            } else {
                setStatus("");
                newCanvasLink.setVisible(false);
            }
        }
    }

    /**
     * returns a node if there is one within range.
     * Returns the closest if multiple, returns NULL if no node close enough.
     */
    public NewLinkTarget findNearbyTarget(double x, double y) {
        return getCanvas().getChildren().stream()
                .filter(child -> child instanceof NewLinkTarget)
                .map(NewLinkTarget.class::cast)
                .min(Comparator.comparingDouble(o -> getMinimumSquareDistanceToXY(o, x, y)))
                .filter(newLinkTarget -> getMinimumSquareDistanceToXY(newLinkTarget, x, y) < ExperimentCanvas.SQUARED_MAX_CURSOR_TARGET_DISTANCE)
                .orElse(null);

    }

    private static double getMinimumSquareDistanceToXY(NewLinkTarget nc, double x, double y) {
        double nx = nc.getCanvasBounds().getMinX();
        if (x >= nc.getCanvasBounds().getMinX() && x <= nc.getCanvasBounds().getMaxX()) nx = x;
        else if (x > nc.getCanvasBounds().getMaxX()) nx = nc.getCanvasBounds().getMaxX();

        double ny = nc.getCanvasBounds().getMinY();
        if (y >= nc.getCanvasBounds().getMinY() && y <= nc.getCanvasBounds().getMaxY()) ny = y;
        else if (y > nc.getCanvasBounds().getMaxY()) ny = nc.getCanvasBounds().getMaxY();

        return squareDistance(x, y, nx, ny);
    }


    /**
     * Helper method that computes the distance between two points
     */
    private static double squareDistance(double xa, double ya, double xb, double yb) {
        return ((xa - xb) * (xa - xb)) + ((ya - yb) * (ya - yb));
    }


    protected abstract boolean processNewLinkRequest(NewCanvasLink newCanvasLink);


    public void onMouseDragReleased(MouseDragEvent mouseDragEvent) {
        //we are adding a new element from the toolbox
        assert (draggedItemProvider.getDraggedItem() != null);

        Point2D location = new Point2D(
                mouseDragEvent.getSceneX()
                        - getCanvas().getLocalToSceneTransform().getTx()
                        - draggedItemProvider.getDraggedItem().getPrefWidth() / 2,
                mouseDragEvent.getSceneY()
                        - getCanvas().getLocalToSceneTransform().getTy()
                        - draggedItemProvider.getDraggedItem().getPrefHeight() / 2
        );

        addCanvasNodeFromToolboxItem(draggedItemProvider.getDraggedItem(), location);

        draggedItemProvider.clearDraggedItem();
        setCursor(null);
        mouseDragEvent.consume();
    }

    protected abstract void addCanvasNodeFromToolboxItem(ToolboxItem item, Point2D location);


    public void copySelected() {
        clipboard.clear();

        if (!(getSelectionProvider().getSelectedObject() instanceof CanvasNode)) {
            JFDialogs.create()
                    .owner(this.getScene().getWindow())
                    .message("Nothing to copy!\r\nPlease select an item first.")
                    .masthead("Copy action failed")
                    .showWarning();
            return;
        }

        clipboard.add((CanvasNode) getSelectionProvider().getSelectedObject());
    }

    public final void paste() {
        if (clipboard.isEmpty()) {
            JFDialogs.create()
                    .owner(this.getScene().getWindow())
                    .message("Nothing to paste!\r\nPlease copy an item first.")
                    .masthead("Paste action failed")
                    .showWarning();
        }

        /* If the user pastes the same node two times, we want it to appear on two different places.
           Therefore we modify the clipboard to the newly created items    */
        List<CanvasNode> newClipboard = new ArrayList<>();

        //we now know that a RSpecCanvasNode has been created, and we can thus safely create the interfaces
        clipboard
                .forEach(canvasNode -> {
                    CanvasNode newCanvasNode = addCopyOfNodeToCanvas(canvasNode);
                    if (newCanvasNode != null)
                        newClipboard.add(newCanvasNode);
                });

        clipboard.clear();
        clipboard.addAll(newClipboard);


    }

    public void duplicateSelected() {
        if (getSelectionProvider().getSelectedObject() instanceof CanvasNode) {
            CanvasNode canvasNode = (CanvasNode) getSelectionProvider().getSelectedObject();

            CanvasNode newRspecCanvasNode = addCopyOfNodeToCanvas(canvasNode);
            if (newRspecCanvasNode != null)
                getSelectionProvider().setSelectedObject(newRspecCanvasNode);
        } else {
            JFDialogs.create()
                    .owner(this.getScene().getWindow())
                    .message("Nothing to duplicate!\r\nPlease select an item first.")
                    .masthead("Duplicate action failed")
                    .showWarning();

        }
    }

    protected abstract CanvasNode addCopyOfNodeToCanvas(CanvasNode node);

    public ReadOnlyListProperty<CanvasNode> getClipboard() {
        return new ReadOnlyListWrapper<>(clipboard).getReadOnlyProperty();
    }

    public String getStatus() {
        return status.get();
    }

    public StringProperty statusProperty() {
        return status;
    }

    protected void setStatus(String status) {
        if (Objects.equals(this.status.get(), status))
            return; //ignore an unchanged status message

        LOG.trace("New Status: {}", status);
        this.status.set(status);
    }
}
