/*
 * Written by Dawid Kurzyniec and released to the public domain, as explained
 * at http://creativecommons.org/licenses/publicdomain
 */

package edu.emory.mathcs.util.gc;

import java.lang.ref.*;
import edu.emory.mathcs.backport.java.util.concurrent.*;
import java.util.*;
import edu.emory.mathcs.backport.java.util.concurrent.helpers.*;
import edu.emory.mathcs.util.concurrent.*;
import java.util.logging.*;

/**
 * Allows grouping together finalizers of similar object types, so that
 * they could e.g. be forcibly and collectively finalized upon VM exit.
 *
 * @author Dawid Kurzyniec
 * @version 1.0
 */
public class FinalizationGroup {

    private static final Logger logger =
        Logger.getLogger("edu.emory.mathcs.util.gc.FinalizationGroup");

    final FinalizationEngine engine;
    final String logID;
    final Map finalizers = new HashMap();
    boolean enabled;
    boolean completed;
    int pendingCount = 0;
    final List completionCallbacks = new ArrayList();

    FinalizationGroup(FinalizationEngine engine, String name) {
        this.engine = engine;
        this.logID = "Finalization group " +
            (name != null ? name : "" + System.identityHashCode(this)) + ": ";
        this.enabled = true;
    }

    // PRE: lock owned
    private synchronized boolean checkCompleted() {
        if (!completed && !enabled &&  finalizers.isEmpty()) {
            completed = true;
            this.notifyAll();
            if (logger.isLoggable(Level.FINE)) {
                logger.fine(logID + "completed");
            }
            return true;
        }
        else {
            return false;
        }
    }

    private void runCompletionCallbacks() {
        for (Iterator itr = completionCallbacks.iterator(); itr.hasNext();) {
            Callback cb = (Callback)itr.next();
            cb.completed(null);
        }
    }

    public void disable() {
        boolean completedNow = false;
        synchronized (this) {
            this.enabled = false;
            completedNow = checkCompleted();
        }
        if (completedNow) runCompletionCallbacks();
    }

    public FinalizationFuture registerFinalizer(Object referrent, Finalizer finalizer) {
        return registerFinalizer(referrent, finalizer, false);
    }

    public synchronized FinalizationFuture registerFinalizer(
        Object referrent, Finalizer finalizer, boolean phantom)
    {
        if (finalizer == null) throw new NullPointerException();
        if (!enabled) throw new IllegalStateException("Group is disabled");
        FinalizableRef ref = phantom
            ? (FinalizableRef)new PhantomFinalizableRef(referrent, this)
            : (FinalizableRef)new WeakFinalizableRef(referrent, this);
        Registration reg = new Registration(ref, finalizer);
        finalizers.put(ref, reg);
        if (pendingCount++ == 0) engine.groupActivated(this);
        if (logger.isLoggable(Level.FINE)) {
            logFinalizer(Level.FINE, finalizer, "registered (" +
                         (phantom ? "phantom" : "weak" + ")"));
        }
        return reg;
    }

    private void logFinalizer(Level level, Finalizer finalizer, String msg) {
        logger.log(level, logID + " finalizer \"" + finalizer + "\": " + msg);
    }

    public synchronized boolean addCompletionCallback(Callback cb) {
        if (completed) return false;
        completionCallbacks.add(cb);
        return true;
    }

    public synchronized boolean areAllDone() {
        return completed;
    }

    public synchronized void awaitAllDone() throws InterruptedException {
        while (!completed) wait();
    }

    public synchronized void awaitAllDone(long timeout, TimeUnit unit)
        throws InterruptedException, TimeoutException
    {
        if (completed) return;

        long nanos = unit.toNanos(timeout);
        long deadline = Utils.nanoTime() + nanos;
        while (nanos > 0) {
            TimeUnit.NANOSECONDS.timedWait(this, nanos);
            if (completed) return;
            nanos = deadline - Utils.nanoTime();
        }
        throw new TimeoutException();
    }

    boolean finalizeRef(FinalizableRef ref, boolean explicit) {
        final Registration reg;
        Finalizer finalizer;
        synchronized (this) {
            reg = (Registration)finalizers.get(ref);
            if (reg == null) return false;
            ref.clear();
            switch (reg.state) {
                case Registration.STATE_PENDING:
                    reg.setRunning();
                    finalizer = reg.finalizer;
                    break;
                case Registration.STATE_RUNNING:
                case Registration.STATE_ASYNC:
                    return false;
                case Registration.STATE_CANCELLED:
                case Registration.STATE_FINALIZED:
                default:
                    return false;
            }
        }

        FinalizationStatus status;
        try {
            status = finalizer.finalizeObject(explicit);
        }
        catch (Exception e) {
            setFinalized(reg, null, e);
            return true;
        }
        if (status == null) {
            // synchronous finalized w/o result
            setFinalized(reg, null, null);
        }
        else {
            boolean async = status.addCompletionCallback(new Callback() {
                public void completed(Object result) { setFinalized(reg, result, null); }
                public void failed(Throwable cause) { setFinalized(reg, null, cause); }
            });
            if (async) {
                setAsync(reg, status);
            }
            else {
                setFinalized(reg, status.result, status.exception);
            }
        }
        return true;
    }

