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

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import uk.ac.ic.doc.kevlar.Kevlar;
import uk.ac.ic.doc.kevlar.api.programs.XMLModelLoadingException;
import uk.ac.ic.doc.kevlar.api.programs.arguments.ArgumentModelProvider;
import uk.ac.ic.doc.kevlar.api.programs.arguments.IArgumentInstance;
import uk.ac.ic.doc.kevlar.api.programs.descriptions.ProgramDescriptionProvider;
import uk.ac.ic.doc.kevlar.api.programs.returnValues.ReturnModelProvider;
import uk.ac.ic.doc.kevlar.core.framework.exceptions.InvalidProgramException;
import uk.ac.ic.doc.kevlar.core.framework.internal.IProgram;
import uk.ac.ic.doc.kevlar.core.framework.internal.IProgramInstance;
import uk.ac.ic.doc.kevlar.core.framework.internal.execute.ExecutionEngineProvider;
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.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.loaders.xmlparse.IProgramXMLFile;
import uk.ac.ic.doc.kevlar.core.framework.internal.loaders.xmlparse.ProgramXMLFileProvider;
import uk.ac.ic.doc.kevlar.core.framework.internal.programs.BuiltinProgramProvider;
import uk.ac.ic.doc.kevlar.publicInterfaces.IJVMEnvironment;
import uk.ac.ic.doc.kevlar.publicInterfaces.IInputPipe;
import uk.ac.ic.doc.kevlar.publicInterfaces.INodeID;
import uk.ac.ic.doc.kevlar.publicInterfaces.IOutputPipe;

/**
 * @author toa02
 *  
 */
class ProgramLoader implements IProgramLoader, Runnable {

    private static final String PROGRAM_SEPARATOR = ",";

    private static final String PATH_SEPARATOR = "/";

    /*
     * { if (File.separator.equals("\\")) { PATH_SEPARATOR = "\\\\"; } else {
     * PATH_SEPARATOR = File.separator; } }
     */

    //A Queue of events
    final BlockingQueue<IJarEvent> progEvents = new LinkedBlockingQueue<IJarEvent>();

    //The observers we notify
    final Set<IProgramLoaderObserver> observers;

    //The JarFile we are watching
    final Map<JarFile, Set<IProgram>> jarsWatching;

    final Set<IProgram> allPrograms;

    Set<IProgram> builtinPrograms;

    //What the PROGRAMS tag is in the manifest
    private static final String PROGRAMS = "Programs";

    //The NotifierFilter we use for the directory watcher
    private final INotifierFilter inf;

    //The directory watcher.
    private final IDirectoryWatcher watcher;

    private final ITypeLoader itl;

    public ProgramLoader(Set<File> directories, ITypeLoader itl,
            IDirectoryWatcher watcher) {

        this.itl = itl;

        // the notifier, that takes a jar event and puts it into a queue ( so we
        // don't block threads )
        this.inf = new INotifierFilter() {

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

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

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

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

        };

        this.observers = new HashSet<IProgramLoaderObserver>();
        this.watcher = watcher;
        this.jarsWatching = new TreeMap<JarFile, Set<IProgram>>(
                JarFileComparator.getInstance());
        this.allPrograms = new HashSet<IProgram>();

        for (File f : directories) {
            watcher.addDirectory(f, inf);
        }

//        ExecuteHolder.getExecutor().execute(this);
    }

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

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

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

                    addAJar(je.getJar());
                }

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

            if (progEvents.isEmpty()) {
                if (Kevlar.DEBUG) {
                    System.out.println("Program Loader: Rehash");
                }
                rehash();

                synchronized (progEvents) {
                    progEvents.notifyAll();
                }
            }
        }
    }

    private void rehash() {

        synchronized (jarsWatching) {
            synchronized (allPrograms) {

                allPrograms.clear();

                for (JarFile jf : jarsWatching.keySet()) {
                    allPrograms.addAll(jarsWatching.get(jf));
                }
            }

        }

        synchronized (observers) {
            for (final IProgramLoaderObserver obs : observers) {
                ExecuteHolder.getExecutor().execute(new Runnable() {

                    public void run() {
                        obs.programsAvailableUpdated();
                    }
                });
            }
        }

    }

    private Set<IProgram> extractProgramsFromJar(JarFile jf) {
        Set<IProgram> progs = new HashSet<IProgram>();

        try {
            String s = jf.getManifest().getMainAttributes().getValue(PROGRAMS);
            if (s == null) {
                if (Kevlar.DEBUG) {
                    System.err.println("Got a manifest with a null attribute");
                }
                return progs;
            }

            String[] progNames = s.split(PROGRAM_SEPARATOR);

            for (String cFile : progNames) {

                String fName = cFile.replaceAll("\\.", PATH_SEPARATOR) + ".xml";
                JarEntry ze = jf.getJarEntry(fName);

                if (ze != null) {
                    try {
                        IProgram newProg = createProgram(cFile, jf.getName(),
                                jf.getInputStream(ze));
                        if (newProg == null) {
                            continue;
                        }
                        progs.add(newProg);
                    } catch (IOException ioe) {
                        if (Kevlar.DEBUG) {
                            ioe.printStackTrace();
                        }
                        continue;
                    } catch ( NoSuchMethodError e ) {
                        /* for maven-based juniting */
                        continue;
                    }
                } else {
                    if (Kevlar.DEBUG) {
                        System.out.println(fName + ": wasn't in jar file "
                                + jf.getName());
                    }
                }
            }

        } catch (IOException e) {

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

            return progs;
        }

        return progs;

    }

    /**
     * Create a program from a given InputStream
     * 
     * @param className
     *            The className of this program from the jar
     * @param jarPath
     *            The path to the jar file
     * @param is
     *            The InputStream to the IProgram to create. This will be closed
     *            on return
     * @return an IProgram from the input stream argument
     *  
     */
