/*******************************************************************************
* 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
 */
package uk.ac.ic.doc.kevlar.hial;

import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import uk.ac.ic.doc.kevlar.Kevlar;
import uk.ac.ic.doc.kevlar.api.programs.arguments.ArgumentModelProvider;
import uk.ac.ic.doc.kevlar.api.programs.arguments.IArgumentVisitor;
import uk.ac.ic.doc.kevlar.api.programs.arguments.IBasicArgumentInfo;
import uk.ac.ic.doc.kevlar.api.programs.arguments.IBasicArgumentValue;
import uk.ac.ic.doc.kevlar.api.programs.arguments.IBoolArgumentInfo;
import uk.ac.ic.doc.kevlar.api.programs.arguments.IMultilineArgumentInfo;
import uk.ac.ic.doc.kevlar.api.programs.arguments.ISetArgumentInfo;
import uk.ac.ic.doc.kevlar.api.programs.arguments.ISinglelineArgumentInfo;
import uk.ac.ic.doc.kevlar.api.programs.arguments.internal.argumentInstance.BoolArgumentValue;
import uk.ac.ic.doc.kevlar.api.programs.arguments.internal.argumentInstance.SetArgumentValue;
import uk.ac.ic.doc.kevlar.api.programs.arguments.internal.argumentInstance.TextArgumentValue;
import uk.ac.ic.doc.kevlar.api.programs.arguments.internal.iomapping.NodeID;
import uk.ac.ic.doc.kevlar.core.framework.FrameworkProvider;
import uk.ac.ic.doc.kevlar.core.framework.IFramework;
import uk.ac.ic.doc.kevlar.core.framework.IFrameworkListener;
import uk.ac.ic.doc.kevlar.core.framework.IProgramIDDescription;
import uk.ac.ic.doc.kevlar.core.framework.internal.programs.Show;
import uk.ac.ic.doc.kevlar.hial.internal.programTree.ProgramCategoryNode;
import uk.ac.ic.doc.kevlar.hial.internal.programTree.ProgramNode;
import uk.ac.ic.doc.kevlar.hial.internal.programTree.ProgramTreeNode;
import uk.ac.ic.doc.kevlar.publicInterfaces.INodeID;
import uk.ac.ic.doc.kevlar.publicUtil.collections.Tuple;
import uk.ac.ic.doc.kevlar.publicUtil.dom.XMLUtil;
import uk.ac.ic.doc.kevlar.publicUtil.strings.NeedlemanScoringScheme;
import uk.ac.ic.doc.kevlar.publicUtil.strings.NeedlemanWunchStringComparison;

/**
 * @author Daniel Burke
 *
 */
class Hial implements IHial, IFrameworkListener {

	private static final String DEFAULT_CATEGORY = "default";
	private IFramework framework;
	private IHialEventListener listener;
	private Set<IHialContext> contexts = new HashSet<IHialContext>(); 
	private IProgramIDDescription show = null;
	
	public Hial(IHialEventListener listener)
	{
		this.framework = FrameworkProvider.getNewFramework();
		framework.addFrameworkListener(this);
		this.listener = listener;
		
		//Collection<IProgramSearchResult> results = 
		//    search(Arrays.asList(new String[] {"outputt", "files", "directories", "changing", "argyumints" }));
	}
	
	
	public synchronized void setGUIListener(IHialEventListener listener)
	{
	    this.listener = listener;
	}

	public synchronized Tuple<IProgramTreeNode, Set<IProgramIDDescription> > getProgramTreeRoot() {
		
		Set<IProgramIDDescription> descriptions =
			framework.getProgramRepository().getProgramDescriptions();
		
		Map<String,List<IProgramTreeNode>> mappings = new HashMap<String,List<IProgramTreeNode>>();
		
		final String PREFIX_OUTSIDE = "uk.ac.ic.doc.kevlar.core.programs.";
		final String PREFIX_INSIDE = "uk.ac.ic.doc.kevlar.core.framework.internal.programs.";
		for( IProgramIDDescription desc : descriptions ) {
		    
		    String groupName; 
		    if( desc.getCanonicalName().startsWith( PREFIX_OUTSIDE ) ) {
		        groupName = desc.getCanonicalName().substring(PREFIX_OUTSIDE.length());
		        groupName = groupName.substring(0,1).toUpperCase() + groupName.substring(1, groupName.lastIndexOf('.'));
		    } else if ( desc.getCanonicalName().startsWith( PREFIX_INSIDE )){
		        groupName = "Shell Built-in";
		    } else {
		        groupName = "Unclassified";
		    }
		    
		    
		    if( !mappings.containsKey(groupName) ) {
		        mappings.put(groupName,new ArrayList<IProgramTreeNode>());
		    }

		    mappings.get(groupName).add( new ProgramNode(desc));
		}
		
		List<IProgramTreeNode> root = new ArrayList<IProgramTreeNode>();
		
		for( String name : mappings.keySet() ) {
		    root.add(new ProgramCategoryNode(mappings.get(name),name));
		}
		
		ProgramTreeNode rootNode = 
			new ProgramCategoryNode(root);
		
		if(Kevlar.DEBUG)
		{
			StringProgramTreeVisitor v = new StringProgramTreeVisitor();
			rootNode.acceptVisitor(v);
			System.out.println("Hial's program tree representation");
			System.out.println(v.getTreeString());
		}
		
		return new Tuple<IProgramTreeNode, Set<IProgramIDDescription> >(rootNode, descriptions);
	}
	
