package be.iminds.ilabt.jfed.experimenter_gui.gts_editor.views;

import be.iminds.ilabt.jfed.experimenter_gui.config.JFedGuiConfig;
import be.iminds.ilabt.jfed.experimenter_gui.canvas.AbstractEditableCanvas;
import be.iminds.ilabt.jfed.experimenter_gui.canvas.CanvasNode;
import be.iminds.ilabt.jfed.experimenter_gui.canvas.NewCanvasLink;
import be.iminds.ilabt.jfed.experimenter_gui.canvas.NewLinkTarget;
import be.iminds.ilabt.jfed.experimenter_gui.canvas.gts.GtsCanvasLink;
import be.iminds.ilabt.jfed.experimenter_gui.canvas.gts.GtsCanvasNode;
import be.iminds.ilabt.jfed.experimenter_gui.canvas.gts.GtsCanvasPort;
import be.iminds.ilabt.jfed.experimenter_gui.canvas.gts.GtsTypeToCanvasBinding;
import be.iminds.ilabt.jfed.experimenter_gui.canvas.rspec.CanvasNodeFactory;
import be.iminds.ilabt.jfed.experimenter_gui.editor.ModelRspecEditor;
import be.iminds.ilabt.jfed.experimenter_gui.editor.ToolboxItem;
import be.iminds.ilabt.jfed.experimenter_gui.editor.views.DraggedItemProvider;
import be.iminds.ilabt.jfed.experimenter_gui.gts_editor.GTSModelEditor;
import be.iminds.ilabt.jfed.experimenter_gui.gts_editor.toolbox.GTSExternalNetworkToolboxItem;
import be.iminds.ilabt.jfed.experimenter_gui.gts_editor.toolbox.GTSHostToolboxItem;
import be.iminds.ilabt.jfed.experimenter_gui.gts_editor.toolbox.GTSOFXToolboxItem;
import be.iminds.ilabt.jfed.experimenter_gui.gts_editor.toolbox.GTSPortToolboxItem;
import be.iminds.ilabt.jfed.gts.GtsDslConstants;
import be.iminds.ilabt.jfed.gts.model.GtsType;
import be.iminds.ilabt.jfed.gts.model.impl.*;

import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.ui.javafx.GlyphUtils;
import javafx.geometry.Point2D;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import org.controlsfx.glyphfont.FontAwesome;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;

/**
 * User: twalcari
 * Date: 5/21/2015
 * Time: 10:28
 */
public class EditableGTSCanvas extends AbstractEditableCanvas {
    private static final Logger LOG = LoggerFactory.getLogger(EditableGTSCanvas.class);
    private final GTSModelEditor modelEditor;
    private final FXGtsType model;
    private final FXGtsFactory gtsFactory = new FXGtsFactory();
    private final GtsTypeToCanvasBinding binding;

    private final ContextMenu nodeContextMenu;
    private final ContextMenu linkContextMenu;
    private final ContextMenu externalDomainContextMenu;

