/*
 * Copyright 2007-2008 Sun Microsystems, Inc.  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.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package sun.nio.fs;

import java.nio.file.*;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.*;
import java.io.IOException;
import sun.misc.Unsafe;

import static sun.nio.fs.UnixNativeDispatcher.*;
import static sun.nio.fs.UnixConstants.*;

/**
 * Solaris implementation of WatchService based on file events notification
 * facility.
 */

class SolarisWatchService
    extends AbstractWatchService
{
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static int addressSize = unsafe.addressSize();

    private static int dependsArch(int value32, int value64) {
        return (addressSize == 4) ? value32 : value64;
    }

    /*
     * typedef struct port_event {
     *     int             portev_events;
     *     ushort_t        portev_source;
     *     ushort_t        portev_pad;
     *     uintptr_t       portev_object;
     *     void            *portev_user;
     * } port_event_t;
     */
    private static final int SIZEOF_PORT_EVENT  = dependsArch(16, 24);
    private static final int OFFSETOF_EVENTS    = 0;
    private static final int OFFSETOF_SOURCE    = 4;
    private static final int OFFSETOF_OBJECT    = 8;

    /*
     * typedef struct file_obj {
     *     timestruc_t     fo_atime;
     *     timestruc_t     fo_mtime;
     *     timestruc_t     fo_ctime;
     *     uintptr_t       fo_pad[3];
     *     char            *fo_name;
     * } file_obj_t;
     */
    private static final int SIZEOF_FILEOBJ    = dependsArch(40, 80);
    private static final int OFFSET_FO_NAME    = dependsArch(36, 72);

    // port sources
    private static final short PORT_SOURCE_USER     = 3;
    private static final short PORT_SOURCE_FILE     = 7;

    // user-watchable events
    private static final int FILE_MODIFIED      = 0x00000002;
    private static final int FILE_ATTRIB        = 0x00000004;
    private static final int FILE_NOFOLLOW      = 0x10000000;

    // exception events
    private static final int FILE_DELETE        = 0x00000010;
    private static final int FILE_RENAME_TO     = 0x00000020;
    private static final int FILE_RENAME_FROM   = 0x00000040;
    private static final int UNMOUNTED          = 0x20000000;
    private static final int MOUNTEDOVER        = 0x40000000;

    // background thread to read change events
    private final Poller poller;

    SolarisWatchService(UnixFileSystem fs) throws IOException {
        int port = -1;
        try {
            port = portCreate();
        } catch (UnixException x) {
            throw new IOException(x.errorString());
        }

        this.poller = new Poller(fs, this, port);
        this.poller.start();
    }


    WatchKey register(Path dir,
                      WatchEvent.Kind<?>[] events,
                      WatchEvent.Modifier... modifiers)
         throws IOException
    {
        // delegate to poller
        return poller.register(dir, events, modifiers);
    }


    void implClose() throws IOException {
        // delegate to poller
        poller.close();
    }

    /**
     * WatchKey implementation
     */
    private class SolarisWatchKey extends AbstractWatchKey
        implements DirectoryNode
    {
        private final UnixPath dir;
        private final UnixFileKey fileKey;

        // pointer to native file_obj object
        private final long object;

        // events (may be changed). set to null when watch key is invalid
        private volatile Set<? extends WatchEvent.Kind<?>> events;

        // map of entries in directory; created lazily; accessed only by
        // poller thread.
        private Map<Path,EntryNode> children;

        SolarisWatchKey(SolarisWatchService watcher,
                        UnixPath dir,
                        UnixFileKey fileKey,
                        long object,
                        Set<? extends WatchEvent.Kind<?>> events)
        {
            super(watcher);
            this.dir = dir;
            this.fileKey = fileKey;
            this.object = object;
            this.events = events;
        }

        UnixPath getFileRef() {
            return dir;
        }

        UnixFileKey getFileKey() {
            return fileKey;
        }


        public long object() {
            return object;
        }

        void invalidate() {
            events = null;
        }

        Set<? extends WatchEvent.Kind<?>> events() {
            return events;
        }

        void setEvents(Set<? extends WatchEvent.Kind<?>> events) {
            this.events = events;
        }


        public boolean isValid() {
            return events != null;
        }


        public void cancel() {
            if (isValid()) {
                // delegate to poller
                poller.cancel(this);
            }
        }


        public void addChild(Path name, EntryNode node) {
            if (children == null)
                children = new HashMap<Path,EntryNode>();
            children.put(name, node);
        }


        public void removeChild(Path name) {
            children.remove(name);
        }


        public EntryNode getChild(Path name) {
            if (children != null)
                return children.get(name);
            return null;
        }
    }

    /**
     * Background thread to read from port
     */
    private class Poller extends AbstractPoller {

        // maximum number of events to read per call to port_getn
        private static final int MAX_EVENT_COUNT            = 128;

        // events that map to ENTRY_DELETE
        private static final int FILE_REMOVED =
            (FILE_DELETE|FILE_RENAME_TO|FILE_RENAME_FROM);

        // events that tell us not to re-associate the object
        private static final int FILE_EXCEPTION =
            (FILE_REMOVED|UNMOUNTED|MOUNTEDOVER);

        // address of event buffers (used to receive events with port_getn)
        private final long bufferAddress;

        private final SolarisWatchService watcher;

        // the I/O port
        private final int port;

        // maps file key (dev/inode) to WatchKey
        private final Map<UnixFileKey,SolarisWatchKey> fileKey2WatchKey;

        // maps file_obj object to Node
        private final Map<Long,Node> object2Node;

        /**
         * Create a new instance
         */
        Poller(UnixFileSystem fs, SolarisWatchService watcher, int port) {
            this.watcher = watcher;
            this.port = port;
            this.bufferAddress =
                unsafe.allocateMemory(SIZEOF_PORT_EVENT * MAX_EVENT_COUNT);
            this.fileKey2WatchKey = new HashMap<UnixFileKey,SolarisWatchKey>();
            this.object2Node = new HashMap<Long,Node>();
        }


        void wakeup() throws IOException {
            // write to port to wakeup polling thread
            try {
                portSend(port, 0);
            } catch (UnixException x) {
                throw new IOException(x.errorString());
            }
        }


        Object implRegister(Path obj,
                            Set<? extends WatchEvent.Kind<?>> events,
                            WatchEvent.Modifier... modifiers)
        {
            // no modifiers supported at this time
            if (modifiers.length > 0) {
                if (modifiers[0] == null)
                    return new NullPointerException();
                return new UnsupportedOperationException("Modifier not supported");
            }

            UnixPath dir = (UnixPath)obj;

            // check file is directory
            UnixFileAttributes attrs = null;
            try {
                attrs = UnixFileAttributes.get(dir, true);
            } catch (UnixException x) {
                return x.asIOException(dir);
            }
            if (!attrs.isDirectory()) {
                return new NotDirectoryException(dir.getPathForExecptionMessage());
            }

            // return existing watch key after updating events if already
            // registered
            UnixFileKey fileKey = attrs.fileKey();
            SolarisWatchKey watchKey = fileKey2WatchKey.get(fileKey);
            if (watchKey != null) {
                updateEvents(watchKey, events);
                return watchKey;
            }

            // register directory
            long object = 0L;
            try {
                object = registerImpl(dir, (FILE_MODIFIED | FILE_ATTRIB));
            } catch (UnixException x) {
                return x.asIOException(dir);
            }

            // create watch key and insert it into maps
            watchKey = new SolarisWatchKey(watcher, dir, fileKey, object, events);
            object2Node.put(object, watchKey);
            fileKey2WatchKey.put(fileKey, watchKey);

            // register all entries in directory
            registerChildren(dir, watchKey, false);

            return watchKey;
        }

        // cancel single key

        void implCancelKey(WatchKey obj) {
           SolarisWatchKey key = (SolarisWatchKey)obj;
           if (key.isValid()) {
               fileKey2WatchKey.remove(key.getFileKey());

               // release resources for entries in directory
               if (key.children != null) {
                    for (Path name: key.children.keySet()) {
                        EntryNode node = key.children.get(name);
                        long object = node.object();
                        object2Node.remove(object);
                        releaseObject(object, true);
                    }
               }

               // release resources for directory
               long object = key.object();
               object2Node.remove(object);
               releaseObject(object, true);

               // and finally invalidate the key
               key.invalidate();
           }
        }

        // close watch service

        void implCloseAll() {
            // release all native resources
            for (Long object: object2Node.keySet()) {
                releaseObject(object, true);
            }

            // invalidate all keys
            for (UnixFileKey fileKey: fileKey2WatchKey.keySet()) {
                SolarisWatchKey watchKey = fileKey2WatchKey.get(fileKey);
                if (watchKey != null)
                    watchKey.invalidate();
            }

            // clean-up
            object2Node.clear();
            fileKey2WatchKey.clear();

            // free global resources
            unsafe.freeMemory(bufferAddress);
            UnixNativeDispatcher.close(port);
        }

        /**
         * Poller main loop. Blocks on port_getn waiting for events and then
         * processes them.
         */

        public void run() {
            try {
                for (;;) {
                    int n = portGetn(port, bufferAddress, MAX_EVENT_COUNT);
                    assert n > 0;

                    long address = bufferAddress;
                    for (int i=0; i<n; i++) {
                        boolean shutdown = processEvent(address);
                        if (shutdown)
                            return;
                        address += SIZEOF_PORT_EVENT;
                    }
                }
            } catch (UnixException x) {
                x.printStackTrace();
            }
        }

        /**
         * Process a single port_event
         *
         * Returns true if poller thread is requested to shutdown.
         */
        boolean processEvent(long address) {
            // pe->portev_source
            short source = unsafe.getShort(address + OFFSETOF_SOURCE);
            // pe->portev_object
            long object = unsafe.getAddress(address + OFFSETOF_OBJECT);
            // pe->portev_events
            int events = unsafe.getInt(address + OFFSETOF_EVENTS);

            // user event is trigger to process pending requests
            if (source != PORT_SOURCE_FILE) {
                if (source == PORT_SOURCE_USER) {
                    // process any pending requests
                    boolean shutdown = processRequests();
                    if (shutdown)
                        return true;
                }
                return false;
            }

            // lookup object to get Node
            Node node = object2Node.get(object);
            if (node == null) {
                // should not happen
                return false;
            }

            // As a workaround for 6642290 and 6636438/6636412 we don't use
            // FILE_EXCEPTION events to tell use not to register the file.
            // boolean reregister = (events & FILE_EXCEPTION) == 0;
            boolean reregister = true;

            // If node is EntryNode then event relates to entry in directory
            // If node is a SolarisWatchKey (DirectoryNode) then event relates
            // to a watched directory.
            boolean isDirectory = (node instanceof SolarisWatchKey);
            if (isDirectory) {
                processDirectoryEvents((SolarisWatchKey)node, events);
            } else {
                boolean ignore = processEntryEvents((EntryNode)node, events);
                if (ignore)
                    reregister = false;
            }

            // need to re-associate to get further events
            if (reregister) {
                try {
                    events = FILE_MODIFIED | FILE_ATTRIB;
                    if (!isDirectory) events |= FILE_NOFOLLOW;
                    portAssociate(port,
                                  PORT_SOURCE_FILE,
                                  object,
                                  events);
                } catch (UnixException x) {
                    // unable to re-register
                    reregister = false;
                }
            }

            // object is not re-registered so release resources. If
            // object is a watched directory then signal key
            if (!reregister) {
                // release resources
                object2Node.remove(object);
                releaseObject(object, false);

                // if watch key then signal it
                if (isDirectory) {
                    SolarisWatchKey key = (SolarisWatchKey)node;
                    fileKey2WatchKey.remove( key.getFileKey() );
                    key.invalidate();
                    key.signal();
                } else {
                    // if entry then remove it from parent
                    EntryNode entry = (EntryNode)node;
                    SolarisWatchKey key = (SolarisWatchKey)entry.parent();
                    key.removeChild(entry.name());
                }
            }

            return false;
        }

        /**
         * Process directory events. If directory is modified then re-scan
         * directory to register any new entries
         */
        void processDirectoryEvents(SolarisWatchKey key, int mask) {
            if ((mask & (FILE_MODIFIED | FILE_ATTRIB)) != 0) {
                registerChildren(key.getFileRef(), key,
                    key.events().contains(StandardWatchEventKind.ENTRY_CREATE));
            }
        }

        /**
         * Process events for entries in registered directories. Returns {@code
         * true} if events are ignored because the watch key has been cancelled.
         */
        boolean processEntryEvents(EntryNode node, int mask) {
            SolarisWatchKey key = (SolarisWatchKey)node.parent();
            Set<? extends WatchEvent.Kind<?>> events = key.events();
            if (events == null) {
                // key has been cancelled so ignore event
                return true;
            }

            // entry modified
            if (((mask & (FILE_MODIFIED | FILE_ATTRIB)) != 0) &&
                events.contains(StandardWatchEventKind.ENTRY_MODIFY))
            {
                key.signalEvent(StandardWatchEventKind.ENTRY_MODIFY, node.name());
            }

            // entry removed
            if (((mask & (FILE_REMOVED)) != 0) &&
                events.contains(StandardWatchEventKind.ENTRY_DELETE))
            {
                // Due to 6636438/6636412 we may get a remove event for cases
                // where a rmdir/unlink/rename is attempted but fails. Until
                // this issue is resolved we re-lstat the file to check if it
                // exists. If it exists then we ignore the event. To keep the
                // workaround simple we don't check the st_ino so it isn't
                // effective when the file is replaced.
                boolean removed = true;
                try {
                    UnixFileAttributes
                        .get(key.getFileRef().resolve(node.name()), false);
                    removed = false;
                } catch (UnixException x) { }

                if (removed)
                    key.signalEvent(StandardWatchEventKind.ENTRY_DELETE, node.name());
            }
            return false;
        }

        /**
         * Registers all entries in the given directory
         *
         * The {@code sendEvents} parameter indicates if ENTRY_CREATE events
         * should be queued when new entries are found. When initially
         * registering a directory then will always be false. When re-scanning
         * a directory then it depends on if the event is enabled or not.
         */
        void registerChildren(UnixPath dir,
                              SolarisWatchKey parent,
                              boolean sendEvents)
        {
            // if the ENTRY_MODIFY event is not enabled then we don't need
            // modification events for entries in the directory
            int events = FILE_NOFOLLOW;
            if (parent.events().contains(StandardWatchEventKind.ENTRY_MODIFY))
                events |= (FILE_MODIFIED | FILE_ATTRIB);

            DirectoryStream<Path> stream = null;
            try {
                stream = dir.newDirectoryStream();
            } catch (IOException x) {
                // nothing we can do
                return;
            }
            try {
                for (Path entry: stream) {
                    Path name = entry.getName();

                    // skip entry if already registered
                    if (parent.getChild(name) != null)
                        continue;

                    // send ENTRY_CREATE if enabled
                    if (sendEvents) {
                        parent.signalEvent(StandardWatchEventKind.ENTRY_CREATE, name);
                    }

                    // register it
                    long object = 0L;
                    try {
                        object = registerImpl((UnixPath)entry, events);
                    } catch (UnixException x) {
                        // can't register so ignore for now.
                        continue;
                    }

                    // create node
                    EntryNode node = new EntryNode(object, entry.getName(), parent);
                    // tell the parent about it
                    parent.addChild(entry.getName(), node);
                    object2Node.put(object, node);
                }
            } catch (ConcurrentModificationException x) {
                // error during iteration which we ignore for now
            } finally {
                try {
                    stream.close();
                } catch (IOException x) { }
            }
        }

        /**
         * Update watch key's events. Where the ENTRY_MODIFY changes then we
         * need to update the events of registered children.
         */
        void updateEvents(SolarisWatchKey key, Set<? extends WatchEvent.Kind<?>> events) {
            // update events, rembering if ENTRY_MODIFY was previously
            // enabled or disabled.
            boolean wasModifyEnabled = key.events()
                .contains(StandardWatchEventKind.ENTRY_MODIFY);
            key.setEvents(events);

            // check if ENTRY_MODIFY has changed
            boolean isModifyEnabled = events
                .contains(StandardWatchEventKind.ENTRY_MODIFY);
            if (wasModifyEnabled == isModifyEnabled) {
                return;
            }

            // if changed then update events of children
            if (key.children != null) {
                int ev = FILE_NOFOLLOW;
                if (isModifyEnabled)
                    ev |= (FILE_MODIFIED | FILE_ATTRIB);

                for (Path name: key.children.keySet()) {
                    EntryNode node = key.children.get(name);
                    long object = node.object();

                    try {
                        portAssociate(port,
                                      PORT_SOURCE_FILE,
                                      object,
                                      ev);
                    } catch (UnixException x) {
                        // nothing we can do.
                    }
                }
            }
        }

        /**
         * Calls port_associate to register the given path.
         * Returns pointer to fileobj structure that is allocated for
         * the registration.
         */
        long registerImpl(UnixPath dir, int events)
            throws UnixException
        {
            // allocate memory for the path (file_obj->fo_name field)
            byte[] path = dir.getByteArrayForSysCalls();
            int len = path.length;
            long name = unsafe.allocateMemory(len+1);
            unsafe.copyMemory(path, Unsafe.ARRAY_BYTE_BASE_OFFSET, null,
                name, (long)len);
            unsafe.putByte(name + len, (byte)0);

            // allocate memory for filedatanode structure - this is the object
            // to port_associate
            long object = unsafe.allocateMemory(SIZEOF_FILEOBJ);
            unsafe.setMemory(null, object, SIZEOF_FILEOBJ, (byte)0);
            unsafe.putAddress(object + OFFSET_FO_NAME, name);

            // associate the object with the port
            try {
                portAssociate(port,
                              PORT_SOURCE_FILE,
                              object,
                              events);
            } catch (UnixException x) {
                // debugging
                if (x.errno() == EAGAIN) {
                    System.err.println("The maximum number of objects associated "+
                        "with the port has been reached");
                }

                unsafe.freeMemory(name);
                unsafe.freeMemory(object);
                throw x;
            }
            return object;
        }

        /**
         * Frees all resources for an file_obj object; optionally remove
         * association from port
         */
        void releaseObject(long object, boolean dissociate) {
            // remove association
            if (dissociate) {
                try {
                    portDissociate(port, PORT_SOURCE_FILE, object);
                } catch (UnixException x) {
                    // ignore
                }
            }

            // free native memory
            long name = unsafe.getAddress(object + OFFSET_FO_NAME);
            unsafe.freeMemory(name);
            unsafe.freeMemory(object);
        }
    }

    /**
     * A node with native (file_obj) resources
     */
    private static interface Node {
        long object();
    }

    /**
     * A directory node with a map of the entries in the directory
     */
    private static interface DirectoryNode extends Node {
        void addChild(Path name, EntryNode node);
        void removeChild(Path name);
        EntryNode getChild(Path name);
    }

    /**
     * An implementation of a node that is an entry in a directory.
     */
    private static class EntryNode implements Node {
        private final long object;
        private final Path name;
        private final DirectoryNode parent;

        EntryNode(long object, Path name, DirectoryNode parent) {
            this.object = object;
            this.name = name;
            this.parent = parent;
        }


        public long object() {
            return object;
        }

        Path name() {
            return name;
        }

        DirectoryNode parent() {
            return parent;
        }
    }

    // -- native methods --

    private static native void init();

    private static native int portCreate() throws UnixException;

    private static native void portAssociate(int port, int source, long object, int events)
        throws UnixException;

    private static native void portDissociate(int port, int source, long object)
        throws UnixException;

    private static native void portSend(int port, int events)
        throws UnixException;

    private static native int portGetn(int port, long address, int max)
        throws UnixException;

    static {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                System.loadLibrary("nio");
                return null;
        }});
        init();
    }
}
