/**
 * Copyright (C) 2011 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.iq80.leveldb.util;

import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;

public class Finalizer<T>
{
    public static final FinalizerMonitor IGNORE_FINALIZER_MONITOR = new FinalizerMonitor()
    {
        public void unexpectedException(Throwable throwable)
        {
        }
    };

    private final int threads;
    private final FinalizerMonitor monitor;

    private final ConcurrentHashMap<FinalizerPhantomReference<T>, Object> references = new ConcurrentHashMap<FinalizerPhantomReference<T>, Object>();
    private final ReferenceQueue<T> referenceQueue = new ReferenceQueue<T>();
    private boolean destroyed;
    private ExecutorService executor;

    public Finalizer()
    {
        this(1, IGNORE_FINALIZER_MONITOR);
    }

    public Finalizer(int threads)
    {
        this(1, IGNORE_FINALIZER_MONITOR);
    }

    public Finalizer(int threads, FinalizerMonitor monitor)
    {
        this.monitor = monitor;
        Preconditions.checkArgument(threads >= 1, "threads must be at least 1");
        this.threads = threads;
    }

    public synchronized void addCleanup(T item, Callable<?> cleanup)
    {
        Preconditions.checkNotNull(item, "item is null");
        Preconditions.checkNotNull(cleanup, "cleanup is null");
        Preconditions.checkState(!destroyed, "%s is destroyed", getClass().getName());

        if (executor == null) {
            // create executor
            ThreadFactory threadFactory = new ThreadFactoryBuilder()
                    .setNameFormat("FinalizerQueueProcessor-%d")
                    .setDaemon(true)
                    .build();
            executor = Executors.newFixedThreadPool(threads, threadFactory);

            // start queue processor jobs
            for (int i = 0; i < threads; i++) {
                executor.submit(new FinalizerQueueProcessor());
            }
        }

        // create a reference to the item so we are notified when it is garbage collected
        FinalizerPhantomReference<T> reference = new FinalizerPhantomReference<T>(item, referenceQueue, cleanup);

        // we must keep a strong reference to the reference object so we are notified when the item
        // is no longer reachable (if the reference object is garbage collected we are never notified)
        references.put(reference, Boolean.TRUE);
    }
    
    public synchronized void destroy()
    {
        destroyed = true;
        if( executor!=null ) {
            executor.shutdownNow();
        }
        for(FinalizerPhantomReference<T> r: references.keySet() ) {
            try {
                r.cleanup();
            } catch (Exception e) {
            }
        }
    }

    public interface FinalizerMonitor
    {
        void unexpectedException(Throwable throwable);
    }

    private static class FinalizerPhantomReference<T> extends PhantomReference<T>
    {
        private final AtomicBoolean cleaned = new AtomicBoolean(false);
        private final Callable<?> cleanup;

        private FinalizerPhantomReference(T referent, ReferenceQueue<? super T> queue, Callable<?> cleanup)
        {
            super(referent, queue);
            this.cleanup = cleanup;
        }

        private void cleanup() throws Exception {
            if(cleaned.compareAndSet(false, true)) {
                cleanup.call();
            }
        }
    }

    private class FinalizerQueueProcessor implements Runnable
    {
        @Override
        public void run()
        {
            while (!destroyed) {
                // get the next reference to cleanup
                FinalizerPhantomReference<T> reference;
                try {
                    reference = (FinalizerPhantomReference<T>) referenceQueue.remove();
                }
                catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                }

                // remove the reference object itself from our list of references
                references.remove(reference);

                boolean rescheduleAndReturn = false;
                try {
                    reference.cleanup();
                    rescheduleAndReturn = Thread.currentThread().isInterrupted();
                }
                catch (Throwable userException) {
                    try {
                        monitor.unexpectedException(userException);
                    }
                    catch (Exception ignored) {
                        // todo consider a broader notification
                    }

                    if (userException instanceof InterruptedException) {
                        rescheduleAndReturn = true;
                        Thread.currentThread().interrupt();
                    }
                    else if (userException instanceof Error) {
                        rescheduleAndReturn = true;
                    }
                }

                if (rescheduleAndReturn) {
                    synchronized (Finalizer.this) {
                        if (!destroyed) {
                            executor.submit(new FinalizerQueueProcessor());
                        }
                    }
                    return;
                }
            }
        }
    }
}
