package be.iminds.ilabt.jfed.highlevel.controller;

import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetails;
import be.iminds.ilabt.jfed.log.cache.ApiCallDetailsCache;
import be.iminds.ilabt.jfed.log.cache.ApiCallDetailsRef;
import be.iminds.ilabt.jfed.util.MappedList;
import javafx.application.Platform;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.*;
import javafx.beans.value.ObservableLongValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * JavaFXTaskThread provides a JavaFX model for the data in the TaskThread.
 * TaskThread contains no JavaFX code, but does contain some helpers to make the
 * implementation of this class easier (see the javafx fields)
 * <p/>
 * Periodically, this model is updated so it reflects the situation in TaskThread.
 * <p/>
 * This method of showing everything in JavaFX is some extra work, but it simplifies things.
 * JavaFX isn't very easy to use with multiple threads otherwise.
 * (it's hard to use ObservableList and properties from other threads.)
 * <p/>
 * Performance could be improved if required. Some hints:
 * - use a "dirty" flag on TaskThread objects so that only changed objects are updated
 * - alternatively use lists with a builtin "dirty" flag  (boolean javafx_dirty gets set to true by each changes, while this thread sets it to false after update)
 * - take into account that some lists only get added to, so no checks are needed as long as size doesn't change
 * - take into account that some objects start null, get a value assigned, and never change after that (exception, start and stop date, etc)
 * - check if ObservableList.setAll is smart enough to see a list is unchanged or not (now assumed it isn't)
 * - trigger updates only when things are changed? (but still at limited rate)
 */
@Singleton
public class JavaFXTaskThread {
    private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(JavaFXTaskThread.class);

    private final TaskThread real;
    private final ApiCallDetailsCache apiCallDetailsCache;

    private final ObservableList<Task> tasks = FXCollections.observableArrayList();
    private final ObservableList<FXTaskExecution> FXTaskExecutions = FXCollections.observableArrayList();
    private final ObservableList<FutureTask> futureTasks = FXCollections.observableArrayList();
    private final ReadOnlyIntegerWrapper callsCount = new ReadOnlyIntegerWrapper(0);
    private final ReadOnlyIntegerWrapper unfinishedCallsCount = new ReadOnlyIntegerWrapper(0);
    private final BooleanExpression busy = unfinishedCallsCount.greaterThan(0);
    private int taskdebug_helper_PreviousRealAllTasksSize = -1;

    @Inject
    JavaFXTaskThread(TaskThread real, ApiCallDetailsCache apiCallDetailsCache) {
        this.real = real;
        this.apiCallDetailsCache = apiCallDetailsCache;

        Thread updateThread = new Thread(() -> {
            while (!JavaFXTaskThread.this.real.isStopRequested()) {
                //max 5 times per second
                try {
                    Thread.sleep(200);
                } catch (InterruptedException ignored) {
                }

                synchronized (JavaFXTaskThread.this) {
                    Platform.runLater(() -> {
                        synchronized (JavaFXTaskThread.this) {
                            update();
                            JavaFXTaskThread.this.notifyAll();
                        }
                    });

                    //wait until update complete (important if update takes longer than wait)
                    try {
                        JavaFXTaskThread.this.wait();
                    } catch (InterruptedException e) {
                        LOG.warn("Interrupted while waiting for JavaFXTaskThread update",e);
                    }
                }
            }
        });
        updateThread.setDaemon(true);
        updateThread.setName("JavaFXTaskThread");
        LOG.debug("Starting JavaFXTaskThread periodic updates");
        updateThread.start();
    }

    private static <T> void syncList(List<T> orig, ObservableList<T> target) {
        boolean changed = false;
        if (orig.size() == target.size())
            for (int i = 0; i < orig.size(); i++) {
                if (orig.get(i) != target.get(i)) {
                    changed = true;
                    break;
                }
            }
        else
            changed = true;

        if (changed)
            target.setAll(orig);
    }

    public void update() {
        int unfinishedCount = 0;
        //create new FutureTask, FXTaskExecution and Tasks
        if (LOG.isDebugEnabled() && taskdebug_helper_PreviousRealAllTasksSize != real.allTasks.size())
            LOG.debug("JavaFXTaskThread periodic update: real.allTasks.size()==" + real.allTasks.size());

        synchronized(real.allTasks) {
            for (TaskExecution taskExecution : real.allTasks) {
                be.iminds.ilabt.jfed.highlevel.controller.Task task = taskExecution.task;

                TaskExecution.TaskState state = taskExecution.getState();
                if (state == TaskExecution.TaskState.QUEUED
                        || state == TaskExecution.TaskState.RUNNING)
                    unfinishedCount++;

                if (task.javafx == null) {
                    task.javafx = new Task(task);
                    tasks.add(task.javafx);
                }

                for (TaskThread.FutureTask futureTask : task.futureTasks)
                    if (futureTask.javafx == null) {
                        futureTask.javafx = new FutureTask(futureTask);
                        futureTasks.add(futureTask.javafx);
                    }

                if (taskExecution.javafx == null) {
                    taskExecution.javafx =
                            new FXTaskExecution(taskExecution, taskExecution.getName(), taskExecution.getId());
                    FXTaskExecutions.add(taskExecution.javafx);
                }
            }
        }
        unfinishedCallsCount.set(unfinishedCount);

        if (LOG.isDebugEnabled() && taskdebug_helper_PreviousRealAllTasksSize != real.allTasks.size())
            LOG.debug("JavaFXTaskThread periodic update:" +
                    "                                                   " +
                    "  tasks.size()==" + tasks.size() +
                    "  FXTaskExecutions.size()==" + FXTaskExecutions.size() +
                    "  futureTasks.size()==" + futureTasks.size());
        taskdebug_helper_PreviousRealAllTasksSize = real.allTasks.size();

        //update existing tasks
        for (Task task : tasks)
            task.update();
        for (FXTaskExecution st : FXTaskExecutions)
            st.update();
        for (FutureTask ft : futureTasks)
            ft.update();
    }

    public void cancel(FXTaskExecution task) {
        real.cancel(task.real);
    }



    public ReadOnlyIntegerProperty unfinishedCallsCountProperty() {
        return unfinishedCallsCount.getReadOnlyProperty();
    }

    public BooleanExpression busyProperty() {
        return busy;
    }

    public ObservableList<FXTaskExecution> getAllTasks() {
        return FXTaskExecutions;
    }

    public int getCallsCount() {
        return callsCount.get();
    }

    public ReadOnlyIntegerProperty callsCountProperty() {
        return callsCount.getReadOnlyProperty();
    }

    /**
     * ATTENTION: Dangerous function! Should only be used for debugging purposes
     */
    public void clear() {
        List<FXTaskExecution> toRemove = new ArrayList<>();

        for (FXTaskExecution fxTaskExecution : getAllTasks()) {
            fxTaskExecution.clearCallsCache();

            if (fxTaskExecution.getState() == TaskExecution.TaskState.SUCCESS ||
                    fxTaskExecution.getState() == TaskExecution.TaskState.FAILED ||
                    fxTaskExecution.getState() == TaskExecution.TaskState.CANCELLED) {
                toRemove.add(fxTaskExecution);

            }
        }

        FXTaskExecutions.removeAll(toRemove);
    }

    public class FutureTask {
        private final TaskThread.FutureTask real;

        private final LongProperty timeLeftMs = new SimpleLongProperty();

        public FutureTask(TaskThread.FutureTask real) {
            this.real = real;
        }

        public void update() {
            timeLeftMs.set(real.getTimeLeftMs());
        }

        public ObservableLongValue getTimeLeftMs() {
            return timeLeftMs;
        }
    }

    public class Task {
        private final be.iminds.ilabt.jfed.highlevel.controller.Task real;

        private final ObservableList<FutureTask> futures = FXCollections.observableArrayList();
        private final ObservableList<FXTaskExecution> currentActiveFXTaskExecutions = FXCollections.observableArrayList();

        public Task(be.iminds.ilabt.jfed.highlevel.controller.Task real) {
            this.real = real;
        }

        public void update() {
            List<FXTaskExecution> newCurrentActiveFXTaskExecutions = new ArrayList<>();
            for (TaskExecution st : real.getCurrentActiveTaskExecutionsCopy()) {
                if (st.javafx == null) continue;
                newCurrentActiveFXTaskExecutions.add(st.javafx);
            }
            syncList(newCurrentActiveFXTaskExecutions, currentActiveFXTaskExecutions); //TODO: check if this is fixed correctly


            List<FutureTask> newFutures = new ArrayList<>();
            for (TaskThread.FutureTask ft : real.getFuturesCopy()) {
                if (ft.javafx == null) continue;
                newFutures.add(ft.javafx);
            }
            syncList(newFutures, futures);
        }

        public ObservableList<FXTaskExecution> getCurrentActiveFXTaskExecutions() {
            return currentActiveFXTaskExecutions;
        }

        public ObservableList<FutureTask> getFutures() {
            return futures;
        }
    }

    public class FXTaskExecution {
        private final TaskExecution<?> real;

        private final String name;
        private final String id;
        private final ObjectProperty<TaskExecution.TaskState> state =
                new SimpleObjectProperty<>();
        private final ObservableList<FXTaskExecution> observableDependsOn = FXCollections.observableArrayList();
        private final ObservableList<FXTaskExecution> observableDependingOnThis = FXCollections.observableArrayList();
        private final ObservableList<ApiCallDetailsRef> apiCallHistory =
                FXCollections.observableArrayList();
        private final ObjectProperty<Date> runStart = new SimpleObjectProperty<>();
        private final ObjectProperty<Date> runStop = new SimpleObjectProperty<>();
        private final BooleanProperty cancelled = new SimpleBooleanProperty(false);
        private final ObjectProperty<Throwable> exception = new SimpleObjectProperty<>();

        public FXTaskExecution(TaskExecution<?> real, String name, String id) {
            this.real = real;
            this.name = name;
            this.id = id;
            update();
        }

        public void update() {
            synchronized (real) {
                state.set(real.getState());

                if (real.getApiCallHistory().size() != apiCallHistory.size()) {
                    callsCount.set(callsCount.get() + real.getApiCallHistory().size() - apiCallHistory.size());
                }
                syncList(real.getApiCallHistory(), apiCallHistory);
                runStart.set(real.getRunStart());
                runStop.set(real.getRunStop());
                cancelled.set(real.isCanceledByUser());
                exception.set(real.getException());


                List<FXTaskExecution> newDeps = new ArrayList<>();
                for (TaskExecution<?> dep : real.getDependsOn()) {
                    if (dep.javafx == null) continue;
                    newDeps.add(dep.javafx);
                }
                syncList(newDeps, observableDependsOn);

                List<FXTaskExecution> newDepOn = new ArrayList<>();
                for (TaskExecution<?> dep : real.getDependingOnThis()) {
                    if (dep.javafx == null) continue;
                    newDepOn.add(dep.javafx);
                }
                syncList(newDepOn, observableDependingOnThis);
            }
        }

        public String getName() {
            return name;
        }

        public String getId() {
            return id;
        }

        public ObjectProperty<TaskExecution.TaskState> stateProperty() {
            return state;
        }

        public TaskExecution.TaskState getState() {
            return state.get();
        }

        public ObjectProperty<Date> runStartDateProperty() {
            return runStart;
        }

        public ObjectProperty<Date> runStopDateProperty() {
            return runStop;
        }

        public ObservableList<FXTaskExecution> getObservableDependsOn() {
            return observableDependsOn;
        }

        public ObservableList<FXTaskExecution> getObservableDependingOnThis() {
            return observableDependingOnThis;
        }

        public ObservableList<SerializableApiCallDetails> getApiCallHistory() {
            return new MappedList<>(apiCallHistory, apiCallDetailsCache::getApiCallDetails);
        }

        public ObservableValue<Boolean> completedProperty() {
            return state.isEqualTo(TaskExecution.TaskState.FAILED).or(state.isEqualTo(TaskExecution.TaskState.SUCCESS));
        }

        public ObservableValue<Boolean> cancelledByUserProperty() {
            return cancelled;
        }

        public ObjectProperty<Throwable> getException() {
            return exception;
        }


        /**
         * ATTENTION: Dangerous function! Should only be used for debugging purposes
         */
        private void clearCallsCache() {
            int oldCallsCount = apiCallHistory.size();

            apiCallHistory.clear();

            callsCount.set(callsCount.get() - oldCallsCount);
        }
    }
}
