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.experimenter_gui.timeline.util.TimeCoordinateTranslator;
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.rspec.model.controller.ExperimentCommand;
import be.iminds.ilabt.jfed.rspec.model.controller.Timestamp;
import javafx.beans.property.DoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Point2D;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.GlyphFont;
import org.controlsfx.glyphfont.GlyphFontRegistry;

import java.util.ArrayList;


/**
 * @author Cedric
 */
public class TimelineNodeCommand implements TimelineComponent {

    private final Line dur;
    private final Rectangle command;
    private final double length = 15;

    private final Tooltip tooltip;
    private ContextMenu menu;

    private final ExperimentCommand experimentCommand;
    private final TimelineModel model;
    private final TimelineController controller;
    private final TimeCoordinateTranslator translator;
    private final TimelineNode parent;
    private final TimelineBarrierSegment segmentParent;

    private ChangeListener<Number> startingTimeListener;
    private ChangeListener<Number> durationListener;
    private ChangeListener<Boolean> startedListener;
    private ChangeListener<Boolean> finishedListener;

    public TimelineNodeCommand(int commandID, TimelineModel model, TimelineController controller, TimelineNode parent, TimelineBarrierSegment segmentParent) {
        this.model = model;
        this.controller = controller;
        this.translator = TimeCoordinateTranslator.getInstance();
        this.parent = parent;
        this.segmentParent = segmentParent;

        this.experimentCommand = model.getCommand(commandID);

        command = new Rectangle();
        command.setHeight(length);
        command.setWidth(length);
        command.setArcWidth(length / 2);
        command.setArcHeight(length / 2);

        if (!experimentCommand.isFinished())
            command.getStyleClass().add("command");
        else
            command.getStyleClass().add("commandDone");

        dur = new Line();
        dur.setMouseTransparent(true);
        dur.getStyleClass().add("duration");

        Timestamp time = experimentCommand.getStartingTime();
        tooltip = new Tooltip(time.toString() + " " + experimentCommand.getTag());
        tooltip.getStyleClass().add("tooltip");

        parent.getChildren().add(dur);
        parent.getChildren().add(command);

        initMenu();
        initMoveHandlers();
        initClickHandler();
        initTooltipHandlers();
        initChangeListeners();
    }

    /**
     * Dragging a node past the maxWidth of the ControllerNode will cause the
     * ControllerNode to increase its width
     * <p/>
     * IMPORTANT: A node cannot be dragged beyond the elapsed time. Doing this
     * will set the time to the elapsed time, causing the command to start
     */
    private void initMoveHandlers() {
        command.setOnMouseDragged(t -> {
            if (t.getButton() == MouseButton.PRIMARY) {
                double x;
                if (t.getX() < 0) {
                    x = 0;
                } else {
                    x = t.getX();
                }

                setCenterX(x);
                Timestamp time = new Timestamp(translator.translateWidthToTime(x));
                tooltip.setText(time.toString() + " " + experimentCommand.getTag());
                Point2D p = command.localToScene(0.0, 0.0);
                tooltip.show(command
                        , command.getScene().getWindow().getX() + command.getScene().getX() + p.getX() + command.getX() + length
                        , command.getScene().getWindow().getY() + command.getScene().getY() + p.getY() + command.getY() + length);
                segmentParent.updateRequiredWidth(x);
            }
        });

        command.setOnMousePressed(t -> {
            if (t.getButton() == MouseButton.PRIMARY) {
                command.setOnMouseExited(null);
            }
        });

        command.setOnMouseReleased(t -> {
            if (!command.isHover()) {
                tooltip.hide();
                unHighlight();
            }
            initTooltipHandlers();

            if (t.getButton() == MouseButton.PRIMARY)
                controller.updateCommand(experimentCommand
                        , new ExperimentCommand(experimentCommand.getTag(), experimentCommand.getNodeUniqueID(),
                        experimentCommand.getBarrierSegmentOrderNumber(), experimentCommand.getCommand(),
                        new Timestamp(translator.translateWidthToTime(command.getX() + length / 2)),
                        experimentCommand.getDuration()));
        });

    }

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

        menu = new ContextMenu();
        MenuItem mnProp = new MenuItem("Configure command");
        mnProp.setOnAction(t -> CommandPropertiesDialog.showSimplePropertiesDialog(parent.getScene().getWindow(), controller, experimentCommand));
        mnProp.setGraphic(fontAwesome.create(FontAwesome.Glyph.PENCIL).color(Color.BLACK));

        MenuItem mnDelete = new MenuItem("Delete command");
        mnDelete.setOnAction(t -> controller.deleteCommand(experimentCommand));
        mnDelete.setGraphic(fontAwesome.create(FontAwesome.Glyph.TIMES).color(Color.RED));

        MenuItem mnOutput = new MenuItem("Show output");
        mnOutput.setOnAction(t -> controller.showCommandOutput(experimentCommand));
        mnOutput.setGraphic(fontAwesome.create(FontAwesome.Glyph.REORDER).color(Color.BLUE));

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

        MenuItem mnSaveOutput = new MenuItem("Save output");
        mnSaveOutput.setOnAction(t -> {
            ArrayList<ExperimentCommand> commands = new ArrayList<>();
            commands.add(experimentCommand);
            controller.saveOutput(commands);
        });
        mnSaveOutput.setGraphic(fontAwesome.create(FontAwesome.Glyph.SAVE).color(Color.BLUE));

