/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.commons.vfs2.cache;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.vfs2.FileName;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystem;
import org.apache.commons.vfs2.VfsLog;
import org.apache.commons.vfs2.util.Messages;

/**
 * This implementation caches every file as long as it is strongly reachable by
 * the java vm. As soon as the vm needs memory - every softly reachable file
 * will be discarded.
 *
 * @see SoftReference
 */
public class SoftRefFilesCache extends AbstractFilesCache
{
    private static final int TIMEOUT = 1000;

    private static final Log log = LogFactory.getLog(SoftRefFilesCache.class);

    private final ConcurrentMap<FileSystem, Map<FileName, Reference<FileObject>>> fileSystemCache =
          new ConcurrentHashMap<FileSystem, Map<FileName, Reference<FileObject>>>();
    private final Map<Reference<FileObject>, FileSystemAndNameKey> refReverseMap =
          new HashMap<Reference<FileObject>, FileSystemAndNameKey>(100);
    private final ReferenceQueue<FileObject> refQueue = new ReferenceQueue<FileObject>();

    private volatile SoftRefReleaseThread softRefReleaseThread = null; // @GuardedBy("lock")

    private final Lock lock = new ReentrantLock();


    /**
     * This thread will listen on the ReferenceQueue and remove the entry in the
     * filescache as soon as the vm removes the reference
     */
    private final class SoftRefReleaseThread extends Thread
    {
        private volatile boolean requestEnd; // used for inter-thread communication

        private SoftRefReleaseThread()
        {
            setName(SoftRefReleaseThread.class.getName());
            setDaemon(true);
        }

        @Override
        public void run()
        {
            loop: while (!requestEnd && !Thread.currentThread().isInterrupted())
            {
                try
                {
                    final Reference<?> ref = refQueue.remove(TIMEOUT);
                    if (ref == null)
                    {
                        continue;
                    }

                    lock.lock();
                    try
                    {
                        final FileSystemAndNameKey key = refReverseMap.get(ref);

                        if (key != null && removeFile(key))
                        {
                            close(key.getFileSystem());
                        }
                    }
                    finally
                    {
                        lock.unlock();
                    }
                }
                catch (final InterruptedException e)
                {
                    if (!requestEnd)
                    {
                        VfsLog.warn(getLogger(), log,
                                    Messages.getString("vfs.impl/SoftRefReleaseThread-interrupt.info"));
                    }
                    break loop;
                }
            }
        }
    }

    public SoftRefFilesCache()
    {
    }

    private void startThread()
    {
        // Double Checked Locking is allowed when volatile
        if (softRefReleaseThread != null)
        {
            return;
        }

        synchronized (lock)
        {
            if (softRefReleaseThread == null)
            {
                softRefReleaseThread = new SoftRefReleaseThread();
                softRefReleaseThread.start();
            }
        }
    }

    private void endThread()
    {
        synchronized (lock)
        {
            final SoftRefReleaseThread thread = softRefReleaseThread;
            softRefReleaseThread = null;
            if (thread != null)
            {
                thread.requestEnd = true;
                thread.interrupt();
            }
        }
    }

    @Override
    public void putFile(final FileObject fileObject)
    {
        if (log.isDebugEnabled())
        {
            log.debug("putFile: " + this.getSafeName(fileObject));
        }

        final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileObject.getFileSystem());

        final Reference<FileObject> ref = createReference(fileObject, refQueue);
        final FileSystemAndNameKey key = new FileSystemAndNameKey(fileObject.getFileSystem(), fileObject.getName());

