/* Copyright (c) 2007 Timothy Wall, All Rights Reserved
 *
 * The contents of this file is dual-licensed under 2
 * alternative Open Source/Free licenses: LGPL 2.1 or later and
 * Apache License 2.0. (starting with JNA version 4.0.0).
 *
 * You can freely decide which license you want to apply to
 * the project.
 *
 * You may obtain a copy of the LGPL License at:
 *
 * http://www.gnu.org/licenses/licenses.html
 *
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "LGPL2.1".
 *
 * You may obtain a copy of the Apache License at:
 *
 * http://www.apache.org/licenses/
 *
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "AL2.0".
 */
package com.sun.jna.platform.win32;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import com.sun.jna.platform.FileMonitor;
import com.sun.jna.platform.win32.BaseTSD.ULONG_PTRByReference;
import com.sun.jna.platform.win32.WinBase.OVERLAPPED;
import com.sun.jna.platform.win32.WinNT.FILE_NOTIFY_INFORMATION;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;
import java.util.logging.Level;
import java.util.logging.Logger;

public class W32FileMonitor extends FileMonitor {

    private static final Logger LOG = Logger.getLogger(W32FileMonitor.class.getName());

    private static final int BUFFER_SIZE = 4096;

    private class FileInfo {
        public final File file;
        public final HANDLE handle;
        public final int notifyMask;
        public final boolean recursive;
        public final FILE_NOTIFY_INFORMATION info = new FILE_NOTIFY_INFORMATION(BUFFER_SIZE);
        public final IntByReference infoLength = new IntByReference();
        public final OVERLAPPED overlapped = new OVERLAPPED();
        public FileInfo(File f, HANDLE h, int mask, boolean recurse) {
            this.file = f;
            this.handle = h;
            this.notifyMask = mask;
            this.recursive = recurse;
        }
    }
    private Thread watcher;
    private HANDLE port;
    private final Map<File, FileInfo> fileMap = new HashMap<>();
    private final Map<HANDLE, FileInfo> handleMap = new HashMap<>();
    private boolean disposing = false;

    private void handleChanges(FileInfo finfo) throws IOException {
        Kernel32 klib = Kernel32.INSTANCE;
        FILE_NOTIFY_INFORMATION fni = finfo.info;
        // Need an explicit read, since data was filled in asynchronously
        fni.read();
        do {
            FileEvent event = null;
            File file = new File(finfo.file, fni.getFilename());
            switch(fni.Action) {
                case 0:
                    break;
                case WinNT.FILE_ACTION_MODIFIED:
                    event = new FileEvent(file, FILE_MODIFIED);
                    break;
                case WinNT.FILE_ACTION_ADDED:
                    event = new FileEvent(file, FILE_CREATED);
                    break;
                case WinNT.FILE_ACTION_REMOVED:
                    event = new FileEvent(file, FILE_DELETED);
                    break;
                case WinNT.FILE_ACTION_RENAMED_OLD_NAME:
                    event = new FileEvent(file, FILE_NAME_CHANGED_OLD);
                    break;
                case WinNT.FILE_ACTION_RENAMED_NEW_NAME:
                    event = new FileEvent(file, FILE_NAME_CHANGED_NEW);
                    break;
                default:
                    // TODO: other actions...
                    LOG.log(Level.WARNING, "Unrecognized file action ''{0}''", fni.Action);
            }

            if (event != null) {
                notify(event);
            }

            fni = fni.next();
        } while (fni != null);

        // trigger the next read
        if (!finfo.file.exists()) {
            unwatch(finfo.file);
            return;
        }

        if (!klib.ReadDirectoryChangesW(finfo.handle, finfo.info,
                finfo.info.size(), finfo.recursive, finfo.notifyMask,
                finfo.infoLength, finfo.overlapped, null)) {
            if (!disposing) {
                int err = klib.GetLastError();
                throw new IOException("ReadDirectoryChangesW failed on "
                        + finfo.file + ": '"
                        + Kernel32Util.formatMessageFromLastErrorCode(err)
                        + "' (" + err + ")");
            }
        }
    }

    private FileInfo waitForChange() {
        IntByReference rcount = new IntByReference();
        ULONG_PTRByReference rkey = new ULONG_PTRByReference();
        PointerByReference roverlap = new PointerByReference();
        if (! Kernel32.INSTANCE.GetQueuedCompletionStatus(port, rcount, rkey, roverlap, WinBase.INFINITE)) {
            return null;
        }
        synchronized (this) {
            return handleMap.get(new HANDLE(rkey.getValue().toPointer()));
        }
    }