    public EditableGTSCanvas(GTSModelEditor modelEditor, JFedGuiConfig config, CanvasNodeFactory canvasNodeFactory,
                             DraggedItemProvider draggedItemProvider, TestbedInfoSource testbedInfoSource) {
        super(draggedItemProvider);
        this.modelEditor = modelEditor;
        this.model = modelEditor.getModel();

        this.binding = new GtsTypeToCanvasBinding(model, this);

        // create context menus
        this.nodeContextMenu = new ContextMenu();

        MenuItem configureNameItem =
                new MenuItem("Change node name", GlyphUtils.createSmallGlyph(FontAwesome.Glyph.QUOTE_LEFT));
        configureNameItem.setOnAction(actionEvent -> {
            assert (getSelectionProvider().getSelectedObject() instanceof GtsCanvasNode);
            GtsCanvasNode gtsCanvasNode = (GtsCanvasNode) getSelectionProvider().getSelectedObject();

            onConfigureNodeNamePressed(gtsCanvasNode.getNode());
        });
        nodeContextMenu.getItems().add(configureNameItem);


        MenuItem configureItem = new MenuItem("Configure node", GlyphUtils.createSmallGlyph(FontAwesome.Glyph.PENCIL));
        configureItem.setOnAction(actionEvent -> {
            assert (getSelectionProvider().getSelectedObject() instanceof GtsCanvasNode);
            GtsCanvasNode canvasNode = (GtsCanvasNode) getSelectionProvider().getSelectedObject();

            onConfigureNodePressed(canvasNode);
        });
        nodeContextMenu.getItems().add(configureItem);

        MenuItem deleteItem = new MenuItem("Delete", GlyphUtils.createSmallGlyph(FontAwesome.Glyph.TIMES));
        deleteItem.setOnAction(actionEvent -> {
            assert (getSelectionProvider().getSelectedObject() instanceof GtsCanvasNode);
            GtsCanvasNode rspecCanvasNode = (GtsCanvasNode) getSelectionProvider().getSelectedObject();

            onDeleteNodePressed(rspecCanvasNode);
        });
        nodeContextMenu.getItems().add(deleteItem);

        //contextMenu for links
        this.linkContextMenu = new ContextMenu();

        // was disabled as FXRspecLink.LinkSetting is currently not supported in practice, but it seems to work for VM's now
        MenuItem configureLinkItem =
                new MenuItem("Configure link", GlyphUtils.createSmallGlyph(FontAwesome.Glyph.PENCIL));
        configureLinkItem.setOnAction(actionEvent -> {
            GtsCanvasLink gtsCanvasLink = (GtsCanvasLink) getSelectionProvider().getSelectedObject();

            onConfigureLinkPressed(gtsCanvasLink.getGtsLink());
        });
        linkContextMenu.getItems().add(configureLinkItem);

        MenuItem linkDeleteItem = new MenuItem("Delete", GlyphUtils.createSmallGlyph(FontAwesome.Glyph.TIMES));
        linkDeleteItem.setOnAction(actionEvent -> {
            GtsCanvasLink gtsCanvasLink = (GtsCanvasLink) getSelectionProvider().getSelectedObject();

            onDeleteLinkPressed(gtsCanvasLink);
        });
        linkContextMenu.getItems().add(linkDeleteItem);


        this.externalDomainContextMenu = new ContextMenu();

        MenuItem configureDomainItem =
                new MenuItem("Configure External Domain", GlyphUtils.createSmallGlyph(FontAwesome.Glyph.PENCIL));
        configureDomainItem.setOnAction(actionEvent -> {
            assert (getSelectionProvider().getSelectedObject() instanceof GtsCanvasNode);
            GtsCanvasNode canvasNode =
                    (GtsCanvasNode) getSelectionProvider().getSelectedObject();

            assert canvasNode.getNode() instanceof FXGtsExternalNetwork;

            onConfigureExternalDomainPressed((FXGtsExternalNetwork) canvasNode.getNode());
        });
        externalDomainContextMenu.getItems().add(configureDomainItem);

        MenuItem deleteDomainItem = new MenuItem("Delete", GlyphUtils.createSmallGlyph(FontAwesome.Glyph.TIMES));
        deleteDomainItem.setOnAction(actionEvent -> {
            assert (getSelectionProvider().getSelectedObject() instanceof GtsCanvasNode);
            GtsCanvasNode rspecCanvasNode = (GtsCanvasNode) getSelectionProvider().getSelectedObject();

            onDeleteNodePressed(rspecCanvasNode);
        });
        externalDomainContextMenu.getItems().add(deleteDomainItem);

    }

    private void onConfigureNodeNamePressed(FXGtsNode gtsNode) {

        TextInputDialog dialog = new TextInputDialog(gtsNode.getId());
        dialog.setHeaderText("Edit node name");
        dialog.setContentText("New node name:");

        dialog.showAndWait().ifPresent(newName -> {
            if (ModelRspecEditor.isValidNodeName(newName)) {
                gtsNode.setId(newName);
            } else {
                new Alert(Alert.AlertType.ERROR, "The given name is invalid.").showAndWait();
            }
        });
    }

