package nl.utwente.group10.ui;

import java.util.ArrayList;
import java.util.Optional;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;

import nl.utwente.ewi.caes.tactilefx.control.TactilePane;
import nl.utwente.group10.ghcj.GhciException;
import nl.utwente.group10.ghcj.GhciSession;
import nl.utwente.group10.haskell.catalog.HaskellCatalog;
import nl.utwente.group10.haskell.env.Env;
import nl.utwente.group10.ui.components.blocks.Block;
import nl.utwente.group10.ui.components.blocks.FunctionBlock;
import nl.utwente.group10.ui.components.lines.Connection;
import nl.utwente.group10.ui.handlers.ConnectionCreationManager;

/**
 * Extension of TactilePane that keeps state for the user interface.
 */
public class CustomUIPane extends TactilePane {
    private ObjectProperty<Optional<Block>> selectedBlock;
    private ConnectionCreationManager connectionCreationManager;
    private Optional<GhciSession> ghci;

    private Point2D dragStart;
    private Point2D offset;

    private HaskellCatalog catalog;
    private Env envInstance;

    /**
     * Constructs a new instance.
     */
    public CustomUIPane(HaskellCatalog catalog) {
        this.connectionCreationManager = new ConnectionCreationManager(this);
        this.selectedBlock = new SimpleObjectProperty<>(Optional.empty());
        this.dragStart = Point2D.ZERO;
        this.offset = Point2D.ZERO;
        this.catalog = catalog;
        this.envInstance = catalog.asEnvironment();

        try {
            this.ghci = Optional.of(new GhciSession());
        } catch (GhciException e) {
            this.ghci = Optional.empty();
        }

        this.addEventHandler(MouseEvent.MOUSE_PRESSED, this::handlePress);
        this.addEventHandler(MouseEvent.MOUSE_DRAGGED, this::handleDrag);
        this.addEventHandler(ScrollEvent.SCROLL, this::handleScroll);

        this.addEventHandler(KeyEvent.KEY_PRESSED, this::handleKey);
    }

    private void handleKey(KeyEvent keyEvent) {
        int dist = 100;

        switch (keyEvent.getCode()) {
            case W: this.setTranslateY(this.getTranslateY() + dist); break;
            case S: this.setTranslateY(this.getTranslateY() - dist); break;
            case A: this.setTranslateX(this.getTranslateX() + dist); break;
            case D: this.setTranslateX(this.getTranslateX() - dist); break;

            case H: // C&C-style
            case BACK_SPACE: // SC-style
                this.setTranslateX(0);
                this.setTranslateY(0);
                break;

            case EQUALS: this.setScale(this.getScaleX() * 1.25); break;
            case MINUS:  this.setScale(this.getScaleX() * 0.8); break;
            case DIGIT1: this.setScale(1); break;

            case LEFT: bowtieSelected(-1); break;
            case RIGHT: bowtieSelected(+1); break;

            case DELETE:
                removeSelected();
                break;
        }
    }

    private void handlePress(MouseEvent e) {
        offset = new Point2D(this.getTranslateX(), this.getTranslateY());
        dragStart = new Point2D(e.getScreenX(), e.getScreenY());
    }

    private void handleDrag(MouseEvent e) {
        Point2D dragCurrent = new Point2D(e.getScreenX(), e.getScreenY());
        Point2D delta = dragStart.subtract(dragCurrent);

        this.setTranslateX(offset.getX() - delta.getX());
        this.setTranslateY(offset.getY() - delta.getY());
    }

    private void setScale(double scale) {
        this.setScaleX(scale);
        this.setScaleY(scale);
    }

    private void handleScroll(ScrollEvent scrollEvent) {
        double scale = this.getScaleX();
        double ratio = 1.0;

        if (scrollEvent.getDeltaY() > 0 && scale < 8) {
            ratio = 1.25;
        } else if (scrollEvent.getDeltaY() < 0 && scale > 0.5) {
            ratio = 0.8;
        }

        this.setScale(scale * ratio);
        this.setTranslateX(this.getTranslateX() * ratio);
        this.setTranslateY(this.getTranslateY() * ratio);

    }

