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.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.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.GlyphFont;
import org.controlsfx.glyphfont.GlyphFontRegistry;

import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;

/**
 * User: Cedric
 * Date: 19/02/14
 * Time: 14:02
 */
public class TimelineBarrierSegment extends StackPane implements TimelineComponent {
    private final TimelineModel model;
    private final TimelineController controller;
    private final ExperimentBarrierSegment barrierSegment;
    private final TimeCoordinateTranslator translator;
    private final List<TimelineComponent> components;

    private final ChangeListener<Boolean> startedListener = new ChangeListener<Boolean>() {

        @Override
        public void changed(ObservableValue<? extends Boolean> ov, Boolean oldValue, Boolean newValue) {
            if (newValue) {
                t.play();
            }
        }
    };
    private final ChangeListener<Boolean> completedListener = new ChangeListener<Boolean>() {

        @Override
        public void changed(ObservableValue<? extends Boolean> ov, Boolean oldValue, Boolean newValue) {
            if (newValue) {
                t.stop();
            }
        }
    };

    //view
    private final TimelineBase base;
    private final TimelineElapsedTimeIndicator indicator;
    private final TimelineNodes nodes;

    //A Timeline is a timed thread which will handle updating the timeline's progress in time
    //Updating is done asynchronously from the model's value
    private final Timeline t = new Timeline();

    //Keeping track of the last command
    //highest starting time = highest priority
    private final PriorityQueue<ExperimentCommand> queue =
            new PriorityQueue<>(100, (c1, c2) -> Double.compare(c2.getStartingTimeProperty().get(), c1.getStartingTimeProperty().get()));

    private final double WIDTH_OFFSET = 40;
    private double DEFAULT_WIDTH = 100;

    public TimelineBarrierSegment(TimelineModel model, final TimelineController controller, ExperimentBarrierSegment b) {
        this.model = model;
        this.controller = controller;
        this.barrierSegment = b;
        translator = TimeCoordinateTranslator.getInstance();

        b.getStarted().addListener(startedListener);
        b.completedProperty().addListener(completedListener);

        this.setPrefWidth(DEFAULT_WIDTH);

        components = new ArrayList<>();
        
        /*
            Components
        */
        base = new TimelineBase();
        indicator = new TimelineElapsedTimeIndicator(model, controller, barrierSegment, this);
        nodes = new TimelineNodes(model, controller, barrierSegment, this);

        getChildren().addAll(base, nodes, indicator);

        components.add(base);
        components.add(nodes);
        components.add(indicator);

        //Timeline-thread related stuff
        initTimelineThread();

        this.setClip(new Rectangle(getWidth(), getHeight()));
        
        for (FXRspecNode n : model.getNodes()) {
            update(UpdateEvent.ADD_NODE, n);
            for (ExperimentCommand c : n.getCommands()) {
                update(UpdateEvent.ADD_COMMAND, c);
            }
        }

        initContextMenu();
    }

    private void initTimelineThread() {
        //32 ms = 30 fps
        t.getKeyFrames().add(new KeyFrame(Duration.millis(32), actionEvent -> {
            updateComponents(UpdateEvent.UPDATE_TIME, null);

            if (barrierSegment.isCompleted())
                t.stop();
        }));
        t.setCycleCount(Timeline.INDEFINITE);

    }

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

        final ContextMenu mn = new ContextMenu();
        MenuItem mnDelete = new MenuItem("Delete this segment");
        mnDelete.setOnAction(t1 -> controller.deleteBarrierSegment(barrierSegment));
        mnDelete.setGraphic(fontAwesome.create(FontAwesome.Glyph.TIMES).color(Color.RED));

        final MenuItem mnSkip = new MenuItem("Skip this segment");
        mnSkip.setOnAction(t1 -> controller.setElapsedTime(translator.translateWidthToTime(getRequiredWidth()), barrierSegment));
        mnSkip.setGraphic(fontAwesome.create(FontAwesome.Glyph.FAST_FORWARD).color(Color.BLACK));

        final MenuItem mnStartHere = new MenuItem("Start from this segment");
        mnStartHere.setOnAction(t1 -> {
            controller.setElapsedTime(0, barrierSegment);
            controller.startTime();
        });
        mnStartHere.setGraphic(fontAwesome.create(FontAwesome.Glyph.PLAY).color(Color.GREEN));