    private boolean onConfigureExternalDomainPressed(FXGtsExternalNetwork externalNetwork) {

        String portId = !externalNetwork.getPorts().isEmpty() ?
                externalNetwork.getPorts().values().iterator().next().getId() : null;

        ExternalDomain currentDomain =
                Arrays.stream(domains).filter(domain -> Objects.equals(domain.getLocation(), externalNetwork.getLocation())
                        && Objects.equals(domain.getPortId(), portId)).findFirst()
                        .orElse(null);


        ChoiceDialog<ExternalDomain> dialog = new ChoiceDialog<>(currentDomain, domains);

        dialog.setHeaderText("Select an external domain");
        dialog.setTitle("Select an external domain");

        Optional<ExternalDomain> result = dialog.showAndWait();
        if (result.isPresent()) {
            ExternalDomain externalDomain = result.get();
            externalNetwork.setLocation(externalDomain.getLocation());
            externalNetwork.setRequiredPortName(externalDomain.getPortId());

            if (!externalNetwork.getPorts().isEmpty()) {
                //change the port name

                FXGtsPort existingPort = externalNetwork.getPorts().values().iterator().next();

                externalNetwork.getPorts().remove(existingPort.getId());
                existingPort.setId(externalDomain.getPortId());
                externalNetwork.getPorts().put(existingPort.getId(), existingPort);
            }


            externalNetwork.getPorts().clear();
            FXGtsPort port = gtsFactory.createGtsPort();
            port.setId(externalDomain.getPortId());
            externalNetwork.addPort(port);
            return true;
        } else
            return false;
    }


    private static final ExternalDomain[] domains = new ExternalDomain[]{
            new ExternalDomain(GtsDslConstants.GTS_LOCATION_AMS, "ep1", "iMinds #1", "from AMS via GN/BRU, BELnet", "Fed4FIRE\n"),
            new ExternalDomain(GtsDslConstants.GTS_LOCATION_AMS, "ep2", "ONOS-US", "via GN/AMS, MANLAN, AL2S", "ONOS/DREAMER\n"),
            //    new ExternalDomain( "BRA","ep1","unassigned\n"+                                  ),
            new ExternalDomain(GtsDslConstants.GTS_LOCATION_BRA, "ep2", "ExoGENI ", "via GN/AMS, MANLAN, AL2S", "GTS demo SC14\n"),
            new ExternalDomain(GtsDslConstants.GTS_LOCATION_LJU, "ep1", "Create-net circuit #2 ", "via GN/MIL, GARR", "ONOS/DREAMER"),
            new ExternalDomain(GtsDslConstants.GTS_LOCATION_LJU, "ep2", "iMinds #2", "from LJU via GN/BRU, BELnet", "Fed4FIRE"),
            //          new ExternalDomain("PRG","ep1","unassigned\n"),
            new ExternalDomain(GtsDslConstants.GTS_LOCATION_PRG, "ep2", "Create-net circuit #1 ", "via GN/MIL, GARR", "ONOS/DREAMER")};

    private static class ExternalDomain {
        private final String location;
        private final String portId;
        private final String friendlyName;
        private final String info1;
        private final String info2;

        public ExternalDomain(String location, String portId, String friendlyName, String info1, String info2) {
            this.friendlyName = friendlyName;
            this.location = location;
            this.portId = portId;
            this.info1 = info1;
            this.info2 = info2;
        }

        public String getFriendlyName() {
            return friendlyName;
        }

        public String getInfo1() {
            return info1;
        }

        public String getInfo2() {
            return info2;
        }

        public String getLocation() {
            return location;
        }

        public String getPortId() {
            return portId;
        }

        public String toString() {
            return friendlyName + " (" + info1 + ")";
        }
    }

    private void onConfigureLinkPressed(FXGtsLink gtsLink) {
        TextInputDialog dialog = new TextInputDialog("10");
        dialog.setTitle("Link settings");
        dialog.setHeaderText("Link Settings");
        dialog.setContentText("Capacity [in Mbps]:");

        dialog.showAndWait().ifPresent(answer -> {
            boolean success = true;

            try {
                int value = Integer.parseInt(answer);
                if (value >= 1 && value <= 10_000) {
                    gtsLink.setCapacity(value);
                } else {
                    success = false;
                }
            } catch (NumberFormatException ex) {
                success = false;
            }

            if (!success) {
                new Alert(Alert.AlertType.ERROR, "Invalid capacity was entered! Only integer values between 1 and 10000 are allowed!").showAndWait();
            }
        });

    }

