/*******************************************************************************
* Copyright (c)                                                                *
*           2005   Tristan Allwood,                                            *
*           2005   Daniel Burke,                                               *
*           2005   Marc Hull,                                                  *
*           2005   Ekaterina Itskova,                                          *
*           2005   Steve Zymler                                                *
*                                                                              *
* Permission is hereby granted, free of charge, to any person obtaining a      *
* copy of this software and associated documentation files (the                *
* "Software"), to deal in the Software without restriction, including          *
* without limitation the rights to use, copy, modify, merge, publish,          *
* distribute, sublicense, and/or sell copies of the Software, and to           *
* permit persons to whom the Software is furnished to do so, subject to        *
* the following conditions:                                                    *
*                                                                              *
* The above copyright notice and this permission notice shall be included      *
* in all copies or substantial portions of the Software.                       *
*                                                                              *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS      *
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF                   *
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.       *
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY         *
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,         *
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE            *
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                       *
*******************************************************************************/
package uk.ac.ic.doc.kevlar.core.framework.internal.loaders.directorywatcher;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.JarFile;

import uk.ac.ic.doc.kevlar.Kevlar;
import uk.ac.ic.doc.kevlar.core.framework.internal.loaders.util.JarFileComparator;
import uk.ac.ic.doc.kevlar.core.framework.internal.loaders.util.JarFileFilter;

/**
 * @author tora
 */
class DirectoryWatcher implements IDirectoryWatcher {

    private static final boolean LOAD_FAIL_DEBUG = Boolean.parseBoolean(System.getProperty("loadfaildebug","false"));
    
    /**
     * Map of directories to watch with the notify filters for each one.
     */
    final Map<File, Set<INotifierFilter>> dirsToWatch;

    /**
     * Map of directories to jarfile's in each directory with their last
     * time-stamps we knew about.
     */
    final Map<File, Set<JarFile>> jarsInDirs;

    // Use this as the lock for the two items below.
    final Object jarMapLock = new Object();

    /**
     * Map of timestamps from jarfiles
     */
    final Map<JarFile, Long> timestamps;

    /**
     * Map of jarfiles to the notifiers interested in them.
     */
    final Map<JarFile, Set<INotifierFilter>> interestedWatchers;

    DirectoryWatcher() {
        dirsToWatch = new HashMap<File, Set<INotifierFilter>>();
        jarsInDirs = new HashMap<File, Set<JarFile>>();
        timestamps = new TreeMap<JarFile, Long>(JarFileComparator.getInstance());
        interestedWatchers = new TreeMap<JarFile, Set<INotifierFilter>>(
                JarFileComparator.getInstance());
    }

    /**
     * @see uk.ac.ic.doc.kevlar.core.framework.internal.loaders.directorywatcher.IDirectoryWatcher#addDirectory(java.io.File,
     *      uk.ac.ic.doc.kevlar.core.framework.internal.loaders.directorywatcher.INotifierFilter)
     */
    public void addDirectory(File f, INotifierFilter inf) {

        
        if (!f.canRead() && !f.isDirectory()) { throw new IllegalArgumentException(
                "Directory isn't valid"); }
        
        f = f.getAbsoluteFile();
        
        synchronized (dirsToWatch) {

            //if we are watching this directory already:
            if (dirsToWatch.containsKey(f)) {

                Set<INotifierFilter> nots = dirsToWatch.get(f);

                //if it so happens we are already watching this directory with
                // this watcher
                if (nots.contains(inf)) {
                    if (Kevlar.DEBUG) {
                        System.err
                                .println("Attempting to add a watcher to a directory that already has it subscribed");
                    }
                    return;

                    // add this watcher to the directory, and inform it about
                    // the jar files that it cares about.
                } else {

                    synchronized (jarsInDirs) {

                        Set<JarFile> jars = jarsInDirs.get(f);

                        for (JarFile jf : jars) {
                            try {
                                if (inf.hasInterest(jf.getManifest()
                                        .getMainAttributes())) {

                                    addInterest(jf, inf);
                                    inf.notifyAdded(jf);
                                }
                            } catch (IOException e) {
                                // TODO toa02 handle this exception
                                e.printStackTrace();
                            }
                        }

                        nots.add(inf);
                    }

                }

                // if this is a new directory entirely
            } else {
                Set<INotifierFilter> s = new HashSet<INotifierFilter>();
                s.add(inf);
                dirsToWatch.put(f, s);

                synchronized (jarsInDirs) {
                    jarsInDirs.put(f, new TreeSet<JarFile>(JarFileComparator
                            .getInstance()));
                }
            }
        }
    }