        if (!model.isOfflineEditor() && !model.allConnectableProperty().get()) {
            mnSkip.setDisable(true);
            mnStartHere.setDisable(true);

            model.allConnectableProperty().addListener(new ChangeListener<Boolean>() {

                @Override
                public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
                    if (t1) {
                        mnSkip.setDisable(false);
                        mnStartHere.setDisable(false);
                        model.allConnectableProperty().removeListener(this);
                    }
                }
            });
        }

        if (!model.isOfflineEditor()) {
            mn.getItems().addAll(mnStartHere, mnSkip);
        }

        mn.getItems().add(mnDelete);

        addEventHandler(MouseEvent.MOUSE_CLICKED, me -> {
            if (me.getButton() == MouseButton.SECONDARY) {
                mn.show(TimelineBarrierSegment.this, me.getScreenX(), me.getScreenY());
                me.consume();
            }
        });

        addEventFilter(MouseEvent.MOUSE_CLICKED, t1 -> mn.hide());

    }

    public void updateComponents(UpdateEvent event, Object o) {
        for (TimelineComponent c : components) {
            c.update(event, o);
        }
    }

    @Override
    public void update(UpdateEvent event, Object o) {

        if (event == UpdateEvent.START_TIME) {
            if (barrierSegment.getStarted().get())
                t.play();
        } else if (event == UpdateEvent.STOP_TIME) {
            t.stop();
        } else if (event == UpdateEvent.REWIND_TIME) {
            t.stop();
        }

        //In case the zoom is changed or a command is added, check if the width has to be increased/decreased
        else if (event == UpdateEvent.UPDATE_ZOOM) {
            setPrefWidth(getRequiredWidth());
        } else if (event == UpdateEvent.ADD_COMMAND) {
            if (((ExperimentCommand) o).getBarrierSegmentOrderNumber() == barrierSegment.getOrderNumber()) {
                queue.add((ExperimentCommand) o);

                setPrefWidth(getRequiredWidth());
            }
        } else if (event == UpdateEvent.DELETE_COMMAND) {
            if (((ExperimentCommand) o).getBarrierSegmentOrderNumber() == barrierSegment.getOrderNumber()) {
                queue.remove(o);
            }
        } else if (event == UpdateEvent.UPDATE_COMMAND) {
            if (((ExperimentCommand) o).getBarrierSegmentOrderNumber() == barrierSegment.getOrderNumber()) {
                //PriorityQueue doesn't update if the object is changed internally
                queue.remove(o);
                queue.add((ExperimentCommand) o);

                setPrefWidth(getRequiredWidth());
            }
        } else if (event == UpdateEvent.CLEAR) {
            t.stop();
        } else if (event == UpdateEvent.UNREGISTER) {
            barrierSegment.getStarted().removeListener(startedListener);
            barrierSegment.completedProperty().removeListener(completedListener);
        }

        updateComponents(event, o);
    }

    public double getRequiredWidth() {
        if (queue.isEmpty()) {
            return DEFAULT_WIDTH;
        } else {
            double requiredCommandWidth = translator.translateTimeToWidth(queue.peek().getStartingTime().toMillis());
            return requiredCommandWidth + WIDTH_OFFSET < DEFAULT_WIDTH ? DEFAULT_WIDTH
                    : requiredCommandWidth + WIDTH_OFFSET;
        }
    }

    public double getDefaultWidth() {
        return DEFAULT_WIDTH;
    }

    /**
     * Sets the new minimum width of this segment. This will automatically check if this component needs to increase its width.
     *
     * @param defWidth
     */
    public void setDefaultWidth(double defWidth) {
        DEFAULT_WIDTH = defWidth;

        setPrefWidth(getRequiredWidth());
    }

    public void updateRequiredWidth(double newCommandX) {
        double requiredWidth = getRequiredWidth();
        setPrefWidth(newCommandX + WIDTH_OFFSET > requiredWidth ? newCommandX + WIDTH_OFFSET : requiredWidth);
    }

    @Override
    public void resize(double d, double d1) {
        super.resize(d, d1);
        this.setClip(new Rectangle(d, d1));
    }


}
