/* Copyright (c) 2007 Timothy Wall, All Rights Reserved
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 */
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;

public class W32FileMonitor extends FileMonitor {
    
    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<File, FileInfo>();
    private final Map<HANDLE, FileInfo> handleMap = new HashMap<HANDLE, FileInfo>();
    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...
                System.err.println("Unrecognized file action '" + 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;

    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++)) {
                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();
        }
    }

    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);
        }
    }
    
    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);
        port = null;
        watcher = null;
    }
}