    /**
     * @return The Env instance to be used within this CustomUIPane.
     */
    public Env getEnvInstance() {
        return envInstance;
    }

    /**
     * Re-evaluate all display blocks.
     * This is inefficient.
     */
    public final void invalidateAll() {
        for (Node node : getChildren()) {
            if (node instanceof Block) {
                ((Block) node).invalidateConnectionState();
            }
        }
    }

    public final void errorAll() {
        for (Node node : getChildren()) {
            if (node instanceof Block) {
                ((Block) node).setError(true);
            } else if (node instanceof Connection) {
                ((Connection) node).setError(true);
            }
        }
    }

    public Optional<Block> getSelectedBlock() {
        return selectedBlock.get();
    }

    public void setSelectedBlock(Block selectedBlock) {
        this.selectedBlock.set(Optional.ofNullable(selectedBlock));
    }

    public ObjectProperty<Optional<Block>> selectedBlockProperty() {
        return selectedBlock;
    }

    /** Remove the given block from this UI pane, including its connections. */
    public void removeBlock(Block block) {
        Optional<Block> target = Optional.of(block);
        ArrayList<Node> toRemove = new ArrayList<>();

        for (Node node : getChildren()) {
            if (node instanceof Connection) {
                Optional<Block> in = ((Connection) node).getInputBlock();
                Optional<Block> out = ((Connection) node).getOutputBlock();

                if (in.equals(target) || out.equals(target)) {
                    toRemove.add(node);
                }
            } else if (node.equals(block)) {
                toRemove.add(node);
            }
        }

        this.getChildren().removeAll(toRemove);
    }

    /** Remove the selected block, if any. */
    private void removeSelected() {
        this.getSelectedBlock().ifPresent(this::removeBlock);
    }

    private void bowtieSelected(int delta) {
        this.getSelectedBlock().ifPresent(block -> {
            if (block instanceof FunctionBlock) {
                FunctionBlock fun = (FunctionBlock) block;

                try {
                    fun.setBowtieIndex(fun.getBowtieIndex() + delta);
                } catch (IndexOutOfBoundsException ignored) {
                }
            }
        });
    }

    public ConnectionCreationManager getConnectionCreationManager() {
        return connectionCreationManager;
    }

    public Optional<GhciSession> getGhciSession() {
        return ghci;
    }
}

@@ -656,70 +656,8 @@
 ck;%0A
-import nl.utwente.group10.ui.components.blocks.FunctionBlock;%0A
 impo
@@ -2192,10 +2192,15 @@
 ase 
-W:
+UP:    
  thi
@@ -2269,18 +2269,23 @@
    case 
-S:
+DOWN:  
  this.se
@@ -2354,10 +2354,15 @@
 ase 
-A:
+LEFT:  
  thi
@@ -2435,10 +2435,15 @@
 ase 
-D:
+RIGHT: 
  thi
@@ -2870,110 +2870,8 @@
 k;%0A%0A
-            case LEFT: bowtieSelected(-1); break;%0A            case RIGHT: bowtieSelected(+1); break;%0A%0A
     
@@ -6026,421 +6026,8 @@
  %7D%0A%0A
-    private void bowtieSelected(int delta) %7B%0A        this.getSelectedBlock().ifPresent(block -%3E %7B%0A            if (block instanceof FunctionBlock) %7B%0A                FunctionBlock fun = (FunctionBlock) block;%0A%0A                try %7B%0A                    fun.setBowtieIndex(fun.getBowtieIndex() + delta);%0A                %7D catch (IndexOutOfBoundsException ignored) %7B%0A                %7D%0A            %7D%0A        %7D);%0A    %7D%0A%0A
     

Revert "Add bow-tying keybindings"