	public synchronized void setProgramTree(IProgramTreeNode programTree)
	{
		/* Empty for now */
	}

    public synchronized IHialContext createNewContext(IHialContextListener listener) {
        HialContext context = new HialContext(framework, listener, getShow());
        contexts.add(context);
        return context;
    }

    public synchronized void removeContext(IHialContext context) {
        if(!contexts.contains(context))
        {
            throw new IllegalArgumentException("context is not a valid context in the framework");
        }
        
        synchronized(context)
        {
	        
	        ((HialContext)context).destroy();
	        contexts.remove(context);
        }
    }	
    

    public synchronized void availableProgramsUpdated()
    {
        if(listener != null)
        {
            listener.availableProgramsUpdated();
        }
    }
    
    public List<IProgramSearchResult> search(Collection<String> keywords)
    {
        List<String> lowerCaseKeywords = new ArrayList<String>(keywords);
        for(int i = 0 ; i < lowerCaseKeywords.size() ; i++)
        {
            lowerCaseKeywords.set(i, lowerCaseKeywords.get(i).toLowerCase());
        }
        keywords = lowerCaseKeywords;
        
        NeedlemanWunchStringComparison c = new NeedlemanWunchStringComparison();
        NeedlemanScoringScheme scheme = new NeedlemanScoringScheme(null, 15, -15, -10, -1);
        
        List<IProgramSearchResult> results = new ArrayList<IProgramSearchResult>();
		
        Set<IProgramIDDescription> descriptions =
			framework.getProgramRepository().getProgramDescriptions();
		
        Collection<String> programKeywords = new ArrayList<String>();
        
        for(IProgramIDDescription description : descriptions)
        {  
            Collection<String> matchingKeywords = new HashSet<String>();

            for(String userKeyword : keywords)
            {
                float bestMatch = 0;
                String matchingKeyword = null;
                
                
                programKeywords.clear();
                for(String programKeyword : description.getKeywords())
                {
                    programKeywords.add(programKeyword);  
                }
                programKeywords.add(description.getFriendlyName());
                
                for(String programKeyword : programKeywords)
                {
                    float result = ((float)c.computeSimilarity(
                            userKeyword, programKeyword.toLowerCase(), scheme)) / userKeyword.length();
                    
                    if(matchingKeyword == null || result > bestMatch)
                    {
                        matchingKeyword = programKeyword;
                        bestMatch = result;
                    }
                }
                
                if(bestMatch > 6)
                {
                    matchingKeywords.add(matchingKeyword);
                }
            }

            if(matchingKeywords.size() > 0)
            {
                results.add( new ProgramSearchResult(description, matchingKeywords));
            }
        }
        
        Collections.sort(results, new Comparator<IProgramSearchResult>()
                {

                    public int compare(IProgramSearchResult r1, IProgramSearchResult r2) {
                        return r2.getMatchingKeywords().size() - r1.getMatchingKeywords().size();
                    }
            
                });
        
        return results;
    }
    
    
    


    public void saveContextToFile(String filename, IHialContext context, IGUIContextSaver guiCallback) throws IOException
    {
        if(!contexts.contains(context))
        {
            throw new IllegalArgumentException("context is not a valid context in the framework");
        }
    	
    	StringBuilder text = new StringBuilder();
    	((HialContext)context).putContext(text, guiCallback, 0);
    	
    	FileWriter f = new FileWriter(filename);
    	f.write(text.toString());
    	f.close();
    }

    
    public String loadThumbnailStringFromFile(String filename) throws IOException, XMLLoadingException
    {
        FileInputStream f = new FileInputStream(filename);
        Element contextElem = XMLUtil.stream2Elem(f);
        f.close();
        
        try {
            return XMLUtil.yankText(contextElem, "gui-context", "thumb");           
        }
        catch(Exception e)
        {
            throw new XMLLoadingException(e);
        }
        
        
    }

