package leigh.mecha.nodegraph.javafx;

import javafx.geometry.Bounds;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseDragEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import leigh.mecha.nodegraph.NodeConnection;
import leigh.mecha.nodegraph.NodeObject;

/**
 * This class manages a JavaFX connection between two NodeGraph objects. The implementation draws {@link Line} between
 * a pair of provided {@link Circle} nodes representing the sockets on the edges of the node.
 *
 * @author Alex Leigh
 * @since mk14 mod19 (CRYSTAL)
 */
public class SocketFlow {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(SocketFlow.class);
    private Circle src;
    private Circle dst;
    private Line line;
    private final NodeGraphJFX nodegraph;
    private NodeObject obj;
    private NodeConnection objConnection;

    public SocketFlow(NodeGraphJFX nodegraph) {
        this.nodegraph = nodegraph;
    }

    /**
     * Create a new socket flow when the src and destination are already known. This is useful when loading a
     * previously saved state. Once created, drawConnection() should be called to actually draw the connection
     * between the nodes.
     */
    public SocketFlow(NodeGraphJFX nodegraph, Circle src, Circle dst) {
        this(nodegraph);
        this.src = src;
        this.dst = dst;
    }

    /**
     * Remove the socket flow from the given pane. This removes it from the UI and calls
     * <code>notifyUpdate()</code> on the {@link leigh.mecha.nodegraph.NodeGraph}.
     *
     * @param root
     */
    public void removeConnection(Pane root) {
        root.getChildren().remove(line);
        line = null;
        nodegraph.notifyUpdate();
    }

    public NodeObject getObj() {
        return obj;
    }

    public void setObj(NodeObject obj) {
        this.obj = obj;
    }

    public NodeConnection getObjConnection() {
        return objConnection;
    }

    public void setObjConnection(NodeConnection objConnection) {
        this.objConnection = objConnection;
    }

    /**
     * Either create or update a connection JavaFX line between the given socket circles in the given pane.
     * <p>
     * If the connection has already been drawn into a pane, calling this method with a different pane will still
     * update the old pane.
     */
    public void drawConnection(Pane root) {
        if (line == null) {
            line = new Line();
            NodeGraphJFX.setupLine(line);
            root.getChildren().add(line);
            line.toBack();

            ContextMenu contextMenu = new ContextMenu();
            MenuItem menuItem = new MenuItem("Delete");
            menuItem.setOnAction(event -> {
                root.getChildren().remove(line);
                line = null;
                nodegraph.removeConnection(this);
                event.consume();
            });
            contextMenu.getItems().add(menuItem);

            line.setOnContextMenuRequested(event -> {
                contextMenu.show(line, event.getScreenX(), event.getScreenY());
                event.consume();
            });
        }

        Bounds srcBounds = NodeGraphJFX.socketRootBounds(src);
        Bounds dstBounds = NodeGraphJFX.socketRootBounds(dst);

        // start line at center of node
        line.setStartX((srcBounds.getMinX() + srcBounds.getMaxX()) / 2);
        line.setStartY((srcBounds.getMinY() + srcBounds.getMaxY()) / 2);
        line.setEndX((dstBounds.getMinX() + dstBounds.getMaxX()) / 2);
        line.setEndY((dstBounds.getMinY() + dstBounds.getMaxY()) / 2);
    }

    /**
     * Determine if the mouse event for a drag operation results in a valid connection. Disqualifying conditions
     * include the src and destination socket being identical, attempting to connect same-cardinality sockets,
     * and having a missing src or destination.
     * <p>
     * Calling this method also sets up <code>getSrc()</code> and <code>getDst()</code> based on the cardinality
     * of the sockets. In other words, <code>getSrc()</code> will always return the output socket.
     */
    public boolean isValid(MouseDragEvent evt) {
        Circle a = (Circle) evt.getGestureSource();
        Circle b = (Circle) evt.getSource();

        if (a == null || b == null) return false;

        SocketUserData au = (SocketUserData) a.getUserData();
        SocketUserData bu = (SocketUserData) b.getUserData();

        if (au == null || bu == null) return false;
        if (au.isOutput() == bu.isOutput()) return false;

        // Determine cardinality here of the circles.
        if (!au.isOutput()) {
            src = a;
            dst = b;
        } else {
            src = b;
            dst = a;
        }

        return true;
    }

    public Circle getSrc() {
        return src;
    }

    public Circle getDst() {
        return dst;
    }

    @Override
    public String toString() {
        return "SocketFlow{" +
                "src=" + src +
                ", dst=" + dst +
                '}';
    }
}