    // todo: check the efficiency
    public boolean finalizeAll() {
        boolean result = false;
        FinalizableRef[] refs;
        synchronized (this) {
            if (enabled) throw new IllegalStateException("Group is not disabled");
            refs = (FinalizableRef[])
                finalizers.keySet().toArray(new FinalizableRef[finalizers.size()]);
        }
        for (int i=0; i<refs.length; i++) {
            result |= finalizeRef(refs[i], true);
        }
        return result;
    }

    // todo: check the efficiency
    public boolean cancelAll(boolean mayInterruptIfRunning) {
        boolean result = false;
        FinalizableRef[] refs;
        synchronized (this) {
            refs = (FinalizableRef[])
                finalizers.keySet().toArray(new FinalizableRef[finalizers.size()]);
        }
        for (int i=0; i<refs.length; i++) {
            result |= cancelRef(refs[i], mayInterruptIfRunning);
        }
        return result;
    }

    void setAsync(Registration reg, FinalizationStatus status) {
        boolean cancelled = false;
        synchronized (this) {
            switch (reg.state) {
                case Registration.STATE_PENDING:
                    assert false;
                    break;
                case Registration.STATE_RUNNING:
                    reg.setAsync(status);
                    break;
                case Registration.STATE_ASYNC:
                    assert false;
                    break;
                case Registration.STATE_CANCELLED:
                    cancelled = true;
                    break;
                case Registration.STATE_FINALIZED:
                    break;
            }
        }
        if (cancelled) status.cancel(true);
    }

    void setFinalized(Registration reg, Object result, Throwable exception) {
        boolean finalized = false;
        synchronized (this) {
            switch (reg.state) {
                case Registration.STATE_PENDING:
                    assert false;
                    return;
                case Registration.STATE_RUNNING:
                case Registration.STATE_ASYNC:
                    Registration curr = (Registration)finalizers.remove(reg.ref);
                    assert (curr == reg);
                    reg.setFinalized(result, exception);
                    finalized = true;
                    break;
                case Registration.STATE_CANCELLED:
                case Registration.STATE_FINALIZED:
                    return;
            }
        }
        if (finalized) {
            reg.fireCallbacks(exception);
            if (checkCompleted()) runCompletionCallbacks();
        }
    }

//    void setFinalized(List regs, List results, List exceptions) {
//        // etc.
//    }

    boolean cancelRef(FinalizableRef ref, boolean mayInterruptIfRunning) {
        Registration reg;
        boolean cancelled = false;
        FinalizationStatus asyncStatus = null;
        synchronized (this) {
            reg = (Registration)finalizers.get(ref);
            if (reg == null) return false;
            ref.clear();
            switch (reg.state) {
                case Registration.STATE_PENDING:
                    finalizers.remove(ref);
                    reg.setCancelled();
                    cancelled = true;
                    if (--pendingCount == 0) {
                        engine.groupDeactivated(this);
                    }
                    break;
                case Registration.STATE_RUNNING:
                    finalizers.remove(ref);
                    reg.setCancelled();
                    cancelled = true;
                    break;
                case Registration.STATE_ASYNC:
                    finalizers.remove(ref);
                    reg.setCancelled();
                    asyncStatus = reg.asyncStatus;
                    cancelled = true;
                    break;
                case Registration.STATE_CANCELLED:
                case Registration.STATE_FINALIZED:
                    assert false;
                    break;
            }
        }
        if (asyncStatus != null) asyncStatus.cancel(mayInterruptIfRunning);
        if (cancelled) {
            reg.fireCallbacks(new CancellationException());
            if (checkCompleted()) runCompletionCallbacks();
        }
        return cancelled;
    }

    static interface FinalizableRef extends ReferenceCleaner.CleanableReference {
        boolean cancel(boolean mayInterruptIfRunning);
        boolean finalizeNow();
        void clear();
        boolean enqueue();
        boolean isEnqueued();
        FinalizationGroup getGroup();
    }

    static class WeakFinalizableRef extends WeakReference implements FinalizableRef {
        final FinalizationGroup group;
        WeakFinalizableRef(Object referrent, FinalizationGroup group) {
            super(referrent, group.engine.getQueue());
            this.group = group;
        }
        public void cleanReference() {
            group.finalizeRef(this, false);
        }
        public boolean cancel(boolean mayInterruptIfRunning) {
            return group.cancelRef(this, mayInterruptIfRunning);
        }
        public boolean finalizeNow() {
            return group.finalizeRef(this, true);
        }
        public FinalizationGroup getGroup() { return group; }
    }