    public void loadContextFromFile(String filename, IGUIContextLoader guiCallback) throws IOException, XMLLoadingException
    {
        FileInputStream f = new FileInputStream(filename);
        Element contextElem = XMLUtil.stream2Elem(f);
        f.close();
        
        try {
        	loadContext(contextElem, guiCallback);           
        }
        catch(Exception e)
        {
            throw new XMLLoadingException(e);
        }
        
    }
    
    public synchronized String saveContext(IHialContext context, IGUIContextSaver guiCallback)
    {
        if(!contexts.contains(context))
        {
            throw new IllegalArgumentException("context is not a valid context in the framework");
        }
    	
    	StringBuilder text = new StringBuilder();
    	((HialContext)context).putContext(text, guiCallback, 0);
    	return text.toString();
    }

    
    
    public synchronized void loadContext(
            String contextXml,
            IGUIContextLoader guiCallback) throws XMLLoadingException
    {
        
        Element contextElem = XMLUtil.string2Elem(contextXml);
        
        try {
        	loadContext(contextElem, guiCallback);           
        }
        catch(Exception e)
        {
        	e.printStackTrace();
            throw new XMLLoadingException(e);
        }

    }
    

	void loadContext(Element contextElem, IGUIContextLoader guiCallback) throws XMLLoadingException
	{
	    Element guiContextElem = XMLUtil.yankElement(contextElem, "gui-context");
	    guiCallback.loadGUIContext(guiContextElem);
	    
	    Map<Long, IRefObject> idMap = new HashMap<Long, IRefObject>();
	    
	    Set<IProgramIDDescription> programDescriptions = framework.getProgramRepository().getProgramDescriptions();
	    Map<String, IProgramIDDescription> descriptionMap = 
	        new HashMap<String, IProgramIDDescription>();
	    for(IProgramIDDescription programDescription : programDescriptions)
	    {
	        descriptionMap.put(programDescription.getCanonicalName(), programDescription);
	    }
	    
	    Element programsElem = XMLUtil.yankElement(contextElem, "programs");
	    NodeList programList = programsElem.getElementsByTagName("program");
	    
	    for(Element program : XMLUtil.getElementIterator(programList))
	    {
	        Tuple<Long, IRefObject> id2refMapping = 
	            loadProgram(program, guiCallback, descriptionMap);
	        
	        if(id2refMapping != null)
	        {
	            idMap.put(id2refMapping.getFirst(), id2refMapping.getSecond());
	        }
	    }
	    
	    Element pipesElem = XMLUtil.yankElement(contextElem, "pipes");
	    NodeList pipeList = pipesElem.getElementsByTagName("pipe");
	    
	    for(Element pipe : XMLUtil.getElementIterator(pipeList))
	    {
	        loadPipe(pipe, guiCallback, idMap);
	    }

	}
	
	private void loadPipe(Element pipe, IGUIContextLoader guiCallback, Map<Long, IRefObject> idMap) throws XMLLoadingException
	{
	    Long fromProgramID;
	    Long toProgramID;
	    
	    try
	    {
	        fromProgramID = Long.valueOf(XMLUtil.yankText(pipe, "from", "program"));
	        toProgramID = Long.valueOf(XMLUtil.yankText(pipe, "to", "program"));
	    }
	    catch(NumberFormatException nfe)
	    {
	        throw new XMLLoadingException(nfe);
	        //guiCallback.generalPipeCorruption();
	        //return;
	    }
	    
	    IRefObject fromProgram = idMap.get(fromProgramID);
	    IRefObject toProgram = idMap.get(toProgramID);
	    
	    if(fromProgram == null || toProgram == null)
	    {
	        throw new XMLLoadingException("Non-existant program references by pipe in XML file");
	        //guiCallback.generalPipeCorruption();
	        //return;
	    }
	    
	    INodeID fromNodeID = new NodeID(XMLUtil.yankText(pipe, "from", "node"));
	    INodeID toNodeID = new NodeID(XMLUtil.yankText(pipe, "to", "node"));
        
	    Element guiPipe = XMLUtil.yankElement(pipe, "gui-pipe");
	    if(guiPipe == null)
	    {
	        throw new AssertionError("XML validation code should've already spotted this");
	        //guiCallback.generalPipeCorruption();
	        //return;
	    }
	    
	    guiCallback.loadGUIPipe(guiPipe, fromProgram, toProgram, fromNodeID, toNodeID);
	}
	
