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

import be.iminds.ilabt.jfed.log.ApiCallDetails;
import be.iminds.ilabt.jfed.log.ResultListener;
import be.iminds.ilabt.jfed.log.cache.ApiCallDetailsCache;
import be.iminds.ilabt.jfed.log.cache.ApiCallDetailsRef;

import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;

/**
 *
 * */
public class TaskExecution<T extends Task> implements ResultListener {

    private static final AtomicInteger taskUniqueIdHelper = new AtomicInteger();
    protected final T task;
    protected final String name;
    protected final String id;
//    protected final List<ApiCallDetailsRef> apiCallHistory = new ArrayList<>();    //this caused ConcurrentModificationException
    //CopyOnWriteArrayList solves that in the most programmer friendly way, but is is costly (copy entire list for each add)
    //However, a copy of the entire list for each call, is not THAT costly. Compared to the call, this is fast enough.
    //The alternative would be:
    //   - wrap the ArrayList in a Collections.synchonizedList()
    //   - !!! For each getApiCallHistory() that iterates over the list, WRAP that for loop in synchonized(list) { loop }  !!! (this is too easy to forget)
    protected final List<ApiCallDetailsRef> apiCallHistory = new CopyOnWriteArrayList<>();


    private final ApiCallDetailsCache cache;
    /*
    * These are resolved dependencies, with only the actually needed onces added.
    *
    * The task can be queued and run once all these dependencies are SUCCESS.
    * The task fails if any one of these FAILED.
    *
    * The difference between initDependsOn() and initAlwaysDependsOn() has been processed into this once list:
    *    - initDependsOn() deps will depend on the original FXTaskExecution associated with this task
    *    - initAlwaysDependsOn() deps will depend on the original one only if it is not yet succesful. It it is done already, they will make a new one.
    * */
    private final List<TaskExecution<?>> dependsOn = new ArrayList<>();
    private final List<TaskExecution<?>> dependingOnThis = new ArrayList<>();
    /**
     * Collection of callbacks that should be performed when a TaskExecution has finished.
     */
    private final Collection<TaskExecutionFinishedCallback<T>> callbacks;
    protected TaskState state;
    protected Throwable exception = null;
    protected JavaFXTaskThread.FXTaskExecution javafx;
    private boolean canceledByUser = false;
    private ZonedDateTime runStart;
    private ZonedDateTime runStop;
    private Thread thread;

    public TaskExecution(T task, Collection<TaskExecutionFinishedCallback<T>> callbacks, ApiCallDetailsCache cache) {
        this.task = task;
        this.cache = cache;
        this.name = task.getName();
        this.id = "" + taskUniqueIdHelper.getAndIncrement();
        this.callbacks = callbacks;
        state = TaskState.UNSUBMITTED;
    }

    public TaskExecution(T task, ApiCallDetailsCache cache) {
        this(task, Collections.emptyList(), cache);
    }

    Thread getThread() {
        return thread;
    }

    void setThread(Thread thread) {
        this.thread = thread;
    }

    public List<TaskExecution<?>> getDependsOn() {
        return dependsOn;
    }

    public List<TaskExecution<?>> getDependingOnThis() {
        return dependingOnThis;
    }

    @Override
    public void onResult(ApiCallDetails result) {
        apiCallHistory.add(cache.registerApiCallDetails(result));
    }

    public List<ApiCallDetailsRef> getApiCallHistory() {
        return apiCallHistory;
    }

    public boolean isCanceledByUser() {
        return canceledByUser;
    }

    void setCanceledByUser(boolean canceledByUser) {
        this.canceledByUser = canceledByUser;
    }

    public Date getRunStart() {
        return runStart != null ? Date.from(runStart.toInstant()) : null;
    }

    public Date getRunStop() {
        return runStop != null ? Date.from(runStop.toInstant()) : null;
    }

    public TaskState getState() {
        return state;
    }

    public void setState(TaskState newState) {
        state = newState;
    }

    public T getTask() {
        return task;
    }

    public boolean isCompleted() {
        return Objects.equals(state, TaskState.FAILED) || Objects.equals(state, TaskState.SUCCESS) || Objects.equals(state, TaskState.CANCELLED);
    }

    //These are used to set the actual start and stop time. It cannot be done directly, because this is not called on a JavaFX thread!
    void registerRunStart() {
        assert runStart == null;
        runStart = ZonedDateTime.now();
    }

    void registerRunStop() {
        assert runStop == null;
        runStop = ZonedDateTime.now();
    }

    public String getName() {
        return name;
    }

    public String getId() {
        return id;
    }

    public Throwable getException() {
        return exception;
    }

    public String toString() {
        String deps = "";
        for (TaskExecution dep : dependsOn) {
            if (!deps.isEmpty()) deps += ", ";
            deps += "\"" + dep.name + "\" " + dep.state;
        }
        return "TaskExecution(" + id + " \"" + getName() + "\" " + state + ", deps={" + deps + "})";
    }

    public JavaFXTaskThread.FXTaskExecution getFXTaskExecution() {
        return javafx;
    }

    public Collection<TaskExecutionFinishedCallback<T>> getTaskExecutionFinishedCallbacks() {
        return callbacks;
    }

    public enum TaskState {UNSUBMITTED, BLOCKED, QUEUED, RUNNING, FAILED, FUTURE, SUCCESS, CANCELLED}
}
