/* Copyright 2011 Yorba Foundation
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution. 
 */

// This callback is executed when an associated BackgroundJob completes.  It is called from within
// the Gtk event loop, *not* the background thread's context.
public delegate void CompletionCallback(BackgroundJob job);

// This callback is executed when an associated BackgroundJob has been cancelled (via its
// Cancellable).  Note that it's *possible* the BackgroundJob performed some or all of its work
// prior to executing this delegate.
public delegate void CancellationCallback(BackgroundJob job);

// This callback is executed by the BackgroundJob when a unit of work is completed, but not the
// entire job.  It is called from within the Gtk event loop, *not* the background thread's
// context.
//
// Note that there does not seem to be any guarantees of order in the Idle queue documentation,
// and this it's possible (and, depending on assigned priorities, likely) that notifications could
// arrive in different orders, and even after the CompletionCallback.  Thus, no guarantee of
// ordering is made here.
//
// NOTE: Would like Value to be nullable, but can't due to this bug:
// https://bugzilla.gnome.org/show_bug.cgi?id=607098
//
// NOTE: There will be a memory leak using NotificationCallbacks due to this bug:
// https://bugzilla.gnome.org/show_bug.cgi?id=571264
//
// NOTE: Because of these two bugs, using an abstract base class rather than Value.  When both are
// fixed (at least the second), may consider going back to Value.

public abstract class NotificationObject {
}

public abstract class InterlockedNotificationObject : NotificationObject {
    private Semaphore semaphore = new Semaphore();
    
    // Only called by BackgroundJob; no need for users or subclasses to use
    public void internal_wait_for_completion() {
        semaphore.wait();
    }
    
    // Only called by BackgroundJob; no need for users or subclasses to use
    public void internal_completed() {
        semaphore.notify();
    }
}

public delegate void NotificationCallback(BackgroundJob job, NotificationObject? user);

// This abstract class represents a unit of work that can be executed within a background thread's
// context.  If specified, the job may be cancellable (which can be checked by execute() and the
// worker thread prior to calling execute()).  The BackgroundJob may also specify a
// CompletionCallback and/or a CancellationCallback to be executed within Gtk's event loop.
// A BackgroundJob may also emit NotificationCallbacks, all of which are also executed within
// Gtk's event loop.
//
// The BackgroundJob may be constructed with a reference to its "owner".  This is not used directly
// by BackgroundJob or Worker, but merely exists to hold a reference to the Object that is receiving
// the various callbacks from BackgroundJob.  Without this, it's possible for the object creating
// BackgroundJobs to be freed before all the callbacks have been received, or even during a callback,
// which is an unstable situation.
public abstract class BackgroundJob {
    public enum JobPriority {
        HIGHEST = 100,
        HIGH = 75,
        NORMAL = 50,
        LOW = 25,
        LOWEST = 0;
        
        // Returns negative if this is higher, zero if equal, positive if this is lower
        public int compare(JobPriority other) {
            return (int) other - (int) this;
        }
        
        public static int compare_func(void *a, void *b) {
            return (int) b - (int) a;
        }
    }
    
    private class NotificationJob {
        public NotificationCallback callback;
        public BackgroundJob background_job;
        public NotificationObject? user;
        
        public NotificationJob(NotificationCallback callback, BackgroundJob background_job,
            NotificationObject? user) {
            this.callback = callback;
            this.background_job = background_job;
            this.user = user;
        }
    }
    
    private static Gee.ArrayList<NotificationJob> notify_queue = new Gee.ArrayList<NotificationJob>();
    
    private Object owner;
    private CompletionCallback callback;
    private Cancellable cancellable;
    private CancellationCallback cancellation;
    private BackgroundJob self = null;
    private AbstractSemaphore semaphore = null;
    
    // The thinking here is that there is exactly one CompletionCallback per job, and the caller
    // probably wants to know that to set off UI and other events in response.  There are several
    // (possibly hundreds or thousands) or notifications, and thus should arrive in a more
    // controlled way (to avoid locking up the UI, for example).  This has ramifications about
    // the order in which completion and notifications arrive (see above note).
    private int completion_priority = Priority.HIGH;
    private int notification_priority = Priority.DEFAULT_IDLE;
    