private IProgram createProgram(final String className,
            final String jarPath, InputStream is) {
        //TODO toa02: handle a lot of errors properly
        IProgramXMLFile ipxf = ProgramXMLFileProvider.getProgramXMLFile(is);

        try {
            is.close();
        } catch (IOException e) {
            if (Kevlar.DEBUG) {
                e.printStackTrace();
            }
        }

        try {
            
            final ReturnModelProvider rmp = new ReturnModelProvider(ipxf
                    .getReturnModel());
            
            final ArgumentModelProvider amp;
            try{
	            amp = new ArgumentModelProvider(ipxf
	                    .getArgumentModel(), itl);
            }catch( ClassNotFoundException cnfe ){
                /*
                 * TODO : toa02 special case: program not loaded due to bad
                 * types
                 */
                return null;
            }
            
            final ProgramDescriptionProvider pdp = new ProgramDescriptionProvider(
                    ipxf.getDescriptionModel());

            return new IProgram() {

                public ArgumentModelProvider getArgumentModelProvider()
                        throws InvalidProgramException {
                    return amp;
                }

                public ProgramDescriptionProvider getDescription()
                        throws InvalidProgramException {
                    return pdp;
                }

                public IProgramInstance run(IArgumentInstance args,
                        IJVMEnvironment env, Map<INodeID, IInputPipe> ins,
                        Map<INodeID, IOutputPipe> outs)
                        throws InvalidProgramException {

                    return ExecutionEngineProvider.getExecutionEngine()
                            .createProgramInstance(className, jarPath, env, rmp.getReturnModel(),
                                    args, ins, outs, null);
                }

                public ReturnModelProvider getReturnModelProvider() {
                    return rmp;
                }

                public String toString() {
                    return "IProgram: "
                            + pdp.getProgramDescription().getFriendlyName();
                }

                public boolean isBuiltin() {
                    return false;
                }

                public String getCanonicalName() {
                    return className;
                }
                

            };

        } catch (XMLModelLoadingException e) {
            if( Kevlar.DEBUG ) {
                e.printStackTrace();
            }
            return null;
        }

    }
    /**
     * Add a jar file to those that we are watching ( Synchronizes )
     * 
     * @param jf
     */
    private void addAJar(JarFile jf) {
        Set<IProgram> thisJarProgs = extractProgramsFromJar(jf);

        synchronized (jarsWatching) {
            if (jarsWatching.containsKey(jf)) { throw new AssertionError(
                    "Precondition failure"); }
            jarsWatching.put(jf, thisJarProgs);
        }
    }

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

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

    /**
     * @see uk.ac.ic.doc.kevlar.core.framework.internal.loaders.IProgramLoader#getPrograms()
     */
    public Set<IProgram> getPrograms() {
        synchronized (progEvents) {
            while (!progEvents.isEmpty()) {
                try {
                    progEvents.wait();
                } catch (InterruptedException e) {
                    /* do nothing */
                }
            }
        }

        synchronized (allPrograms) {

            if( builtinPrograms == null ) {
                this.builtinPrograms = BuiltinProgramProvider.getBuiltinMethods();
            }
            
            if (Kevlar.DEBUG) {
                System.out.println("Loaders: Getting programs #"
                        + (allPrograms.size() + builtinPrograms.size()));
            }
            Set<IProgram> out = new HashSet<IProgram>(allPrograms);

            
            out.addAll(builtinPrograms);
            return out;
        }
    }

    void addObserver(IProgramLoaderObserver iplo) {

        synchronized (observers) {
            observers.add(iplo);
        }

    }

}
