/* Copyright 2016 Software Freedom Conservancy Inc.
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution.
 */

// Semaphores may be used to be notified when a job is completed.  This provides an alternate
// mechanism (essentially, a blocking mechanism) to the system of callbacks that BackgroundJob
// offers.  They can also be used for other job-dependent notification mechanisms.
public abstract class AbstractSemaphore {
    public enum Type {
        SERIAL,
        BROADCAST
    }
    
    protected enum NotifyAction {
        NONE,
        SIGNAL
    }
    
    protected enum WaitAction {
        SLEEP,
        READY
    }
    
    private Type type;
    private Mutex mutex = Mutex();
    private Cond monitor = Cond();
    
    public AbstractSemaphore(Type type) {
        assert(type == Type.SERIAL || type == Type.BROADCAST);
        
        this.type = type;
    }
    
    private void trigger() {
        if (type == Type.SERIAL)
            monitor.signal();
        else
            monitor.broadcast();
    }
    
    public void notify() {
        mutex.lock();
        
        NotifyAction action = do_notify();
        switch (action) {
            case NotifyAction.NONE:
                // do nothing
            break;
            
            case NotifyAction.SIGNAL:
                trigger();
            break;
            
            default:
                error("Unknown semaphore action: %s", action.to_string());
        }
        
        mutex.unlock();
    }
    
    // This method is called by notify() with the semaphore's mutex locked.
    protected abstract NotifyAction do_notify();
    
    public void wait() {
        mutex.lock();
        
        while (do_wait() == WaitAction.SLEEP)
            monitor.wait(mutex);
        
        mutex.unlock();
    }
    
    // This method is called by wait() with the semaphore's mutex locked.
    protected abstract WaitAction do_wait();
    
    // Returns true if the semaphore is reset, false otherwise.
    public bool reset() {
        mutex.lock();
        bool is_reset = do_reset();
        mutex.unlock();
        
        return is_reset;
    }
    
    // This method is called by reset() with the semaphore's mutex locked.  Returns true if reset,
    // false if not supported.
    protected virtual bool do_reset() {
        return false;
    }
}

public class Semaphore : AbstractSemaphore {
    bool passed = false;
    
    public Semaphore() {
        base (AbstractSemaphore.Type.BROADCAST);
    }
    
    protected override AbstractSemaphore.NotifyAction do_notify() {
        if (passed)
            return NotifyAction.NONE;
        
        passed = true;
        
        return NotifyAction.SIGNAL;
    }
    
    protected override AbstractSemaphore.WaitAction do_wait() {
        return passed ? WaitAction.READY : WaitAction.SLEEP;
    }
}

public class CountdownSemaphore : AbstractSemaphore {
    private int total;
    private int passed = 0;
    
    public CountdownSemaphore(int total) {
        base (AbstractSemaphore.Type.BROADCAST);
        
        this.total = total;
    }
    
    protected override AbstractSemaphore.NotifyAction do_notify() {
        if (passed >= total)
            critical("CountdownSemaphore overrun: %d/%d", passed + 1, total);
        
        return (++passed >= total) ? NotifyAction.SIGNAL : NotifyAction.NONE;
    }
    
    protected override AbstractSemaphore.WaitAction do_wait() {
        return (passed < total) ? WaitAction.SLEEP : WaitAction.READY;
    }
}

public class EventSemaphore : AbstractSemaphore {
    bool fired = false;
    
    public EventSemaphore() {
        base (AbstractSemaphore.Type.BROADCAST);
    }
    
    protected override AbstractSemaphore.NotifyAction do_notify() {
        fired = true;
        
        return NotifyAction.SIGNAL;
    }
    
    protected override AbstractSemaphore.WaitAction do_wait() {
        return fired ? WaitAction.READY : WaitAction.SLEEP;
    }
    
    protected override bool do_reset() {
        fired = false;
        
        return true;
    }
}