    public BackgroundJob(Object? owner = null, CompletionCallback? callback = null,
        Cancellable? cancellable = null, CancellationCallback? cancellation = null) {
        this.owner = owner;
        this.callback = callback;
        this.cancellable = cancellable;
        this.cancellation = cancellation;
    }
    
    public abstract void execute();
    
    public virtual JobPriority get_priority() {
        return JobPriority.NORMAL;
    }
    
    // For the CompareFunc delegate, according to JobPriority.
    public static int priority_compare_func(BackgroundJob a, BackgroundJob b) {
        return a.get_priority().compare(b.get_priority());
    }
    
    // For the Comparator delegate, according to JobPriority.
    public static int64 priority_comparator(void *a, void *b) {
        return priority_compare_func((BackgroundJob) a, (BackgroundJob) b);
    }
    
    // This method is not thread-safe.  Best to set priority before the job is enqueued.
    public void set_completion_priority(int priority) {
        completion_priority = priority;
    }
    
    // This method is not thread-safe.  Best to set priority before the job is enqueued.
    public void set_notification_priority(int priority) {
        notification_priority = priority;
    }
    
    // This method is thread-safe, however, because of race conditions between setting a semaphore
    // and another thread waiting on it, it's best to set this before enqueuing the job.
    public void set_completion_semaphore(AbstractSemaphore semaphore) {
        lock (this.semaphore) {
            this.semaphore = semaphore;
        }
    }
    
    // This method is thread-safe, but only waits if a completion semaphore has been set, otherwise
    // exits immediately.  Note that blocking for a semaphore does NOT spin the event loop, so a
    // thread relying on it to continue should not use this.
    public void wait_for_completion() {
        lock (semaphore) {
            if (semaphore != null)
                semaphore.wait();
        }
    }
    
    public Cancellable? get_cancellable() {
        return cancellable;
    }
    
    public bool is_cancelled() {
        return (cancellable != null) ? cancellable.is_cancelled() : false;
    }
    
    public void cancel() {
        if (cancellable != null)
            cancellable.cancel();
    }
    
    // This should only be called by Workers.  Beware to all who fail to heed.
    public void internal_notify_completion() {
        // notify anyone waiting
        lock (semaphore) {
            if (semaphore != null)
                semaphore.notify();
        }
        
        if (callback == null && cancellation == null)
            return;
        
        if (is_cancelled() && cancellation == null)
            return;
        
        // Because Idle doesn't maintain a ref count of the job, and it's going to be dropped by
        // the worker thread soon, need to maintain a ref until the completion callback is made
        self = this;
        
        Idle.add_full(completion_priority, on_notify_completion);
    }
    
    private bool on_notify_completion() {
        // it's still possible the caller cancelled this operation during or after the execute()
        // method was called ... since the completion work can be costly for a job that was
        // already cancelled, and the caller might've dropped all references to the job by now,
        // only notify completion in this context if not cancelled
        if (is_cancelled()) {
            if (cancellation != null)
                cancellation(this);
        } else {
            if (callback != null)
                callback(this);
        }
        
        // drop the ref so this object can be freed ... must not touch "this" after this point
        self = null;
        
        return false;
    }
    
    // This call may be executed by the child class during execute() to inform of a unit of
    // work being completed
    protected void notify(NotificationCallback callback, NotificationObject? user) {
        lock (notify_queue) {
            notify_queue.add(new NotificationJob(callback, this, user));
        }
        
        Idle.add_full(notification_priority, on_notification_ready);
        
        // If an interlocked notification, block until the main thread completes the notification
        // callback
        InterlockedNotificationObject? interlocked = user as InterlockedNotificationObject;
        if (interlocked != null)
            interlocked.internal_wait_for_completion();
    }
    
    private bool on_notification_ready() {
        // this is called once for every notification added, so there should always be something
        // waiting for us
        NotificationJob? notification_job = null;
        lock (notify_queue) {
            if (notify_queue.size > 0)
                notification_job = notify_queue.remove_at(0);
        }
        assert(notification_job != null);
        
        notification_job.callback(notification_job.background_job, notification_job.user);
        
        // Release the blocked thread waiting for this notification to complete
        InterlockedNotificationObject? interlocked = notification_job.user as InterlockedNotificationObject;
        if (interlocked != null)
            interlocked.internal_completed();
        
        return false;
    }
}

