package nl.utwente.viskell.ui.components;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Point2D;
import javafx.scene.paint.Color;
import javafx.scene.shape.Shape;
import nl.utwente.viskell.haskell.expr.Expression;
import nl.utwente.viskell.haskell.expr.Hole;
import nl.utwente.viskell.haskell.expr.LetExpression;
import nl.utwente.viskell.haskell.type.Type;
import nl.utwente.viskell.haskell.type.TypeScope;
import nl.utwente.viskell.ui.BlockContainer;
import com.google.common.collect.ImmutableMap;
/**
* ConnectionAnchor that specifically functions as an input.
*/
public class InputAnchor extends ConnectionAnchor implements ConnectionAnchor.Target {
/** The visual representation of the InputAnchor. */
@FXML protected Shape visibleAnchor;
/** The invisible part of the InputAnchor (the touch zone). */
@FXML protected Shape invisibleAnchor;
/** The thing sticking out of an unconnected InputAnchor. */
@FXML protected Shape openWire;
/** The Optional connection this anchor has. */
private Optional<Connection> connection;
/** The local type of this anchor */
private Type type;
/** Property storing the error state. */
private BooleanProperty errorState;
/**
* @param block
* The Block this anchor is connected to.
*/
public InputAnchor(Block block) {
super(block);
this.loadFXML("InputAnchor");
this.connection = Optional.empty();
this.type = TypeScope.unique("???");
this.errorState = new SimpleBooleanProperty(false);
this.errorState.addListener(this::checkError);
}
/** For use in subclasses only. */
protected InputAnchor() {
super(null);
this.connection = Optional.empty();
this.errorState = new SimpleBooleanProperty(false);
this.errorState.addListener(this::checkError);
}
/**
* @param block The Block this anchor is connected to.
* @param type The type constraint for this anchor.
*/
public InputAnchor(Block block, Type type) {
this(block);
this.type = type;
}
@Override
public ConnectionAnchor getAssociatedAnchor() {
return this;
}
/**
* @param state The new error state for this ConnectionAnchor.
*/
protected void setErrorState(boolean state) {
this.errorState.set(state);
}
/** @return The Optional connection this anchor has. */
public Optional<Connection> getConnection() {
return this.connection;
}
/**
* Sets the connection of this anchor.
* @param connection The connection to set.
*/
protected void setConnection(Connection connection) {
this.connection = Optional.of(connection);
this.openWire.setVisible(false);
}
@Override
public void removeConnections() {
if (this.connection.isPresent()) {
Connection conn = this.connection.get();
this.connection = Optional.empty();
conn.remove();
}
this.setErrorState(false);
this.openWire.setVisible(true);
if (this.getWireInProgress() != null) {
this.getWireInProgress().remove();
}
}
@Override
public boolean hasConnection() {
return this.connection.isPresent();
}
/** @return True if this anchor has an error free connection */
public boolean hasValidConnection() {
return this.connection.isPresent() && ! (this.errorState.get() || this.connection.get().hasScopeError());
}
@Override
public Point2D getAttachmentPoint() {
return this.getPane().sceneToLocal(this.localToScene(new Point2D(0, -7)));
}
/**
* @return the local type of this anchor
*/
public Type getType() {
return this.type;
}
@Override
public Type getFreshType() {
return this.type.getFresh();
}
/**
* @return the string representation of the in- or output type.
*/
public final String getStringType() {
return this.getType().prettyPrint();
}
/**
* Sets the type constraint of this input anchor to a fresh type.
* @param type constraint which this input anchor will require.
* @param scope wherein the fresh type is constructed.
*/
public void setFreshRequiredType(Type type, TypeScope scope) {
this.type = type.getFresh(scope);
}
public void setExactRequiredType(Type type) {
this.type = type;
}
/**
* @return Optional of the connection's opposite output anchor.
*/
public Optional<OutputAnchor> getOppositeAnchor() {
return this.connection.map(c -> c.getStartAnchor());
}
/**
* @return The local expression carried by the connection connected to this anchor.
*/
public Expression getLocalExpr(Set<OutputAnchor> outsideAnchors) {
return this.connection.map(c -> c.getStartAnchor().getVariable()).orElse(new Hole());
}
/**
* This function assumes that the function is in the top level container.
* @return The full expression carried by the connection connected to this anchor.
*/
public Expression getFullExpr() {
Set<OutputAnchor> outsideAnchors = new HashSet<>();
LetExpression fullExpr = new LetExpression(this.getLocalExpr(outsideAnchors), false);
/**
* Iterate over the container until it doesn't find new nodes.
*/
boolean cont = true;
for (int numAnchors = -1; numAnchors != outsideAnchors.size() || cont; numAnchors = outsideAnchors.size()) {
cont = (numAnchors != outsideAnchors.size());
extendExprGraph(fullExpr, block.container, outsideAnchors);
outsideAnchors.forEach(connection -> connection.extendExprGraph(fullExpr, block.container, outsideAnchors));
}
return fullExpr;
}
/**
* Extends the expression graph to include all subexpression required
* @param exprGraph the let expression representing the current expression graph
* @param container the container to which this expression graph is constrained
* @param outsideAnchors a mutable set of required OutputAnchors from a surrounding container
*/
protected void extendExprGraph(LetExpression exprGraph, BlockContainer container, Set<OutputAnchor> outsideAnchors) {
connection.ifPresent(connection -> connection.extendExprGraph(exprGraph, container, outsideAnchors));
}
/**
* ChangeListener that will set the error state if isConnected().
*/
public void checkError(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
ObservableList<String> style = this.visibleAnchor.getStyleClass();
style.removeAll("error");
if (newValue) {
style.add("error");
}
}
@Override
protected void setNearbyWireReaction(int goodness) {
if (goodness > 2) {
this.openWire.setStroke(Color.DODGERBLUE);
this.openWire.setStrokeWidth(5);
this.visibleAnchor.setFill(Color.DODGERBLUE);
} else if (goodness > 0) {
this.openWire.setStroke(Color.DARKGREEN);
this.openWire.setStrokeWidth(5);
this.visibleAnchor.setFill(Color.DARKGREEN);
} else if (goodness < 0) {
this.openWire.setStroke(Color.RED);
this.openWire.setStrokeWidth(3);
this.visibleAnchor.setFill(Color.RED);
} else {
this.openWire.setStroke(Color.BLACK);
this.openWire.setStrokeWidth(3);
this.visibleAnchor.setFill(Color.BLACK);
}
}
@Override
public void setWireInProgress(DrawWire wire) {
super.setWireInProgress(wire);
if (wire == null) {
this.openWire.setVisible(!this.hasConnection());
this.invisibleAnchor.setMouseTransparent(false);
} else {
this.openWire.setVisible(false);
this.invisibleAnchor.setMouseTransparent(true);
}
}
/** Called when the VisualState changed. */
public void invalidateVisualState() {
this.connection.ifPresent(c -> c.invalidateVisualState());
}
@Override
public BlockContainer getContainer() {
return this.block.getContainer();
}
@Override
public String toString() {
return "InputAnchor for " + this.block;
}
@Override
public Map<String, Object> toBundle() {
ImmutableMap.Builder<String, Object> bundle = ImmutableMap.builder();
bundle.put(BLOCK_LABEL, this.block.hashCode());
bundle.put(ANCHOR_LABEL, block.getAllInputs().indexOf(this));
return bundle.build();
}
}