        lock.lock();
        try
        {
            final Reference<FileObject> old = files.put(fileObject.getName(), ref);
            if (old != null)
            {
                refReverseMap.remove(old);
            }
            refReverseMap.put(ref, key);
        }
        finally
        {
            lock.unlock();
        }
    }

    private String getSafeName(final FileName fileName)
    {
        return fileName.getFriendlyURI();
    }

    private String getSafeName(final FileObject fileObject)
    {
        return this.getSafeName(fileObject.getName());
    }

    @Override
    public boolean putFileIfAbsent(final FileObject fileObject)
    {
        if (log.isDebugEnabled())
        {
            log.debug("putFile: " + this.getSafeName(fileObject));
        }

        final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileObject.getFileSystem());

        final Reference<FileObject> ref = createReference(fileObject, refQueue);
        final FileSystemAndNameKey key = new FileSystemAndNameKey(fileObject.getFileSystem(), fileObject.getName());

        lock.lock();
        try
        {
            if (files.containsKey(fileObject.getName()) && files.get(fileObject.getName()).get() != null)
            {
                return false;
            }
            final Reference<FileObject> old = files.put(fileObject.getName(), ref);
            if (old != null)
            {
                refReverseMap.remove(old);
            }
            refReverseMap.put(ref, key);
            return true;
        }
        finally
        {
            lock.unlock();
        }
    }

    protected Reference<FileObject> createReference(final FileObject file, final ReferenceQueue<FileObject> refqueue)
    {
        return new SoftReference<FileObject>(file, refqueue);
    }

    @Override
    public FileObject getFile(final FileSystem fileSystem, final FileName fileName)
    {
        final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileSystem);

        lock.lock();
        try
        {
            final Reference<FileObject> ref = files.get(fileName);
            if (ref == null)
            {
                return null;
            }

            final FileObject fo = ref.get();
            if (fo == null)
            {
                removeFile(fileSystem, fileName);
            }
            return fo;
        }
        finally
        {
            lock.unlock();
        }
    }

    @Override
    public void clear(final FileSystem fileSystem)
    {
        final Map<FileName, Reference<FileObject>> files = getOrCreateFilesystemCache(fileSystem);

        lock.lock();
        try
        {
            final Iterator<FileSystemAndNameKey> iterKeys = refReverseMap.values().iterator();
            while (iterKeys.hasNext())
            {
                final FileSystemAndNameKey key = iterKeys.next();
                if (key.getFileSystem() == fileSystem)
                {
                    iterKeys.remove();
                    files.remove(key.getFileName());
                }
            }

            if (files.size() < 1)
            {
                close(fileSystem);
            }
        }
        finally
        {
            lock.unlock();
        }
    }

    /**
     * Called while the lock is held
     * @param fileSystem The file system to close.
     */
    private void close(final FileSystem fileSystem)
    {
        if (log.isDebugEnabled())
        {
            log.debug("close fs: " + fileSystem.getRootName());
        }

        fileSystemCache.remove(fileSystem);
        if (fileSystemCache.size() < 1)
        {
            endThread();
        }
        /* This is not thread-safe as another thread might be opening the file system
        ((DefaultFileSystemManager) getContext().getFileSystemManager())
                ._closeFileSystem(filesystem);
         */
    }

    @Override
    public void close()
    {
        super.close();

        endThread();

        lock.lock();
        try
        {
            fileSystemCache.clear();

            refReverseMap.clear();
        }
        finally
        {
            lock.unlock();
        }
    }

    @Override
    public void removeFile(final FileSystem fileSystem, final FileName fileName)
    {
        if (removeFile(new FileSystemAndNameKey(fileSystem, fileName)))
        {
            close(fileSystem);
        }
    }

    private boolean removeFile(final FileSystemAndNameKey key)
    {
        if (log.isDebugEnabled())
        {
            log.debug("removeFile: " + this.getSafeName(key.getFileName()));
        }

        final Map<?, ?> files = getOrCreateFilesystemCache(key.getFileSystem());

        lock.lock();
        try
        {
            final Object ref = files.remove(key.getFileName());
            if (ref != null)
            {
                refReverseMap.remove(ref);
            }

            return files.size() < 1;
        }
        finally
        {
            lock.unlock();
        }
    }

    protected Map<FileName, Reference<FileObject>> getOrCreateFilesystemCache(final FileSystem fileSystem)
    {
        if (fileSystemCache.size() < 1)
        {
            startThread();
        }

        Map<FileName, Reference<FileObject>> files;

        do
        {
            files = fileSystemCache.get(fileSystem);
            if (files != null)
            {
                break;
            }
            files = new HashMap<FileName, Reference<FileObject>>();
        } while (fileSystemCache.putIfAbsent(fileSystem, files) == null);

        return files;
    }
}
