/*******************************************************************************
* 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.                       *
*******************************************************************************/
/*
 * Created on 20-Oct-2004 by toa02
 *
 */
package uk.ac.ic.doc.kevlar.core.framework.internal.loaders;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.jar.Attributes;
import java.util.jar.JarFile;

import uk.ac.ic.doc.kevlar.Kevlar;
import uk.ac.ic.doc.kevlar.core.framework.InterfaceClass;
import uk.ac.ic.doc.kevlar.core.framework.internal.loaders.directorywatcher.IDirectoryWatcher;
import uk.ac.ic.doc.kevlar.core.framework.internal.loaders.directorywatcher.INotifierFilter;
import uk.ac.ic.doc.kevlar.core.framework.internal.loaders.util.FrameworkClassLoader;
import uk.ac.ic.doc.kevlar.core.framework.internal.loaders.util.JarFileComparator;
import uk.ac.ic.doc.kevlar.core.framework.internal.loaders.util.jarEvents.AddJarEvent;
import uk.ac.ic.doc.kevlar.core.framework.internal.loaders.util.jarEvents.IJarEvent;
import uk.ac.ic.doc.kevlar.core.framework.internal.typecheck.TypeTree;
import uk.ac.ic.doc.kevlar.core.framework.typecheck.IInterfaceClass;
import uk.ac.ic.doc.kevlar.core.framework.typecheck.ITypeTree;
import uk.ac.ic.doc.kevlar.core.framework.typecheck.TypeTreeNode;
import uk.ac.ic.doc.kevlar.publicInterfaces.IImageType;
import uk.ac.ic.doc.kevlar.publicInterfaces.IType;

/**
 * An implementation of a TypeLoader
 * 
 * @author toa02
 *  
 */
class TypeLoader implements ITypeLoader, Runnable {

    private static final String TYPE_SEPARATOR = ",";

    private static final String TYPES = "Types";

    Set<ITypeLoaderObserver> observers;

    private final Object treeLock = new Object();

    ITypeTree tree;

    final FrameworkClassLoader fcl;

    private final INotifierFilter inf;

    final BlockingQueue<IJarEvent> typeEvents = new LinkedBlockingQueue<IJarEvent>();

    final IDirectoryWatcher watcher;

    final Set<JarFile> typeJars;
    
    String classpath;
    
    final Map<String,IInterfaceClass> nameTypeMap;

    
    