    private void onConfigureNodePressed(GtsCanvasNode gtsCanvasNode) {
        String[] locations = new String[]{GtsDslConstants.GTS_LOCATION_BRA,
                                          GtsDslConstants.GTS_LOCATION_AMS,
                                          GtsDslConstants.GTS_LOCATION_LJU,
                                          GtsDslConstants.GTS_LOCATION_PRG,
                                          GtsDslConstants.GTS_LOCATION_HAM,
                                          GtsDslConstants.GTS_LOCATION_MIL,
                                          GtsDslConstants.GTS_LOCATION_LON};
        ChoiceDialog<String> dialog = new ChoiceDialog<>(gtsCanvasNode.getNode().getLocation(), locations);

        dialog.setContentText("Select the location for this host:");
        dialog.showAndWait().ifPresent(location -> gtsCanvasNode.getNode().setLocation(location));

    }

    @Override
    protected void deleteSelectedItem() {
        if (getSelectionProvider().getSelectedObject() instanceof GtsCanvasNode) {
            onDeleteNodePressed((GtsCanvasNode) getSelectionProvider().getSelectedObject());

        } else if (getSelectionProvider().getSelectedObject() instanceof GtsCanvasLink) {
            onDeleteLinkPressed((GtsCanvasLink) getSelectionProvider().getSelectedObject());
        }
    }

    @Override
    public boolean isLegalLink(NewCanvasLink newCanvasLink) {
        if (!newCanvasLink.isLinkComplete()) {
            LOG.trace("Requested link is not complete");
            return false;
        }

        if (!isValidForLink(newCanvasLink.getOrigin()) || !(isValidForLink(newCanvasLink.getTarget())))
            return false;

        if (newCanvasLink.getOrigin() instanceof GtsCanvasPort && newCanvasLink.getTarget() instanceof GtsCanvasPort) {
            LOG.info("Cannot link two ports together!");
            return false;
        }

        return true;
    }

    private boolean isValidForLink(NewLinkTarget newLinkTarget) {
        if (newLinkTarget instanceof GtsCanvasNode) {
            return isValidNodeForNewLink(((GtsCanvasNode) newLinkTarget).getNode());
        } else if (newLinkTarget instanceof GtsCanvasPort) {
            return isValidPortForNewLink(((GtsCanvasPort) newLinkTarget).getPort());
        } else {
            LOG.warn("Unknown object is checked for validity of link: {}", newLinkTarget);
            return true;
        }
    }

    private boolean isValidNodeForNewLink(FXGtsNode node) {
        if (node instanceof FXGtsExternalNetwork) {
            if (!node.getPorts().isEmpty())
                return model.getAdjacencyInfo(node.getPorts().values().iterator().next()) == null;
            else
                return true;
        }

        return true;
    }

    private boolean isValidPortForNewLink(FXGtsPort port) {
        //do not allow ports that already have an adjacency
        if (model.findAdjacency(null, port.getId()) != null) {
            LOG.trace("Port already has an adjacency");
            return false;
        }
        return true;
    }

    private void onDeleteNodePressed(GtsCanvasNode gtsCanvasNode) {
        modelEditor.removeNode(gtsCanvasNode.getNode());
        if (getSelectionProvider().getSelectedObject() == gtsCanvasNode) {
            getSelectionProvider().clearAllSelections();
        }

    }

    private void onDeleteLinkPressed(GtsCanvasLink gtsCanvasLink) {
        modelEditor.removeLink(gtsCanvasLink.getGtsLink());
        if (getSelectionProvider().getSelectedObject() == gtsCanvasLink) {
            getSelectionProvider().clearAllSelections();
        }
    }

