/**************************************************************************
 OmegaT - Computer Assisted Translation (CAT) tool
          with fuzzy matching, translation memory, keyword search,
          glossaries, and translation leveraging into updated projects.

 Copyright (C) 2009-2014 Alex Buloichik
               Home page: http://www.omegat.org/
               Support center: http://groups.yahoo.com/group/OmegaT/

 This file is part of OmegaT.

 OmegaT is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 OmegaT 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 for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 **************************************************************************/

package org.omegat.util;

import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Logger;

/**
 * Class for monitor directory content changes. It just looks directory every x seconds and run callback if
 * some files changed.
 * 
 * @author Alex Buloichik <alex73mail@gmail.com>
 * @author Briac Pilpre
 */
public class DirectoryMonitor extends Thread {
    /** Local logger. */
    private static final Logger LOGGER = Logger.getLogger(DirectoryMonitor.class.getName());

    private boolean stopped = false;
    protected final File dir;
    protected final Callback callback;
    protected final DirectoryCallback directoryCallback;
    private final Map<String, FileInfo> existFiles = new TreeMap<String, FileInfo>();
    protected static final long LOOKUP_PERIOD = 1000;

    /**
     * Create monitor.
     * 
     * @param dir
     *            directory to monitoring
     */
    public DirectoryMonitor(final File dir, final Callback callback) {
        if (dir == null) {
            throw new IllegalArgumentException("Dir cannot be null.");
        }
        this.dir = dir;
        this.callback = callback;
        this.directoryCallback = null;
    }

    public DirectoryMonitor(final File dir, final Callback callback, final DirectoryCallback directoryCallback) {
        if (dir == null) {
            throw new IllegalArgumentException("Dir cannot be null.");
        }
        this.dir = dir;
        this.callback = callback;
        // Can't call this(dir, callback) because fields are final.
        this.directoryCallback = directoryCallback;
    }
    
    public File getDir() {
        return dir;
    }

    /**
     * Stop directory monitoring.
     */
    public void fin() {
        stopped = true;
    }

    @Override
    public void run() {
        setName(this.getClass().getSimpleName());
        setPriority(MIN_PRIORITY);

        while (!stopped) {
            checkChanges();
            try {
                Thread.sleep(LOOKUP_PERIOD);
            } catch (InterruptedException ex) {
                stopped = true;
            }
        }
    }

    public synchronized Set<File> getExistFiles() {
        Set<File> result = new TreeSet<File>();
        for (String fn : existFiles.keySet()) {
            result.add(new File(fn));
        }
        return result;
    }

    /**
     * Process changes in directory. This method can be called before thread start for load all files from
     * directory immediately.
     */
    public synchronized void checkChanges() {
    	boolean directoryChanged = false;
        // find deleted or changed files
        for (String fn : new ArrayList<String>(existFiles.keySet())) {
            if (stopped)
                return;
            File f = new File(fn);
            if (!f.exists()) {
                // file removed
                LOGGER.finer("File '" + f + "' removed");
                existFiles.remove(fn);
                callback.fileChanged(f);
                directoryChanged = true;
            } else {
                FileInfo fi = new FileInfo(f);
                if (!fi.equals(existFiles.get(fn))) {
                    // file changed
                    LOGGER.finer("File '" + f + "' changed");
                    existFiles.put(fn, fi);
                    callback.fileChanged(f);
                    directoryChanged = true;
                }
            }
        }

        // find new files
        List<File> foundFiles = FileUtil.findFiles(dir, new FileFilter() {
            public boolean accept(File pathname) {
                return true;
            }
        });
        for (File f : foundFiles) {
            if (stopped)
                return;
            String fn = f.getPath();
            if (!existFiles.keySet().contains(fn)) {
                // file added
                LOGGER.finer("File '" + f + "' added");
                existFiles.put(fn, new FileInfo(f));
                callback.fileChanged(f);
                directoryChanged = true;
            }
        }
        
        if (directoryCallback != null && directoryChanged)
        {
        	directoryCallback.directoryChanged(dir);
        }
    }

    /**
     * Information about exist file.
     */
    protected static class FileInfo {
        public long lastModified, length;

        public FileInfo(final File file) {
            lastModified = file.lastModified();
            length = file.length();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null || !(obj instanceof FileInfo)) {
                return false;
            }
            FileInfo o = (FileInfo) obj;
            return lastModified == o.lastModified && length == o.length;
        }
    }

    /**
     * Callback for monitoring.
     */
    public interface Callback {
        /**
         * Called on any file changes - created, modified, deleted.
         */
        void fileChanged(File file);
    }
    
    public interface DirectoryCallback {
        /**
         * Called once for every directory where a file was changed - created, modified, deleted.
         */
        void directoryChanged(File file);
    }
}