    /**
     * Constructor
     * 
     * @param fcl
     *            A framework class-loader to use.
     * @param initialDirs
     *            A Set of directories to start watching on.
     * @param watcher
     *            The Directory watcher to use.
     */
    public TypeLoader(Set<File> initialDirs, FrameworkClassLoader fcl,
            IDirectoryWatcher watcher) {
        classpath = "";
        typeJars = new TreeSet<JarFile>(JarFileComparator.getInstance());
        this.fcl = fcl;
        this.watcher = watcher;
        this.nameTypeMap = new HashMap<String,IInterfaceClass>();
        this.inf = new INotifierFilter() {

            public boolean hasInterest(Attributes ats) {
                return null != ats.getValue(TYPES);
            }

            public void notifyAdded(JarFile jf) {
                try {
                    typeEvents.put(new AddJarEvent(jf));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            public void notifyRemoved(JarFile jf) {
                // XXX: toa02 Lets not worry about that at the moment.
            }

            public void notifyUpdated(JarFile jf) {
                // XXX: toa02 Lets not worry about that at the moment.
            }

        };

        observers = new HashSet<ITypeLoaderObserver>();

        for (File cFile : initialDirs) {
            addDirectory(cFile);
        }
        
        //ExecuteHolder.getExecutor().execute(this);

    }

    /**
     * @see uk.ac.ic.doc.kevlar.core.framework.internal.loaders.ITypeLoader#getTypeTree()
     */
    public ITypeTree getTypeTree() {
        synchronized ( typeEvents ) {
            while( !typeEvents.isEmpty() ) {
                try {
                    typeEvents.wait();
                } catch (InterruptedException e) {
                    /* */
                }
            }
        }
        
        synchronized (treeLock) {
            if (tree == null) {
                rehash();
            }
            return tree;
        }
    }

    public void run() {

        while (!typeEvents.isEmpty()) {
            try {
                IJarEvent je = typeEvents.take();

                if (je instanceof AddJarEvent) {
                    if (Kevlar.DEBUG) {
                        System.out.println("TypeLoader: Add a jar");
                    }

                    addAJar(je.getJar());
                }

            } catch (InterruptedException e) {
                if (Kevlar.DEBUG) {
                    e.printStackTrace();
                }
            }

            if (typeEvents.isEmpty()) {
                if (Kevlar.DEBUG) {
                    System.out.println("TypeLoader: Rehash");
                }
                rehash();
                
                synchronized ( typeEvents ) {
                    typeEvents.notifyAll();
                }
            }
        }
    }

    /**
     * @see uk.ac.ic.doc.kevlar.core.framework.internal.loaders.ITypeLoader#addDirectory(java.io.File)
     */
    public void addDirectory(File f) {
        watcher.addDirectory(f, inf);
    }

    void addAJar(JarFile jf) {
        try {
            fcl.addURL(new File(jf.getName()).toURL());
            synchronized ( typeJars ) {
                typeJars.add(jf);
                classpath = classpath + File.pathSeparatorChar + jf.getName();
            }
        } catch (MalformedURLException e) {
            if (Kevlar.DEBUG) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Re-evaluates those directories we are watching, and
     *  
     */
    void rehash() {
        synchronized( treeLock ) {
	        makeTypeTree(loadTypes());
	
	        synchronized (observers) {
	            for (final ITypeLoaderObserver iteo : observers) {
	                ExecuteHolder.getExecutor().execute(new Runnable() {
	
	                    public void run() {
	                        iteo.typeTreeUpdated();
	                    }
	
	                });
	            }
	        }
        }
    }

    final TypeTreeNode root = new TypeTreeNode(new InterfaceClass(IType.class));
    final TypeTreeNode image = new TypeTreeNode(new InterfaceClass(IImageType.class));
    
    private void makeTypeTree(Set<IInterfaceClass> loadedTypes) {
        
        Stack<TypeTreeNode> toDo = new Stack<TypeTreeNode>();
        toDo.push(root);
        //toDo.push(image);
        Set<IInterfaceClass> used = new HashSet<IInterfaceClass>();

        while (toDo.size() != 0) {

            TypeTreeNode current = toDo.pop();
            for (IInterfaceClass ifc : loadedTypes) {

                if (new HashSet<Class>(Arrays.asList(ifc.getTypeClass()
                        .getInterfaces())).contains(current.getInterfaceClass()
                        .getTypeClass())) {
                    if (!used.add(ifc)) {
                        if (Kevlar.DEBUG) {
                            System.err.println("Multiple inheritence found: "
                                    + ifc);
                        }
                        continue;
                    }
                    TypeTreeNode ttn = new TypeTreeNode(ifc);
                    current.addSubNode(ttn);
                    toDo.push(ttn);
                }
            }
        }

        synchronized ( treeLock ) {
            tree = new TypeTree(root);    

        
	        //Maybe move this to somewhere else?
	        synchronized ( nameTypeMap ) {
	            nameTypeMap.clear();
	            
	            nameTypeMap.put( root.getInterfaceClass().getTypeClass().getCanonicalName(), root.getInterfaceClass());
                nameTypeMap.put( image.getInterfaceClass().getTypeClass().getCanonicalName(), image.getInterfaceClass());
	            
	            for( IInterfaceClass ifc : loadedTypes ) {
	                nameTypeMap.put(ifc.getTypeClass().getCanonicalName(), ifc);
	            }
	            
	        }
        
        }

    }

    private Set<IInterfaceClass> loadTypes() {

        /* we have a new jar file to go through */
        Set<IInterfaceClass> allTypes = new HashSet<IInterfaceClass>();

        synchronized (typeJars) {
            for (JarFile jf : typeJars) {
                /* grab the attributes from the manifest */
                Attributes atts;
                try {
                    atts = jf.getManifest().getMainAttributes();
                } catch (IOException e1) {
                    if (Kevlar.DEBUG) {
                        e1.printStackTrace();
                    }
                    continue;
                }

                /* grab the types from the manifest line */
                String types = (String) atts.get(new Attributes.Name(TYPES));

                /* sanity check */
                if (null == types) {
                    if (Kevlar.DEBUG) {
                        System.err.println("Type with bad manifest:" + jf);
                    }
                    continue;
                }

                /* go though each one in the list */
                for (String aType : types.split(TYPE_SEPARATOR)) {

                    Class< ? > c;
                    try {
                        c = fcl.loadClass(aType);
                    } catch (ClassNotFoundException e) {
                        if (Kevlar.DEBUG) {
                            System.err.println("Couldn't find: " + aType);
                            e.printStackTrace();
                        }
                        continue;
                    } catch ( NoClassDefFoundError ncdfe ) {
                        if (Kevlar.DEBUG) {
                            System.err.println("Couldn't find: " + aType);
                            ncdfe.printStackTrace();
                        }
                        continue;
                    }

                    /* is it an interface? */
                    if (c.isInterface() && IType.class.isAssignableFrom(c)) {
                        if (Kevlar.DEBUG) {
                            System.out.println("Found a type interface: " + c);
                        }
                        IInterfaceClass ifc = new InterfaceClass(
                                (Class< ? extends IType>) c);

                        allTypes.add(ifc);

                    } else {
                        if (Kevlar.DEBUG) {
                            System.err.println(c + "was not a valid IType");
                        }
                    }
                }//each class in the jar
            }
        }
        
        allTypes.add(image.getInterfaceClass());
        return allTypes;	
    }

    /**
     * @see uk.ac.ic.doc.kevlar.core.framework.internal.loaders.ITypeLoader#removeDirectory(java.io.File)
     */
    public void removeDirectory(File f) {
        throw new UnsupportedOperationException("TODO");
    }

    /**
     * Adds an observer to tihs class.
     * 
     * @param tlo
     *            ITypeLoaderObserver to watch stuff
     */
    void addObserver(ITypeLoaderObserver tlo) {
        synchronized (observers) {
            observers.add(tlo);
        }
    }

    /**
     * @see uk.ac.ic.doc.kevlar.core.framework.internal.loaders.ITypeLoader#getTypeFromName(java.lang.String)
     */
    public IInterfaceClass getTypeFromName(String name) {
        synchronized ( nameTypeMap ) {
            
            if( nameTypeMap.containsKey(name)) {
                return nameTypeMap.get(name);    
            }
           
            throw new NoSuchElementException(name);
            
        }
    }
    
    
    public String getClassPath() {
        synchronized ( typeJars ) {
            return classpath;
        }
    }
    
}