    static class PhantomFinalizableRef extends WeakReference implements FinalizableRef {
        final FinalizationGroup group;
        PhantomFinalizableRef(Object referrent, FinalizationGroup group) {
            super(referrent, group.engine.getQueue());
            this.group = group;
        }
        public void cleanReference() {
            group.finalizeRef(this, false);
        }
        public boolean cancel(boolean mayInterruptIfRunning) {
            return group.cancelRef(this, mayInterruptIfRunning);
        }
        public boolean finalizeNow() {
            return group.finalizeRef(this, true);
        }
        public FinalizationGroup getGroup() { return group; }
    }

    static class Registration implements FinalizationFuture {

        final static int STATE_PENDING   = 0;
        final static int STATE_DEQUEUED  = 1;
        final static int STATE_RUNNING   = 2;
        final static int STATE_ASYNC     = 3;
        final static int STATE_CANCELLED = 4;
        final static int STATE_FINALIZED = 5;

        int state;

        final Finalizer finalizer;
        volatile FinalizableRef ref;
        final List callbacks = new ArrayList();

        FinalizationStatus asyncStatus;
        Object result;
        Throwable exception;

        Registration(FinalizableRef ref, Finalizer finalizer) {
            this.ref = ref;
            this.finalizer = finalizer;
            this.state = STATE_PENDING;
        }

        public synchronized boolean isPending() {
            return state == STATE_PENDING;
        }
        public synchronized boolean isRunning() {
            return state == STATE_RUNNING;
        }
        public boolean cancel(boolean mayInterruptIfRunning) {
            FinalizableRef ref = this.ref;
            return ref != null ? ref.cancel(mayInterruptIfRunning) : false;
        }
        public boolean finalizeNow() {
            FinalizableRef ref = this.ref;
            return ref != null ? ref.finalizeNow() : false;
        }
        public synchronized boolean isDone() {
            return state == STATE_CANCELLED || state == STATE_FINALIZED;
        }
        public synchronized boolean isCancelled() {
            return state == STATE_CANCELLED;
        }
        public boolean isEnqueued() {
            FinalizableRef ref = this.ref;
            return ref != null ? ref.isEnqueued() : false;
        }
        public boolean enqueue() {
            FinalizableRef ref = this.ref;
            return (ref != null) ? ref.enqueue() : false;
        }
        synchronized void setFinalized(Object result, Throwable exception) {
            FinalizableRef ref = this.ref;
            this.state = STATE_FINALIZED;
            this.result = result;
            this.exception = exception;
            this.ref = null;
            this.asyncStatus = null;
            notifyAll();

            if (logger.isLoggable(Level.FINE)) {
                ref.getGroup().logFinalizer(Level.FINE, finalizer, "finalized");
            }
        }
        synchronized void setCancelled() {
            FinalizableRef ref = this.ref;
            this.state = STATE_CANCELLED;
            this.ref = null;
            this.asyncStatus = null;
            notifyAll();

            if (logger.isLoggable(Level.FINE)) {
                ref.getGroup().logFinalizer(Level.FINE, finalizer, "cancelled");
            }
        }
        synchronized void setRunning() {
            this.state = STATE_RUNNING;

            if (logger.isLoggable(Level.FINER)) {
                ref.getGroup().logFinalizer(Level.FINER, finalizer, "running");
            }
        }
        synchronized void setAsync(FinalizationStatus asyncStatus) {
            this.state = STATE_ASYNC;
            this.asyncStatus = asyncStatus;

            if (logger.isLoggable(Level.FINER)) {
                ref.getGroup().logFinalizer(Level.FINER, finalizer, "asynchronous");
            }
        }
        // pre: lock owned
        private Object doGet() throws ExecutionException {
            if (state == STATE_CANCELLED) throw new CancellationException();
            else if (exception != null) throw new ExecutionException(exception);
            else return result;
        }
        public synchronized Object get() throws InterruptedException, ExecutionException {
            while (!isDone()) wait();
            return doGet();
        }
        public synchronized Object get(long timeout, TimeUnit unit)
            throws InterruptedException, TimeoutException, ExecutionException
        {
            if (isDone()) return doGet();
            long nanos = unit.toNanos(timeout);
            long deadline = Utils.nanoTime() + nanos;
            while (true) {
                if (nanos <= 0) throw new TimeoutException();
                TimeUnit.NANOSECONDS.timedWait(this, nanos);
                if (isDone()) return doGet();
                nanos = deadline - Utils.nanoTime();
            }
        }
        public synchronized boolean addCompletionCallback(Callback cb) {
            if (isDone()) return false;
            callbacks.add(cb);
            return true;
        }

        // PRE: "done" set in a synchronized block
        private void fireCallbacks(Throwable exception) {
            if (exception == null) {
                for (Iterator itr = callbacks.iterator(); itr.hasNext(); ) {
                    Callback cb = (Callback) itr.next();
                    cb.completed(null);
                }
            }
            else {
                for (Iterator itr = callbacks.iterator(); itr.hasNext(); ) {
                    Callback cb = (Callback) itr.next();
                    cb.failed(exception);
                }
            }
        }
    }
}
