package be.iminds.ilabt.jfed.experimenter_gui.timeline.view;

import be.iminds.ilabt.jfed.experimenter_gui.timeline.controller.TimelineController;
import be.iminds.ilabt.jfed.experimenter_gui.timeline.model.TimelineModel;
import be.iminds.ilabt.jfed.experiment.SshConnectionPool;
import be.iminds.ilabt.jfed.experimenter_gui.timeline.view.dialogs.AddMultipleCommandDialog;
import be.iminds.ilabt.jfed.experimenter_gui.timeline.view.dialogs.CommandPropertiesDialog;
import be.iminds.ilabt.jfed.experimenter_gui.slice.NodePropertiesDialogFactory;
import be.iminds.ilabt.jfed.experimenter_gui.util.ImageSize;
import be.iminds.ilabt.jfed.experimenter_gui.util.ImageUtil;
import be.iminds.ilabt.jfed.rspec.model.controller.ExperimentBarrierSegment;
import be.iminds.ilabt.jfed.rspec.model.controller.ExperimentCommand;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.FXRspecNode;
import javafx.beans.InvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.paint.Color;
import javafx.util.Pair;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.GlyphFont;
import org.controlsfx.glyphfont.GlyphFontRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

/**
 * @author Cedric
 */
public class TimelineTreeView extends TreeView<Object> implements TimelineComponent {
    private final static Logger LOG = LoggerFactory.getLogger(TimelineTreeView.class);

    private final TimelineModel model;
    private final TimelineController controller;
    private final TreeItem<Object> rootItem;
    private ContextMenu ctxNode, ctxCommand, ctxBarrierSegment;

    private final Map<ExperimentCommand, ChangeListener<Number>> startingTimeListeners = new HashMap<>();

    private final List<ExperimentBarrierSegment> addedBarriers = new ArrayList<>();
    private final Map<String, Pair<FXRspecNode, TreeItem<Object>>> nodesByUniqueId = new HashMap<>();
    private final NodePropertiesDialogFactory nodePropertiesDialog;
    private final ImageUtil imageUtil;

    public TimelineTreeView(TimelineModel model, TimelineController controller, NodePropertiesDialogFactory nodePropertiesDialog, ImageUtil imageUtil) {
        this.nodePropertiesDialog = nodePropertiesDialog;
        this.imageUtil = imageUtil;
        getStylesheets().add(TimelineTreeView.class.getResource("TreeView.css").toExternalForm());

        this.model = model;
        this.controller = controller;

        rootItem = new TreeItem<>();
        rootItem.setExpanded(true);
        this.setRoot(rootItem);
        this.setShowRoot(false);
//        this.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

        this.setCellFactory(p -> new ControllerTreeCell());

        initContextMenus();
    }

    private void addBarrier(ExperimentBarrierSegment b) {
        LOG.debug("addBarrier \""+b.getTag()+"\" nr="+b.getOrderNumber());

        for (TreeItem<Object> t : rootItem.getChildren()) {
            TreeItem<Object> tb = new TreeItem<>(b);
            t.getChildren().add(tb);
        }

        b.getCommands().forEach(this::addCommand);

        addedBarriers.add(b);
        LOG.debug("addBarrier done");
    }

    private void deleteBarrier(ExperimentBarrierSegment b) {
        for (TreeItem<Object> nodeItem : rootItem.getChildren()) {
            for (TreeItem<Object> barrierSegmentItem : nodeItem.getChildren()) {
                if (Objects.equals(barrierSegmentItem.getValue(), b)) {
                    nodeItem.getChildren().remove(barrierSegmentItem);
                    break;
                }
            }

        }

        addedBarriers.remove(b);
    }