    /**
     * Register a notifier's filter in a specific jar file.
     * 
     * @param jf
     *            The jar file to add interest.
     * @param inf
     *            The INotifierFilter that has interest.
     */
    private void addInterest(JarFile jf, INotifierFilter inf) {

        synchronized (jarMapLock) {
            interestedWatchers.get(jf).add(inf);
        }
    }

    /**
     * @see uk.ac.ic.doc.kevlar.core.framework.internal.loaders.directorywatcher.IDirectoryWatcher#removeDirectory(java.io.File,
     *      uk.ac.ic.doc.kevlar.core.framework.internal.loaders.directorywatcher.INotifierFilter)
     */
    public void removeDirectory(File f, INotifierFilter inf) {
        // XXX: toa02 lets not worry about this one
    }

    /**
     * @see java.lang.Runnable#run()
     */
    public void run() {

        //TODO toa02: handle the case that a jar file has been deleted from
        // the directory when we have a record of it being there.
        synchronized (dirsToWatch) {

            for (File f : dirsToWatch.keySet()) {

                for (File aJar : f.listFiles(JarFileFilter.get())) {
                    aJar = aJar.getAbsoluteFile();
                    try {
                        JarFile jf = new JarFile(aJar);

                        synchronized (jarsInDirs) {

                            Set<JarFile> knownJars = jarsInDirs.get(f);

                            synchronized (jarMapLock) {

                                if (knownJars.contains(jf)) {
                                    // We already have an entry for this
                                    // jarfile

                                    Long thisJarLaskKnownTimestamp = timestamps
                                            .get(jf);

                                    Long thisJarTimestampNow = Long
                                            .valueOf(aJar.lastModified());

                                    // to be newer, now must be larger than
                                    // old
                                    int comparisonOfDates = thisJarTimestampNow
                                            .compareTo(thisJarLaskKnownTimestamp);
                                    if (comparisonOfDates != 0) {
                                        // this is a new jar file (well it
                                        // has a different time-stamp
                                        // anyways!)
                                        // check to see ppl are still
                                        // interested, if they are notify
                                        // updated, else,
                                        // notify deleted.
                                        Set<INotifierFilter> loseObservers = new HashSet<INotifierFilter>();
                                        Set<INotifierFilter> oldObservers = interestedWatchers
                                                .get(jf);

                                        //ok check those that where
                                        // registered, and see if they still
                                        // are.
                                        for (INotifierFilter inf : oldObservers) {
                                            if (inf.hasInterest(jf
                                                    .getManifest()
                                                    .getMainAttributes())) {
                                                // we still have an
                                                // interest.
                                                inf.notifyUpdated(jf);
                                            } else {
                                                inf.notifyRemoved(jf);
                                                loseObservers.add(inf);
                                            }
                                        }

                                        oldObservers.removeAll(loseObservers);

                                        Set<INotifierFilter> thisDirObs = dirsToWatch
                                                .get(f);

                                        Set<INotifierFilter> possibleNew = new HashSet<INotifierFilter>(
                                                thisDirObs);

                                        possibleNew.removeAll(oldObservers);
                                        possibleNew.removeAll(loseObservers);
                                        // now check all those that
                                        // registered on this directory,
                                        // that weren't interested before
                                        // (they may be now!)
                                        for (INotifierFilter inf : possibleNew) {
                                            if (inf.hasInterest(jf
                                                    .getManifest()
                                                    .getMainAttributes())) {
                                                inf.notifyAdded(jf);
                                                oldObservers.add(inf);
                                            }
                                        }

                                    } else if (comparisonOfDates == 0) {
                                        // this is the same jar file
                                        continue;

                                    } else {
                                        throw new AssertionError("Impossible!");
                                    }

                                } else {
                                    // This is a new jar
                                    timestamps.put(jf, Long.valueOf(aJar
                                            .lastModified()));
                                    Set<INotifierFilter> itsObs = new HashSet<INotifierFilter>();

                                    for (INotifierFilter inf : dirsToWatch
                                            .get(f)) {
                                        if (inf.hasInterest(jf.getManifest()
                                                .getMainAttributes())) {
                                            itsObs.add(inf);

                                            inf.notifyAdded(jf);
                                        }
                                    }
                                    jarsInDirs.get(f).add(jf);
                                    interestedWatchers.put(jf, itsObs);
                                }
                            }//sync jarMapLock

                        }//sync jarsInDirs

                    } catch (IOException e1) {
                        if( LOAD_FAIL_DEBUG ) {
                                System.err.println("Error reading in Jar File: " + aJar.getName());
                                System.err.println(e1);
                        }
                    }

                }

            }

        } //sync dirsToWatch

    }

    public String toString() {
        return "Directory Scanner";
    }
}