    @Override
    protected boolean processNewLinkRequest(NewCanvasLink newCanvasLink) {
        LOG.trace("Received new link request between {} AND {}", newCanvasLink.getOrigin(),
                newCanvasLink.getTarget());

        //delegate the real link-creation to ModelRspecEditor
        LOG.trace("Received link request is OK!");

        if (newCanvasLink.getOrigin() instanceof GtsCanvasNode && newCanvasLink.getTarget() instanceof GtsCanvasNode) {
            final FXGtsNode originNode = ((GtsCanvasNode) newCanvasLink.getOrigin()).getNode();

            final FXGtsNode targetNode = ((GtsCanvasNode) newCanvasLink.getTarget()).getNode();
            FXGtsLink newLink = modelEditor.addLink(originNode, targetNode);

            setStatus("Created a new link '" + newLink.getId() + "'");
            return true;
        } else if (newCanvasLink.getOrigin() instanceof GtsCanvasNode) {
            assert newCanvasLink.getTarget() instanceof GtsCanvasPort;

            final FXGtsNode node = ((GtsCanvasNode) newCanvasLink.getOrigin()).getNode();
            final FXGtsPort port = ((GtsCanvasPort) newCanvasLink.getTarget()).getPort();
            GtsType.Adjacency adjacency = modelEditor.addAdjacency(node, port);
            setStatus("Created an adjacency between '" + node.getId() + "' and '" + port.getId() + "'");
            return true;
        } else if (newCanvasLink.getTarget() instanceof GtsCanvasNode) {
            assert newCanvasLink.getOrigin() instanceof GtsCanvasPort;

            final FXGtsNode node = ((GtsCanvasNode) newCanvasLink.getTarget()).getNode();
            final FXGtsPort port = ((GtsCanvasPort) newCanvasLink.getOrigin()).getPort();
            GtsType.Adjacency adjacency = modelEditor.addAdjacency(node, port);
            setStatus("Created an adjacency between '" + node.getId() + "' and '" + port.getId() + "'");
            return true;
        } else {
            return false;
        }
    }

    @Override
    protected void addCanvasNodeFromToolboxItem(ToolboxItem item, Point2D location) {

        if (item instanceof GTSHostToolboxItem) {
            modelEditor.addHost(location);

        } else if (item instanceof GTSPortToolboxItem) {
            modelEditor.addPort(location);

        } else if (item instanceof GTSOFXToolboxItem) {
            modelEditor.addOFX(location);

        } else if (item instanceof GTSExternalNetworkToolboxItem) {
            FXGtsExternalNetwork externalNetwork = modelEditor.addExternalInterface(location);

            boolean success = onConfigureExternalDomainPressed(externalNetwork);

            if (!success) {
                modelEditor.removeNode(externalNetwork);
            }
        } else {
            throw new RuntimeException("Unexpected type of toolboxItem: " + item.getClass().getName());
        }
    }

    @Override
    protected CanvasNode addCopyOfNodeToCanvas(CanvasNode originalRspecCanvasNode) {
        //TODO implement copy
        return null;
    }

    @Override
    public void canvasItemOnMouseClicked(MouseEvent mouseEvent) {
        super.canvasItemOnMouseClicked(mouseEvent);

        if (mouseEvent.getSource() instanceof GtsCanvasNode) {
            GtsCanvasNode canvasNode = (GtsCanvasNode) mouseEvent.getSource();
            getSelectionProvider().setSelectedObject(canvasNode);

            if (canvasNode.getNode() instanceof FXGtsExternalNetwork) {
                if (mouseEvent.getButton() == MouseButton.PRIMARY && mouseEvent.getClickCount() == 2) {
                    onConfigureExternalDomainPressed((FXGtsExternalNetwork) canvasNode.getNode());

                } else if (mouseEvent.getButton() != MouseButton.PRIMARY || mouseEvent.isControlDown()) {
                    //show context menu
                    externalDomainContextMenu.show(canvasNode, mouseEvent.getScreenX(), mouseEvent.getScreenY());
                }
            } else if (canvasNode.getNode() instanceof FXGtsHost) {
                if (mouseEvent.getButton() == MouseButton.PRIMARY && mouseEvent.getClickCount() == 2) {
                    onConfigureNodePressed(canvasNode);

                } else if (mouseEvent.getButton() != MouseButton.PRIMARY || mouseEvent.isControlDown()) {
                    //show context menu
                    nodeContextMenu.show(canvasNode, mouseEvent.getScreenX(), mouseEvent.getScreenY());
                }
            }

        }

        if (mouseEvent.getSource() instanceof GtsCanvasLink) {
            final GtsCanvasLink link = (GtsCanvasLink) mouseEvent.getSource();
            getSelectionProvider().setSelectedObject(link);

            if (mouseEvent.getButton() == MouseButton.PRIMARY && mouseEvent.getClickCount() == 2) {
                onConfigureLinkPressed(link.getGtsLink());
            } else if (mouseEvent.getButton() != MouseButton.PRIMARY || mouseEvent.isControlDown()) {
                //show context menu
                linkContextMenu.show(link, mouseEvent.getScreenX(), mouseEvent.getScreenY());
            }
        }
    }
}