    private void addCommand(ExperimentCommand c) {
        LOG.debug("addCommand \""+c.getCommand()+"\" @ "+c.getNodeUniqueID());
        //Find the correct node
        Pair<FXRspecNode, TreeItem<Object>> pair = nodesByUniqueId.get(c.getNodeUniqueID());
        if (pair == null) {
            //node not yet added!
            //may be ignored, when nod is added later, it will add the barrier and commands.
            return;
        }
        TreeItem<Object> nodeItem = pair.getValue();

        assert Objects.equals(((FXRspecNode) nodeItem.getValue()).getUniqueId(), c.getNodeUniqueID());

        //Find the correct barrier segment
        for (final TreeItem<Object> barrierSegmentItem : nodeItem.getChildren()) {
            if (((ExperimentBarrierSegment) barrierSegmentItem.getValue()).getOrderNumber() == c.getBarrierSegmentOrderNumber()) {
                TreeItem<Object> tc = new TreeItem<>(c);
                barrierSegmentItem.getChildren().add(tc);
//                        FontAwesomeIconView icon = new FontAwesomeIconView(FontAwesomeIcon.ICON_TERMINAL);
//                        icon.setColor(Color.rgb(120, 255, 120));
//                        tc.setGraphic(icon);

                ChangeListener<Number> cl = (ov, a, b) -> checkSorting(barrierSegmentItem);
                c.getStartingTimeProperty().addListener(cl);
                startingTimeListeners.put(c, cl);

                checkSorting(barrierSegmentItem);

                LOG.debug("addCommand done");
                return;
            }
        }
        assert false : "Something wrong in implementation. Could not find segment to add command to. command segment order nr="+c.getBarrierSegmentOrderNumber()+" segmentcount="+nodeItem.getChildren().size();

        LOG.debug("addCommand done");
    }

