/*
 * Copyright (c) 2005, 2009, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.nio.ch;

import java.nio.channels.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.lang.ref.*;
import java.io.FileDescriptor;
import java.io.IOException;

abstract class FileLockTable {
    protected FileLockTable() {
    }

    /**
     * Creates and returns a file lock table for a channel that is connected to
     * the a system-wide map of all file locks for the Java virtual machine.
     */
    public static FileLockTable newSharedFileLockTable(Channel channel,
                                                       FileDescriptor fd)
        throws IOException
    {
        return new SharedFileLockTable(channel, fd);
    }

    /**
     * Adds a file lock to the table.
     *
     * @throws OverlappingFileLockException if the file lock overlaps
     *         with an existing file lock in the table
     */
    public abstract void add(FileLock fl) throws OverlappingFileLockException;

    /**
     * Remove an existing file lock from the table.
     */
    public abstract void remove(FileLock fl);

    /**
     * Removes all file locks from the table.
     *
     * @return  The list of file locks removed
     */
    public abstract List<FileLock> removeAll();

    /**
     * Replaces an existing file lock in the table.
     */
    public abstract void replace(FileLock fl1, FileLock fl2);
}


/**
 * A file lock table that is over a system-wide map of all file locks.
 */
class SharedFileLockTable extends FileLockTable {

    /**
     * A weak reference to a FileLock.
     * <p>
     * SharedFileLockTable uses a list of file lock references to avoid keeping the
     * FileLock (and FileChannel) alive.
     */
    private static class FileLockReference extends WeakReference<FileLock> {
        private FileKey fileKey;

        FileLockReference(FileLock referent,
                          ReferenceQueue<FileLock> queue,
                          FileKey key) {
            super(referent, queue);
            this.fileKey = key;
        }

        FileKey fileKey() {
            return fileKey;
        }
    }

    // The system-wide map is a ConcurrentHashMap that is keyed on the FileKey.
    // The map value is a list of file locks represented by FileLockReferences.
    // All access to the list must be synchronized on the list.
    private static ConcurrentHashMap<FileKey, List<FileLockReference>> lockMap =
        new ConcurrentHashMap<FileKey, List<FileLockReference>>();

    // reference queue for cleared refs
    private static ReferenceQueue<FileLock> queue = new ReferenceQueue<FileLock>();

    // The connection to which this table is connected
    private final Channel channel;

    // File key for the file that this channel is connected to
    private final FileKey fileKey;

    SharedFileLockTable(Channel channel, FileDescriptor fd) throws IOException {
        this.channel = channel;
        this.fileKey = FileKey.create(fd);
    }

    @Override
    public void add(FileLock fl) throws OverlappingFileLockException {
        List<FileLockReference> list = lockMap.get(fileKey);

        for (;;) {

            // The key isn't in the map so we try to create it atomically
            if (list == null) {
                list = new ArrayList<FileLockReference>(2);
                List<FileLockReference> prev;
                synchronized (list) {
                    prev = lockMap.putIfAbsent(fileKey, list);
                    if (prev == null) {
                        // we successfully created the key so we add the file lock
                        list.add(new FileLockReference(fl, queue, fileKey));
                        break;
                    }
                }
                // someone else got there first
                list = prev;
            }

            // There is already a key. It is possible that some other thread
            // is removing it so we re-fetch the value from the map. If it
            // hasn't changed then we check the list for overlapping locks
            // and add the new lock to the list.
            synchronized (list) {
                List<FileLockReference> current = lockMap.get(fileKey);
                if (list == current) {
                    checkList(list, fl.position(), fl.size());
                    list.add(new FileLockReference(fl, queue, fileKey));
                    break;
                }
                list = current;
            }

        }

        // process any stale entries pending in the reference queue
        removeStaleEntries();
    }

    private void removeKeyIfEmpty(FileKey fk, List<FileLockReference> list) {
        assert Thread.holdsLock(list);
        assert lockMap.get(fk) == list;
        if (list.isEmpty()) {
            lockMap.remove(fk);
        }
    }

    @Override
    public void remove(FileLock fl) {
        assert fl != null;

        // the lock must exist so the list of locks must be present
        List<FileLockReference> list = lockMap.get(fileKey);
        if (list == null) return;

        synchronized (list) {
            int index = 0;
            while (index < list.size()) {
                FileLockReference ref = list.get(index);
                FileLock lock = ref.get();
                if (lock == fl) {
                    assert (lock != null) && (lock.acquiredBy() == channel);
                    ref.clear();
                    list.remove(index);
                    break;
                }
                index++;
            }
        }
    }

    @Override
    public List<FileLock> removeAll() {
        List<FileLock> result = new ArrayList<FileLock>();
        List<FileLockReference> list = lockMap.get(fileKey);
        if (list != null) {
            synchronized (list) {
                int index = 0;
                while (index < list.size()) {
                    FileLockReference ref = list.get(index);
                    FileLock lock = ref.get();

                    // remove locks obtained by this channel
                    if (lock != null && lock.acquiredBy() == channel) {
                        // remove the lock from the list
                        ref.clear();
                        list.remove(index);

                        // add to result
                        result.add(lock);
                    } else {
                        index++;
                    }
                }

                // once the lock list is empty we remove it from the map
                removeKeyIfEmpty(fileKey, list);
            }
        }
        return result;
    }

    @Override
    public void replace(FileLock fromLock, FileLock toLock) {
        // the lock must exist so there must be a list
        List<FileLockReference> list = lockMap.get(fileKey);
        assert list != null;

        synchronized (list) {
            for (int index=0; index<list.size(); index++) {
                FileLockReference ref = list.get(index);
                FileLock lock = ref.get();
                if (lock == fromLock) {
                    ref.clear();
                    list.set(index, new FileLockReference(toLock, queue, fileKey));
                    break;
                }
            }
        }
    }

    // Check for overlapping file locks
    private void checkList(List<FileLockReference> list, long position, long size)
        throws OverlappingFileLockException
    {
        assert Thread.holdsLock(list);
        for (FileLockReference ref: list) {
            FileLock fl = ref.get();
            if (fl != null && fl.overlaps(position, size))
                throw new OverlappingFileLockException();
        }
    }

    // Process the reference queue
    private void removeStaleEntries() {
        FileLockReference ref;
        while ((ref = (FileLockReference)queue.poll()) != null) {
            FileKey fk = ref.fileKey();
            List<FileLockReference> list = lockMap.get(fk);
            if (list != null) {
                synchronized (list) {
                    list.remove(ref);
                    removeKeyIfEmpty(fk, list);
                }
            }
        }
    }
}
