package be.iminds.ilabt.jfed.rspec.model.controller;

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.util.Duration;

import java.util.*;

/**
 * This Class will manage scheduling and executing commands.
 * @author Cedric
 */
public class CommandScheduler {
    
    private long startingTime = 0, elapsedTime = 0;
    
    private final Timeline service;
    
    private final PriorityQueue<ExperimentCommand> queue = new PriorityQueue<>(100,
            Comparator.comparingDouble(c -> c.getStartingTimeProperty().get()));
    
    private final List<ExperimentCommand> commands = new ArrayList<>();
    
    private final Map<ExperimentCommand, ChangeListener<Number>> startingTimeListeners = new HashMap<>();
    
    private final Map<ExperimentCommand, Thread> commandThreads = new HashMap<>();
    private final ExperimentBarrierSegment barrierSegment;
    
    public CommandScheduler(ExperimentBarrierSegment barrierSegment){
        this.barrierSegment = barrierSegment;
        
        service = new Timeline();
        service.getKeyFrames().add(new KeyFrame(Duration.millis(100),new EventHandler<ActionEvent>() {
            @Override 
            public void handle(ActionEvent actionEvent){
                elapsedTime = System.currentTimeMillis() - startingTime;
                if(queue.peek() != null && queue.peek().getStartingTimeProperty().get() <= elapsedTime){
                    final ExperimentCommand c = queue.poll();
                    Thread t = new Thread(c);
                    t.start();            
                    commandThreads.put(c, t);
                    
                    c.getFinishedProperty().addListener(new ChangeListener<Boolean>() {

                        @Override
                        public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
                            if(c.isFinished()){
                                commandThreads.remove(c);
                                c.getFinishedProperty().removeListener(this);
                            }
                        }
                    });
                }
            }
        }));
        service.setCycleCount(Timeline.INDEFINITE);
    }
    
    public void addCommand(final ExperimentCommand command){
        commands.add(command);
        queue.add(command);
        
        ChangeListener<Number> cl = (ov, t, t1) -> {
            //No need to do anything if this barrier segment is completed
            //(moving a command will not put is as to-be-started, this function belongs to setting the elapsed time)
            if(!barrierSegment.isCompleted()){
                queue.remove(command);
                if(command.getStartingTime().toMillis() >= elapsedTime){
                    //Interrupt the command if it is currently running
                    if(commandThreads.containsKey(command)){
                        commandThreads.get(command).interrupt();
                        commandThreads.remove(command);
                    }
                    command.resetCommand();
                    queue.add(command);
                } else{
                    command.setSkipped(true);
                    command.setFinished(true);
                    command.setStarted(true);
                }
            }
        };
        
        command.getStartingTimeProperty().addListener(cl);
        
        startingTimeListeners.put(command, cl);
    }
    
    public void deleteCommand(ExperimentCommand command){
        commands.remove(command);
        queue.remove(command);
        
        command.getStartingTimeProperty().removeListener(startingTimeListeners.get(command));
        startingTimeListeners.remove(command);
    }
    
    /**
     * Starts the execution of scheduled commands.
     */
    public void startTime(){
        startingTime = System.currentTimeMillis() - elapsedTime;
        service.play();
    }
    
    public void setElapsedTime(long t){        
        elapsedTime = t;
        if(Objects.equals(service.getStatus(), Animation.Status.RUNNING)){
            startingTime = System.currentTimeMillis() - elapsedTime;
        }
        
        //This will make sure any running thread is interrupted
        //Calling setFinished(true) will remove the listener added to the command by the timeline service
        for(ExperimentCommand comm : commandThreads.keySet()){
            commandThreads.get(comm).interrupt();
            comm.setFinished(true);
        }
        
        queue.clear();
        for(ExperimentCommand rc : commands){
            if(rc.getStartingTimeProperty().get() >= elapsedTime){
                rc.resetCommand();
                queue.add(rc);
            } else {
                rc.setSkipped(true);
                rc.setStarted(true);
                rc.setFinished(true);
            }
        }
        
    }
    
    public void stopTime(){
        service.stop();
    }    
    
    public void reset(){
        service.stop();
        queue.clear();
        commands.clear();
        startingTime = 0;
        
    }
    
    public void rewind(){
        service.stop();
        startingTime = 0;
        setElapsedTime(0);
    }

    public void unregister(){
        for(ExperimentCommand c : startingTimeListeners.keySet()){
            c.getStartingTimeProperty().removeListener(startingTimeListeners.get(c));
        }
        startingTimeListeners.clear();
        service.stop();
    }    
}