    private int convertMask(int mask) {
        int result = 0;
        if ((mask & FILE_CREATED) != 0) {
            result |= WinNT.FILE_NOTIFY_CHANGE_CREATION;
        }
        if ((mask & FILE_DELETED) != 0) {
            result |= WinNT.FILE_NOTIFY_CHANGE_NAME;
        }
        if ((mask & FILE_MODIFIED) != 0) {
            result |= WinNT.FILE_NOTIFY_CHANGE_LAST_WRITE;
        }
        if ((mask & FILE_RENAMED) != 0) {
            result |= WinNT.FILE_NOTIFY_CHANGE_NAME;
        }
        if ((mask & FILE_SIZE_CHANGED) != 0) {
            result |= WinNT.FILE_NOTIFY_CHANGE_SIZE;
        }
        if ((mask & FILE_ACCESSED) != 0) {
            result |= WinNT.FILE_NOTIFY_CHANGE_LAST_ACCESS;
        }
        if ((mask & FILE_ATTRIBUTES_CHANGED) != 0) {
            result |= WinNT.FILE_NOTIFY_CHANGE_ATTRIBUTES;
        }
        if ((mask & FILE_SECURITY_CHANGED) != 0) {
            result |= WinNT.FILE_NOTIFY_CHANGE_SECURITY;
        }
        return result;
    }

    private static int watcherThreadID;

    @Override
    protected synchronized void watch(File file, int eventMask, boolean recursive) throws IOException {
        File dir = file;
        if (!dir.isDirectory()) {
            recursive = false;
            dir = file.getParentFile();
        }
        while (dir != null && !dir.exists()) {
            recursive = true;
            dir = dir.getParentFile();
        }
        if (dir == null) {
            throw new FileNotFoundException("No ancestor found for " + file);
        }
        Kernel32 klib = Kernel32.INSTANCE;
        int mask = WinNT.FILE_SHARE_READ
            | WinNT.FILE_SHARE_WRITE | WinNT.FILE_SHARE_DELETE;
        int flags = WinNT.FILE_FLAG_BACKUP_SEMANTICS
            | WinNT.FILE_FLAG_OVERLAPPED;
        HANDLE handle = klib.CreateFile(file.getAbsolutePath(),
                WinNT.FILE_LIST_DIRECTORY,
                mask, null, WinNT.OPEN_EXISTING,
                flags, null);
        if (WinBase.INVALID_HANDLE_VALUE.equals(handle)) {
            throw new IOException("Unable to open " + file + " ("
                                  + klib.GetLastError() + ")");
        }
        int notifyMask = convertMask(eventMask);
        FileInfo finfo = new FileInfo(file, handle, notifyMask, recursive);
        fileMap.put(file, finfo);
        handleMap.put(handle, finfo);
        // Existing port is returned
        port = klib.CreateIoCompletionPort(handle, port, handle.getPointer(), 0);
        if (WinBase.INVALID_HANDLE_VALUE.equals(port)) {
            throw new IOException("Unable to create/use I/O Completion port "
                    + "for " + file + " ("
                    + klib.GetLastError() + ")");
        }
        // TODO: use FileIOCompletionRoutine callback method instead of a
        // dedicated thread
        if (!klib.ReadDirectoryChangesW(handle, finfo.info, finfo.info.size(),
                                        recursive, notifyMask, finfo.infoLength,
                                        finfo.overlapped, null)) {
            int err = klib.GetLastError();
            throw new IOException("ReadDirectoryChangesW failed on "
                                  + finfo.file + ", handle " + handle
                                  + ": '" + Kernel32Util.formatMessageFromLastErrorCode(err)
                                  + "' (" + err + ")");
        }
        if (watcher == null) {
            watcher = new Thread("W32 File Monitor-" + (watcherThreadID++)) {
                @Override
                public void run() {
                    FileInfo finfo;
                    while (true) {
                        finfo = waitForChange();
                        if (finfo == null) {
                            synchronized (W32FileMonitor.this) {
                                if (fileMap.isEmpty()) {
                                    watcher = null;
                                    break;
                                }
                            }
                            continue;
                        }

                        try {
                            handleChanges(finfo);
                        } catch (IOException e) {
                            // TODO: how is this best handled?
                            e.printStackTrace();
                        }
                    }
                }
            };
            watcher.setDaemon(true);
            watcher.start();
        }
    }

    @Override
    protected synchronized void unwatch(File file) {
        FileInfo finfo = fileMap.remove(file);
        if (finfo != null) {
            handleMap.remove(finfo.handle);
            Kernel32 klib = Kernel32.INSTANCE;
            // bug: the watcher may still be processing this file
            klib.CloseHandle(finfo.handle); // TODO check error code if failed to close
        }
    }

    @Override
    public synchronized void dispose() {
        disposing = true;

        // unwatch any remaining files in map, allows watcher thread to exit
        int i = 0;
        for (Object[] keys = fileMap.keySet().toArray(); !fileMap.isEmpty();) {
            unwatch((File)keys[i++]);
        }

        Kernel32 klib = Kernel32.INSTANCE;
        klib.PostQueuedCompletionStatus(port, 0, null, null);
        klib.CloseHandle(port); // TODO check error code if failed to close
        port = null;
        watcher = null;
    }
}