	private Tuple<Long, IRefObject> loadProgram(
	        Element program, 
	        IGUIContextLoader guiCallback,
	        Map<String, IProgramIDDescription> descriptionMap) throws XMLLoadingException
	{
	    IRefObject ref;
	    IHialProgram hialProgram;
	    
        String programCanonicalName = XMLUtil.yankText(program, "name");
        if(!descriptionMap.containsKey(programCanonicalName))
        {
            guiCallback.programNotFound(programCanonicalName);
            return null;
        }
        
        IProgramIDDescription description = 
            descriptionMap.get(programCanonicalName);
        
        Element guiProgram = XMLUtil.yankElement(program, "gui-program");
        if(guiProgram == null)
        {
            throw new AssertionError("This should've already been caught by the XML validation");
            //guiCallback.generalProgramCorruption();
            //return null;
        }
        
        Tuple<IRefObject, IHialProgram> t = guiCallback.loadGUIProgram(guiProgram, description);
        ref = t.getFirst();
        hialProgram = t.getSecond();

        Element argInstanceElem = XMLUtil.yankElement(program, "arg-instance");
        
        Collection<IArgumentValuePair> values = loadArgInstance(argInstanceElem, hialProgram);

        if(values != null)
        {
            guiCallback.loadGUIArguments(ref, values);
        }
        else //if(Kevlar.DEBUG)
        {
            throw new XMLLoadingException("Argument XML malformed");
            //guiCallback.argumentLoadFailed(ref);
        }
        
        Long id;
        try
        {
            id = Long.valueOf(XMLUtil.yankText(program, "id"));
        }
        catch(NumberFormatException nfe)
        {
            throw new XMLLoadingException(nfe);
            //guiCallback.generalProgramCorruption();
            //return null;
        }

        return new Tuple<Long, IRefObject>(id, ref);
	}
	
	private class ArgLoadingVisitor implements IArgumentVisitor
	{
	    private IBasicArgumentValue value = null;
	    private String data;
	    
	    public ArgLoadingVisitor(String data)
	    {
	        this.data = data;
	    }
	    
	    public void setData(String data)
	    {
	        this.data = data;
	        value = null;
	    }
	    
	    public IBasicArgumentValue getValue()
	    {
	        return value;
	    }
	    
        public void setArgument(ISetArgumentInfo info)
        {
            if(info.getSetValues().containsKey(data))
            {
                value = new SetArgumentValue(data);
            }
        }

        public void boolArgument(IBoolArgumentInfo info)
        {
            if(data.equals("true"))
            {
                value = BoolArgumentValue.valueOf(true);
            }
            else if(data.equals("false"))
            {
                value = BoolArgumentValue.valueOf(false);
            }
        }

        public void singlelineArgument(ISinglelineArgumentInfo info) {
            if((data.indexOf('\n') != -1 || 
                            data.indexOf('\r') != -1 ))
            {
                return;
            }
            
            value = new TextArgumentValue(data);
        }
        
        public void multilineArgument(IMultilineArgumentInfo info) {

            value = new TextArgumentValue(data);
        }
	    
	}
	
	private Collection<IArgumentValuePair> loadArgInstance(Element argInstanceElem, IHialProgram program)
	{
	    ArgLoadingVisitor argLoaderVisitor = new ArgLoadingVisitor("");
	    Collection<IArgumentValuePair> pairs =
	        new ArrayList<IArgumentValuePair>();
	    
	    NodeList pairList = argInstanceElem.getElementsByTagName("pair");
	    for(Element pair : XMLUtil.getElementIterator(pairList))
	    {
	        String argName = XMLUtil.yankText(pair, "name");
	        String data = XMLUtil.yankText(pair, "value");
	        
	        IBasicArgumentInfo argInfo = program.getArgumentInfo(argName);
	        argLoaderVisitor.setData(data);
	        argInfo.takeVisitor(argLoaderVisitor);
	        
	        if(argLoaderVisitor.getValue() != null)
	        {
	            pairs.add(ArgumentModelProvider.makeArgumentValuePair(argInfo, argLoaderVisitor.getValue()));
	        }
	    }
	    
	    return pairs;
	}

	private IProgramIDDescription getShow()
	{
	    if(show != null)
	    {
	        return show;
	    }
	    
        Set<IProgramIDDescription> descriptions =
			framework.getProgramRepository().getProgramDescriptions();
        for(IProgramIDDescription description : descriptions)
        {
            if(description.getCanonicalName() == Show.class.getCanonicalName())
            {
                show = description;
                return show;
            }
        }
        
        throw new AssertionError("Show program not found");
	}
	
}
