/**
 * Copyright (c) 2009-2011, The HATS Consortium. All rights reserved.
 * This file is licensed under the terms of the Modified BSD License.
 */
package org.abs_models.backend.java.scheduling;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

import org.abs_models.backend.java.lib.runtime.ABSDeadlockException;
import org.abs_models.backend.java.lib.runtime.ABSException;
import org.abs_models.backend.java.lib.runtime.ABSFut;
import org.abs_models.backend.java.lib.runtime.ABSRuntime;
import org.abs_models.backend.java.lib.runtime.Logging;
import org.abs_models.backend.java.lib.runtime.Task;

public class GlobalScheduler {
    private static Logger logger = Logging.getLogger(GlobalScheduler.class.getName());
    private final ScheduleOptions options = new ScheduleOptions();
    private final GlobalSchedulingStrategy strategy;
    private final ABSRuntime runtime;
    private volatile boolean isShutdown;
    private final AtomicInteger counter = new AtomicInteger();

    /**
     * Holds a list of threads that wait for other threads to complete
     * their next step
     * protected by lock of this
     */
    private final ArrayList<SimpleLock> nextStepWaitStack = new ArrayList<>();

    public GlobalScheduler(ABSRuntime runtime, GlobalSchedulingStrategy strategy) {
        this.strategy = strategy;
        this.runtime = runtime;
    }

    private long totalNumChoices = 0;

    public void doNextScheduleStep() {
        if (isShutdown) return;
        int i = counter.incrementAndGet();
        logger.finest("==="+i+": Do next step...");
        ScheduleAction next = null;
        synchronized (this) {
            if (nextStepWaitStack.size() > 0) {
                SimpleLock l = nextStepWaitStack.remove(nextStepWaitStack.size()-1);
                logger.finest("==="+i+": Ignored step, awaking thread ");
                l.unlock();
                return;
            }
            if (options.isEmpty()) {
                List<SimpleTaskScheduler.SimpleSchedulerThread> activeThreads = runtime.getThreadManager().getAllCopyOf(
                    SimpleTaskScheduler.SimpleSchedulerThread.class);
                if (!activeThreads.isEmpty()) {
                    Set<Task<?>> suspendedTasks = new HashSet<>();
                    for (SimpleTaskScheduler.SimpleSchedulerThread st : activeThreads) {
                        Task<?> t = st.getExecutingTask().task;
                        if (t != null && !t.isFinished()) {
                            suspendedTasks.add(t);
                        }
                    }
                    // Need to filter out currentTask (that is finishing)
                    Thread tt = Thread.currentThread();
                    if (tt instanceof SimpleTaskScheduler.SimpleSchedulerThread) {
                        Task<?> currT = ((SimpleTaskScheduler.SimpleSchedulerThread)tt).getExecutingTask().task;
                        suspendedTasks.remove(currT);
                    }
                    if (!suspendedTasks.isEmpty()) {
                        ABSException ex = new ABSDeadlockException();
                        for (Task<?> t : suspendedTasks) {
                            t.setException(ex);
                        }
                        runtime.handleABSException(suspendedTasks.iterator().next(), ex);
                        return;
                    }
                }

                logger.info("No steps left. Program finished");
                logger.info("Total number of global choices: " + totalNumChoices);
                if (totalNumChoices == 0) {
                    logger.info("Program is deterministic!");
                }
                return;
            }

            totalNumChoices += options.numOptions() - 1;

            logger.finest("==="+i+" Choose next action...");
            next = strategy.choose(options);
            logger.finest("==="+i+" Action " + next + " choosen");
            options.removeOption(next);
            logger.finest("==="+i+" Executing Action " + next);


        }
        if (isShutdown) return;
        int j = counter.intValue();
        if (i != j)
            logger.warning("#### Interleaving detected "+i+" != "+j);
        next.execute();
        logger.finest("==="+i+" Action " + next + " was executed.");
    }

    public void stepTask(Task<?> task) throws InterruptedException {
        if (isShutdown) return;

        ScheduleAction a = new StepTask(task);

        synchronized (this) {
            options.addOption(a);
            doNextScheduleStep();
        }

        a.await();
    }

    public synchronized void addAction(ScheduleAction action) {
        options.addOption(action);
    }

    private synchronized void ignoreNextStep(SimpleLock l) {
        nextStepWaitStack.add(l);
    }

    static class SimpleLock {
        private boolean locked;
        synchronized void unlock() {
            locked = false;
            notifyAll();
        }

        synchronized void awaitUnlocked() {
            while (locked) {
                try {
                    logger.finest("Awaiting next step...");
                    wait();
                } catch (InterruptedException e) {
                    logger.fine("was interrupted");
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }

    }

    /**
     * Handling a get in the global scheduler is pretty tricky.
     * The following is going on here:
     * the current task does a .get on the given future.
     * 2 Cases:
     *
     * 1. Future is ready, that is easy, we just continue
     *
     * 2. Future is not ready
     *
     * this case is difficult, because we have to
     *  1. block the current thread
     *  2. schedule some other task that is ready by choosing one by the
     *     global scheduler
     *  3. when the future is resolved the blocked thread must be awaked,
     *     BUT we must ensure that only one thread is calling the
     *     doNextScheduleStep method.
     *     For this reason there is an ignoreNextStep field that
     *     can be set to ignore the next call to doNextScheduleStep
     *
     *
     *  what we do now is to create a Waker object
     *  this waker object is added to the future we are waiting for, so
     *  that this future can inform us when it is resolved.
     *  we then schedule the next task by calling runtime.doNextStep()
     *  and after that we suspend the thread to be awaked later by the
     *  future, see Waker class for further docu
     *
     * @param fut
     */
    public void handleGet(ABSFut<?> fut) {
        // note that this code does only work in the presence of global
        // scheduling,
        // otherwise it would not be thread-safe
        if (fut.isDone()) {
            return;
        }

        Waker w = new Waker(this);
        fut.addWaitingThread(w);
        runtime.doNextStep();
        logger.finest("future waiting");
        w.await();

    }

    private static class Waker implements GuardWaiter {
        boolean awaken;
        final GlobalScheduler globalScheduler;
        public Waker(GlobalScheduler scheduler) {
            globalScheduler = scheduler;
        }


        public synchronized void awake() {
            awaken = true;
            notify();
        }

        public synchronized void await() {
            while (!awaken) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    logger.fine("received interrupt exception");
                    Thread.currentThread().interrupt();
                    break;
                }

            }
            logger.finest("task awaked");
        }

        @Override
        public void checkGuard() {
            logger.finest("checking guard...");

            SimpleLock l = new SimpleLock();
            globalScheduler.ignoreNextStep(l);

            awake();

            logger.finest("await next step");

            // we are now waiting for the awaked thread to do the
            // call to doNextScheduleStep, so that there are no
            // two parallel threads running
            l.awaitUnlocked();
        }
    }

    public void shutdown() {
        isShutdown = true;
    }

}