        menu.getItems().addAll(mnDuplicate, mnOutput, mnSaveOutput, mnProp, mnDelete);

        mnOutput.setDisable(model.isOfflineEditor());
        mnSaveOutput.setDisable(model.isOfflineEditor());
    }

    private void initClickHandler() {
        parent.addEventFilter(MouseEvent.MOUSE_CLICKED, t -> menu.hide());

        command.setOnMouseClicked(me -> {
            if (me.getButton() == MouseButton.SECONDARY) {
                menu.show(parent, me.getScreenX(), me.getScreenY());
                me.consume();
            }
        });
    }

    private void initTooltipHandlers() {
        command.setOnMouseEntered(t -> {
            Point2D p = command.localToScene(0.0, 0.0);
            tooltip.show(command
                    , command.getScene().getWindow().getX() + command.getScene().getX() + p.getX() + command.getX() + length
                    , command.getScene().getWindow().getY() + command.getScene().getY() + p.getY() + command.getY() + length);
            highight();
        });

        command.setOnMouseExited(t -> {
            tooltip.hide();
            unHighlight();
        });
    }

    private void initChangeListeners() {
        startingTimeListener = (ov, t, t1) -> setCenterX(translator.translateTimeToWidth((double) t1));
        experimentCommand.getStartingTimeProperty().addListener(startingTimeListener);

        durationListener = (ov, t, t1) -> dur.setEndX(dur.getStartX() + translator.translateTimeToWidth((double) t1));
        experimentCommand.getDurationProperty().addListener(durationListener);

        startedListener = (ov, t, t1) -> {
            //Command is in progress
            if (experimentCommand.isStarted() && !experimentCommand.isFinished()) {
                command.getStyleClass().removeAll(command.getStyleClass());
                dur.getStyleClass().removeAll(dur.getStyleClass());
                command.getStyleClass().add("commandInProgress");
                dur.getStyleClass().add("durationInProgress");
            }
            //Command has not been started
            else if (!experimentCommand.isStarted()) {
                command.getStyleClass().removeAll(command.getStyleClass());
                dur.getStyleClass().removeAll(dur.getStyleClass());
                command.getStyleClass().add("command");
                dur.getStyleClass().add("duration");
            }
            //Command is finished or skipped
            else {
                command.getStyleClass().removeAll(command.getStyleClass());
                dur.getStyleClass().removeAll(dur.getStyleClass());
                command.getStyleClass().add("commandDone");
                dur.getStyleClass().add("durationDone");
            }
        };
        experimentCommand.getStartedProperty().addListener(startedListener);

        finishedListener = (ov, t, t1) -> {
            //Command is in progress
            if (!experimentCommand.isFinished() && experimentCommand.isStarted()) {
                command.getStyleClass().removeAll(command.getStyleClass());
                dur.getStyleClass().removeAll(dur.getStyleClass());
                command.getStyleClass().add("commandInProgress");
                dur.getStyleClass().add("durationInProgress");
            }
            //Command has not been started
            else if (!experimentCommand.isFinished() && !experimentCommand.isStarted()) {
                command.getStyleClass().removeAll(command.getStyleClass());
                dur.getStyleClass().removeAll(dur.getStyleClass());
                command.getStyleClass().add("command");
                dur.getStyleClass().add("duration");
            }
            //Command is finished or skipped
            else {
                command.getStyleClass().removeAll(command.getStyleClass());
                dur.getStyleClass().removeAll(dur.getStyleClass());
                command.getStyleClass().add("commandDone");
                dur.getStyleClass().add("durationDone");
            }
        };
        experimentCommand.getFinishedProperty().addListener(finishedListener);
    }

    public void highight() {
        command.getStyleClass().add("commandHighlight");
        dur.getStyleClass().add("durationHighlight");
    }

    public void unHighlight() {
        command.getStyleClass().removeAll("commandHighlight");
        dur.getStyleClass().removeAll("durationHighlight");
    }

    public int getID() {
        return experimentCommand.getID();
    }

    public void setCenterX(double x) {
        command.setX(x - length / 2);

        dur.setStartX(x);
        dur.setEndX(x + translator.translateTimeToWidth(experimentCommand.getDuration().toMillis()));
    }

    public void setCenterY(double y) {
        command.setY(y - length / 2);

        dur.setStartY(y);
        dur.setEndY(y);

    }

    public DoubleProperty xProperty() {
        return command.xProperty();
    }

    @Override
    public void update(UpdateEvent event, Object o) {
        if (event == UpdateEvent.UPDATE_ZOOM) {
            setCenterX(translator.translateTimeToWidth(experimentCommand.getStartingTime().toMillis()));
        } else if (event == UpdateEvent.REWIND_TIME) {
            command.getStyleClass().remove("commandDone");
            command.getStyleClass().add("command");

            dur.getStyleClass().remove("durationDone");
            dur.getStyleClass().add("duration");
        } else if (event == UpdateEvent.CLEAR) {
            parent.getChildren().removeAll(dur, command);
        } else if (event == UpdateEvent.UNREGISTER) {
            experimentCommand.getStartedProperty().removeListener(startedListener);
            experimentCommand.getStartingTimeProperty().removeListener(startingTimeListener);
            experimentCommand.getDurationProperty().removeListener(durationListener);
            experimentCommand.getFinishedProperty().removeListener(finishedListener);
        }
    }
}