    private void deleteCommand(ExperimentCommand c) {
        c.getStartingTimeProperty().removeListener(startingTimeListeners.get(c));
        startingTimeListeners.remove(c);

        for (TreeItem<Object> nodeItem : rootItem.getChildren()) {
            if (Objects.equals(((FXRspecNode) nodeItem.getValue()).getUniqueId(), c.getNodeUniqueID())) {
                for (TreeItem<Object> barrierSegmentItem : nodeItem.getChildren()) {
                    if (((ExperimentBarrierSegment) barrierSegmentItem.getValue()).getOrderNumber() == c.getBarrierSegmentOrderNumber()) {
                        for (TreeItem<Object> commandItem : barrierSegmentItem.getChildren()) {
                            if (Objects.equals(commandItem.getValue(), c)) {
                                if (startingTimeListeners.containsKey(c)) {
                                    c.getStartingTimeProperty().removeListener(startingTimeListeners.get(c));
                                    startingTimeListeners.remove(c);
                                }
                                barrierSegmentItem.getChildren().remove(commandItem);
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

    private void addNode(FXRspecNode n) {
        LOG.debug("addNode "+n.getUniqueId());
        TreeItem<Object> nodeItem = new TreeItem<>(n);
        nodeItem.setExpanded(false);
        rootItem.getChildren().add(nodeItem);
        nodesByUniqueId.put(n.getUniqueId(), new Pair<>(n, nodeItem));

        for (ExperimentBarrierSegment b : addedBarriers) {
            TreeItem<Object> tb = new TreeItem<>(b);
            nodeItem.getChildren().add(tb);

            for (ExperimentCommand c : b.getCommands()) {
                if (Objects.equals(c.getNodeUniqueID(), n.getUniqueId()))
                    addCommand(c);
            }
        }
        LOG.debug("addNode done");
    }

    /**
     * Insertion sort on the children of the nodeItem
     * All children must have an ExperimentCommand as its value!
     *
     * @param barrierSegmentItem a TreeItem<Object> containing an ExperimentBarrierSegment as its value
     */
    private void checkSorting(TreeItem<Object> barrierSegmentItem) {
        for (int i = 0; i < barrierSegmentItem.getChildren().size(); i++) {
            ExperimentCommand A = (ExperimentCommand) barrierSegmentItem.getChildren().get(i).getValue();
            int j = i;
            while (j > 0 && ((ExperimentCommand) barrierSegmentItem.getChildren().get(j - 1).getValue()).getStartingTimeProperty().doubleValue() > A.getStartingTimeProperty().doubleValue()) {
                barrierSegmentItem.getChildren().get(j).setValue(barrierSegmentItem.getChildren().get(j - 1).getValue()); //,  nodeItem.getChildren().get(j-1));
                j--;
            }
            barrierSegmentItem.getChildren().get(j).setValue(A);
        }
    }

    private MenuItem mnNodeInfo;
    //    private MenuItem mnNodeProperties;
    private MenuItem mnNodeOutput;
    private MenuItem mnNodeAdd;
    private MenuItem mnCommandOutput;
    private MenuItem mnSaveOutput;

    private void initContextMenus() {
        GlyphFont fontAwesome = GlyphFontRegistry.font("FontAwesome");

        //Command
        ctxCommand = new ContextMenu();
        MenuItem mnDelete = new MenuItem("Delete command");
        mnDelete.setOnAction(t -> controller.deleteCommand((ExperimentCommand) (getSelectionModel().getSelectedItem().getValue())));
        mnDelete.setGraphic(fontAwesome.create(FontAwesome.Glyph.TIMES).color(Color.RED));

        MenuItem mnDuplicate = new MenuItem("Duplicate command");
        mnDuplicate.setOnAction(t -> AddMultipleCommandDialog.showSimpleAddDialog(TimelineTreeView.this.getScene().getWindow(), controller, model.getNodes(),
                (ExperimentCommand) getSelectionModel().getSelectedItem().getValue()));
        mnDuplicate.setGraphic(fontAwesome.create(FontAwesome.Glyph.COPY).color(Color.BLUE));

        mnSaveOutput = new MenuItem("Save output");
        mnSaveOutput.setOnAction(t -> {
            ArrayList<ExperimentCommand> commands = new ArrayList<>();
            if (( getSelectionModel().getSelectedItem()).getValue() instanceof ExperimentCommand) {
                commands.add((ExperimentCommand) getSelectionModel().getSelectedItem().getValue());
            } else {
                commands.addAll(((FXRspecNode) (getSelectionModel().getSelectedItem()).getValue()).getCommands());
            }
            controller.saveOutput(commands);
        });
        mnSaveOutput.setGraphic(fontAwesome.create(FontAwesome.Glyph.SAVE).color(Color.BLUE));

        mnCommandOutput = new MenuItem("Show output");
        mnCommandOutput.setOnAction(t -> controller.showCommandOutput((ExperimentCommand) getSelectionModel().getSelectedItem().getValue()));
        mnCommandOutput.setGraphic(fontAwesome.create(FontAwesome.Glyph.REORDER).color(Color.BLUE));

        MenuItem mnProp = new MenuItem("Configure command");
        mnProp.setOnAction(t -> CommandPropertiesDialog.showSimplePropertiesDialog(TimelineTreeView.this.getScene().getWindow(), controller,
                (ExperimentCommand) (getSelectionModel().getSelectedItem()).getValue()));
        mnProp.setGraphic(fontAwesome.create(FontAwesome.Glyph.PENCIL).color(Color.BLACK));

        ctxCommand.getItems().addAll(mnDuplicate, mnCommandOutput, mnSaveOutput, mnProp, mnDelete);

        //////////////////////////////////////////////////////////////////////////////////////////////////////
        //Node      
        ctxNode = new ContextMenu();
        mnNodeAdd = new MenuItem("Add command");
        mnNodeAdd.setOnAction(t -> AddMultipleCommandDialog.showSimpleAddDialog(getScene().getWindow(), controller, model.getNodes(),
                (FXRspecNode) getSelectionModel().getSelectedItem().getValue(), null));
        mnNodeAdd.setGraphic(fontAwesome.create(FontAwesome.Glyph.PLUS).color(Color.GREEN));

        mnNodeOutput = new MenuItem("Show output");
        mnNodeOutput.setOnAction(t -> controller.showNodeOutput((FXRspecNode) getSelectionModel().getSelectedItem().getValue()));
        mnNodeOutput.setGraphic(fontAwesome.create(FontAwesome.Glyph.REORDER).color(Color.BLUE));
        ctxNode.getItems().addAll(mnNodeAdd, mnNodeOutput, mnSaveOutput);

//            mnNodeProperties = new MenuItem("Configure node");
//            mnNodeProperties.setOnAction(new EventHandler<ActionEvent>() {
//
//                @Override
//                public void handle(ActionEvent t) {
//                    AdaptivePropertiesDialog.showPropertiesDialog(ControllerTreeView.this.getScene().getWindow(),
//                            model.getModelRspecEditor(),
//                            (FXRspecNode) getSelectionModel().getSelectedItem().getValue());
//                }
//            });
//            mnNodeProperties.setGraphic(fontAwesome.create(FontAwesome.Glyph.PENCIL).color(Color.BLACK));
//
//            ctxNode.getItems().add(mnNodeProperties);


        mnNodeInfo = new MenuItem("Show node info");
        mnNodeInfo.setOnAction(t -> nodePropertiesDialog.showSimplePropertiesDialog(model.getExperiment().getSlice(),
                (FXRspecNode) getSelectionModel().getSelectedItem().getValue()));
        mnNodeInfo.setGraphic(fontAwesome.create(FontAwesome.Glyph.INFO).color(Color.BLACK));
        ctxNode.getItems().add(mnNodeInfo);


        //////////////////////////////////////////////////////////////////////////////////////////////////////
        //Barrier segment  
        ctxBarrierSegment = new ContextMenu();
        MenuItem mnAddCommand = new MenuItem("Add command");
        mnAddCommand.setOnAction(t -> AddMultipleCommandDialog.showSimpleAddDialog(getScene().getWindow(), controller, model.getNodes(),
                (FXRspecNode) getSelectionModel().getSelectedItem().getParent().getValue(),
                (ExperimentBarrierSegment) getSelectionModel().getSelectedItem().getValue()));
        mnAddCommand.setGraphic(fontAwesome.create(FontAwesome.Glyph.PLUS).color(Color.GREEN));

        MenuItem mnDeleteBarrier = new MenuItem("Delete this barrier segment");
        mnDeleteBarrier.setOnAction(t -> controller.deleteBarrierSegment((ExperimentBarrierSegment) getSelectionModel().getSelectedItem().getValue()));
        mnDeleteBarrier.setGraphic(fontAwesome.create(FontAwesome.Glyph.TIMES).color(Color.RED));

        ctxBarrierSegment.getItems().addAll(mnAddCommand, mnDeleteBarrier);
    }

    public void updateCommandContextMenu(ExperimentCommand command) {
        mnCommandOutput.setDisable(model.isOfflineEditor());
        mnSaveOutput.setDisable(model.isOfflineEditor());
    }

    public void updateNodeContextMenu(FXRspecNode node) {
        boolean noInfo = model.getSshConnectionPool() == null ||
                model.getSshConnectionPool().getNodeState(node.getUniqueId()).get().equals(SshConnectionPool.NodeState.NO_INFO);

        mnNodeInfo.setVisible(!model.isOfflineEditor());
        mnSaveOutput.setVisible(!model.isOfflineEditor());

        mnNodeInfo.setDisable(noInfo);
        mnNodeAdd.setDisable(noInfo);

        mnNodeOutput.setDisable(noInfo);
        mnSaveOutput.setDisable(noInfo);
    }

    @Override
    public void update(UpdateEvent event, Object o) {
        if (event == UpdateEvent.ADD_BARRIER) {
            ExperimentBarrierSegment b = (ExperimentBarrierSegment) o;
            addBarrier(b);
        } else if (event == UpdateEvent.DELETE_BARRIER) {
            ExperimentBarrierSegment b = (ExperimentBarrierSegment) o;
            deleteBarrier(b);
        } else if (event == UpdateEvent.ADD_COMMAND) {
            ExperimentCommand c = (ExperimentCommand) o;
            addCommand(c);
        } else if (event == UpdateEvent.ADD_NODE) {
            FXRspecNode n = (FXRspecNode) o;
            addNode(n);
        } else if (event == UpdateEvent.DELETE_COMMAND) {
            ExperimentCommand c = (ExperimentCommand) o;
            deleteCommand(c);
        } else if (event == UpdateEvent.UNREGISTER) {
            for (TreeItem<Object> nodeItem : rootItem.getChildren()) {
                for (TreeItem<Object> barrierSegmentItem : nodeItem.getChildren()) {
                    for (TreeItem<Object> commandItem : barrierSegmentItem.getChildren()) {
                        ExperimentCommand c = (ExperimentCommand) commandItem.getValue();
                        if (startingTimeListeners.containsKey(c)) {
                            c.getStartingTimeProperty().removeListener(startingTimeListeners.get(c));
                            startingTimeListeners.remove(c);
                        }
                    }
                }

            }
            startingTimeListeners.clear();

            //Clearing TreeCells should call an update(item, empty) which removes the listeners bound to that cell
            for (TreeItem t : rootItem.getChildren()) {
                t.getChildren().clear();
            }
            rootItem.getChildren().clear();
        }
    }

    private final class ControllerTreeCell extends TreeCell<Object> {
        private final GlyphFont fontAwesome = GlyphFontRegistry.font("FontAwesome");
        private Pair<ExperimentCommand, InvalidationListener> commandTagListener, commandStartingTimeListener;

        public ControllerTreeCell() {
            this.setOnContextMenuRequested(t -> {
                TimelineTreeView.this.getSelectionModel().select(getTreeItem());
                try {
                    getContextMenu().show(ControllerTreeCell.this, t.getScreenX(), t.getScreenY());
                } catch (Exception ex) {
                    //On the first contextMenu this can give an odd error with no real implications if it's plainly ignored
                    //Might have something to do with not properly selecting an item while no item has been selected before
                }
            });
        }

        @Override
        public void updateItem(Object item, boolean empty) {
            super.updateItem(item, empty);

            //unbind
            textProperty().unbind();
            //Remove previous listeners
            if (commandTagListener != null && commandStartingTimeListener != null) {
                commandTagListener.getKey().getTagProperty().removeListener( commandTagListener.getValue());
                commandStartingTimeListener.getKey().getTagProperty().removeListener(commandStartingTimeListener.getValue());
                commandTagListener = null;
                commandStartingTimeListener = null;
            }

            if (empty) {
                //setText(null);
                setGraphic(null);
            } else {
                if (item instanceof FXRspecNode) {
                    final FXRspecNode node = (FXRspecNode) item;
                    textProperty().bind(node.clientIdProperty());

                    ImageView icon = new ImageView(imageUtil.getRspecNodeImage(node, ImageSize.X_SMALL));
                    icon.setFitHeight(16);
                    icon.setFitWidth(16);
                    setGraphic(icon);

                    updateNodeContextMenu(node);
                    setContextMenu(ctxNode);
                } else if (item instanceof ExperimentCommand) {
                    final ExperimentCommand c = (ExperimentCommand) item;
                    setText(c.getStartingTime().toString() + " " + c.getTag());
                    InvalidationListener cl1 = observable -> setText(c.getStartingTime().toString() + " " + c.getTag());
                    c.getTagProperty().addListener(cl1);
                    commandTagListener = new Pair<>(c, cl1);

                    InvalidationListener cl2 = observable -> setText(c.getStartingTime().toString() + " " + c.getTag());
                    c.getStartingTimeProperty().addListener(cl2);
                    commandStartingTimeListener = new Pair<>(c, cl2);

                    setGraphic(fontAwesome.create(FontAwesome.Glyph.TERMINAL).color(Color.rgb(120,255,120) /* #78ff78 */));

                    updateCommandContextMenu(c);
                    setContextMenu(ctxCommand);
                } else if (item instanceof ExperimentBarrierSegment) {
                    final ExperimentBarrierSegment b = (ExperimentBarrierSegment) item;
                    textProperty().bind(b.tagProperty());

                    setGraphic(fontAwesome.create(FontAwesome.Glyph.SITEMAP).color(Color.GREEN));
                    getGraphic().setRotate(90);

                    setContextMenu(ctxBarrierSegment);
                }
            }
        }
    }

}
