package bilab;

import java.util.*;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;

import org.eclipse.core.runtime.Path;

import com.thoughtworks.xstream.*;


public class AddOnManager
{
  public AddOnManager(ResourceManager resourceManager, ISourceExecutor executor)
  {
    xstream = new XStream();
    xstream.alias("addons", AddOns.class);
    xstream.alias("group", ExternalFunctionGroup.class);
    xstream.alias("command", ExternalCommand.class);
    xstream.alias("param", ExternalCommand.ParamInfo.class);
    xstream.alias("variable", ExternalFunction.Var.class);
    xstream.alias("file", ExternalCommand.File.class);
    
    xstream.addDefaultImplementation(LinkedList.class, List.class);
    xstream.addImplicitCollection(AddOns.class, "groups");
    //xstream.addDefaultImplementation(HashMap.class, AbstractMap.class);
    
    externalFunctions = new HashMap<String, ExternalFunction>();
    
    rm = resourceManager;
    this.executor = executor;
  }
  
  
  // parse config XML stream (return false if xml config stream is not valid)
  public boolean processConfiguration(InputStream configStream)
  {
    AddOns newAddOns = null;
    try {
      
      newAddOns = (AddOns)xstream.fromXML(new InputStreamReader(configStream));
      
      // put read functions into addOn map, replacing existing ones with the
      //  same fully-qualified name
      for(ExternalFunctionGroup funcGroup : newAddOns.groups) {
        
        if (funcGroup.namespace==null) funcGroup.namespace = ""; 
        
        for(ExternalFunction extFunc : funcGroup.externalFunctions) {
          
          extFunc.group = funcGroup; // setup back reference
          if ((extFunc.namespace == null) || (extFunc.namespace.length()==0)) 
            extFunc.namespace = funcGroup.namespace; // use default namespace if not specified
          extFunc.debug = extFunc.debug || funcGroup.debug;
          
          // add to map
          externalFunctions.put(extFunc.namespace+"."+extFunc.name,extFunc);
        }
      }
      
      configStream.close();
      
    } catch (Throwable t) {
      Notify.logError(this,"unable to read external addon configuration stream, may be invalid format (some external functions may be consequently unavailable):\n "+t.getMessage());
      return false;
    }

    
    try {
      
      // define the functions we've just read in, by group
      for(ExternalFunctionGroup funcGroup : newAddOns.groups)
        defneExternalFunctions(funcGroup.externalFunctions);
      
    } catch (Exception e) {
      Notify.logError(this,"unable to define external functions from addon configuration stream, may be invalid format (some external functions may be consequently unavailable):\n "+e.getMessage());
      return false;
      
    }
    
    return true;
  }
  
  
  // define external functions via executor
  private void defneExternalFunctions(List<ExternalFunction> functions)
  {
    StringBuffer src = new StringBuffer();
    String namespaceName = null;
    String prevNamespace = null;
    boolean firstFunc = true;
    boolean blockOpen = false;
    
    src.append("{\n"); // toplevel block
    
    for(ExternalFunction extFunc : functions) {
      
      if (extFunc instanceof ExternalCommand) {
      
        ExternalCommand cmd = (ExternalCommand)extFunc;
        
        namespaceName = cmd.namespace;
        
        // block/namespace begin (if different)
        if (firstFunc || !namespaceName.equals(prevNamespace)) {
          
          // if a block is already open, close it before opening a new one
          if (blockOpen)
            src.append("  };\n");
          
          blockOpen = true;
          if ((namespaceName != null) && (namespaceName.length() > 0))
            src.append("  namespace "+namespaceName+" {\n");
          else
            src.append("  {\n");
        }
        
        // output func annotations, if present
        if (cmd.summary != null)
          src.append("    @[bilab.Summary(\""+cmd.summary+"\")]\n");
        if (cmd.docTextOrURL != null)
          src.append("    @[bilab.Doc(\""+cmd.docTextOrURL+"\")]\n");
        
        if ((cmd.sophistication != null) && (!cmd.sophistication.equals("normal"))) {
          if (cmd.sophistication.equals("advanced"))
//            src.append(" @[bilab.Sophistication(bilab.Sophistication.Advanced)]\n");
            src.append("    @[bilab.Sophistication(2)]\n"); //!!! FIXME
          else if (cmd.sophistication.equals("developer"))
//            src.append(" @[bilab.Sophistication(bilab.Sophistication.Developer)]\n");
            src.append("    @[bilab.Sophistication(3)]\n"); //!!! FIXME
          else
            Notify.devWarning(this,"invalid sophistication in configuration file for command '"+cmd.namespace+"."+cmd.name+"' (must be normal, advanced or developer)");
        }
        
        
        // declare func
        src.append("    let "+cmd.name+" = func(");
        // params
        for(int pi=0; pi<cmd.params.size(); pi++) {
          ExternalCommand.ParamInfo pinfo = cmd.params.get(pi);
          src.append(pinfo.name+" :"+pinfo.type);
          if (!pinfo.required) {
            String defaultValue = substVariables(cmd,pinfo.defaultValue);
            if (pinfo.type.equals("string") && (defaultValue.charAt(0) != '\"'))
              defaultValue = "\""+defaultValue+"\"";
            src.append(" = "+defaultValue);
          }
          if (pi != cmd.params.size()-1)
            src.append(", ");
        }
        String returnType = ((cmd.returns == null) || (cmd.returns.length()==0))?"any":cmd.returns;
        src.append("-> "+returnType+") {\n"); // end params, begin body
        
        // handle pre-processing, if any
        if (cmd.preProcess != null) {
          src.append("      // pre-processing");
          src.append("      "+cmd.preProcess+";\n");
        }
        
        // call
        src.append("      let result = bilab.AddOnManager.callExtFunc(\""+cmd.namespace+"\", \""+cmd.name+"\"");
        if (cmd.params.size() > 0) src.append(", ");
        
        //  with func args
        for(int pi=0; pi<cmd.params.size(); pi++) {
          ExternalCommand.ParamInfo pinfo = cmd.params.get(pi);
          src.append(pinfo.name);
          if (pi != cmd.params.size()-1) src.append(", ");
        }
        
        src.append(");\n");
        
        // handle post-processing, if any
        if (cmd.postProcess == null)
          src.append("      result;\n"); // nope, just return result
        else {
          src.append("      // post-processing");  
          src.append("      "+cmd.postProcess+";\n");
        }
        
        src.append("    };\n\n"); // end body
        
      }
      else
        Notify.devError(this,"unhandled ExternalFunction subtype:"+extFunc.getClass());
      
      prevNamespace = namespaceName;
      firstFunc = false;
      
    } // for each external func
    
    
    if (blockOpen) // close last block if it was left open
      src.append("  };\n");

    src.append("}\n"); // toplevel block

    // execute it
    try {
//Notify.devInfo(this,"generated source:\n======\n"+src.toString()+"\n======\n");//!!!          
      executor.executeSource(src.toString());
    } catch (Exception e) {
      Notify.devError(this,"error Scigol source:"+e.getMessage()+"\n"+src.toString());
    }
    
  }
  

  
  // !!! temp convenience for generated interpreter code
  public static Object callExtFunc(String namespaceName, String name, Object ... args)
  {
    return BilabPlugin.getAddOnManager().callExternalFunction(namespaceName, name, args);
  }
  
  
  // call an external function
  public Object callExternalFunction(String namespaceName, String name, Object ... args)
  {
    // find the function by name
    ExternalFunction extFunc = externalFunctions.get(namespaceName+"."+name);
    if (extFunc==null)
      throw new BilabException("no external function named '"+namespaceName+"."+name+"' has been registered");

    if (extFunc instanceof ExternalCommand)
      return callExternalCommand((ExternalCommand)extFunc, args);
    
    Notify.devError(this,"unhandled ExternalFunction subtype:"+extFunc.getClass());
    return null;
  }

  
  public Object callExternalCommand(ExternalCommand cmd, Object ... args)
  {
    // process arguments
    
    int a=0; // argument index
    int fileIndex=0;
    boolean sawOptionalParam = false;
    for(int p=0; p<cmd.params.size();p++) { //  for each param
      ExternalCommand.ParamInfo pinfo = cmd.params.get(p);
      
      Object argValue = null;
      if (!pinfo.required) { // optional param
        sawOptionalParam = true;
        
        if (a < args.length) { // present
          argValue = args[a];
          a++;
        }
        else { // not present
          argValue = substVariables(cmd, pinfo.defaultValue );
        }
        
      }
      else { // required param
        if (sawOptionalParam)
          throw new BilabException("external command configuration for '"+cmd.namespace+"."+cmd.name
              +"' specifies a required parameter after an optional one, which isn't allowed.");
        
        argValue = args[a];
        a++;
      }
      
      
      // if an allowedValues array is supplied, check the argValue is
      //  one of them
      if (pinfo.allowedValues != null) {
        if (pinfo.viaFile)
          throw new BilabException("external command configuration for '"+cmd.name
              +"' specified a set of allowed values for an input file parameter, which isn't legal.");
        
        boolean found=false;
        for(int v=0; (v<pinfo.allowedValues.length) && !found; v++) {
          if ( ((String)argValue).equals(pinfo.allowedValues[v]) )
            found = true;
        }
        if (!found)
          throw new BilabException("Illegal argument value for parameter '"+pinfo.name
              +"' of external function '"+cmd.namespace+"."+cmd.name+"'.");
          
      }
      
      
      // If the parameter isn't an input file parameter, convert
      //  it to a String, otherwise, handle file parameter
      if (!pinfo.viaFile)
        pinfo.value = argValue.toString();
      else { // file parameter
        
        // Convert the supplied argument to a file of the required
        //  type (or use an existing resource file if an appropriate
        //   one is already associated with the argument)

        if ((cmd.inFiles == null) || (fileIndex >= cmd.inFiles.size()))
          throw new BilabException("Parameter '"+pinfo.name
              +"' of external function '"+cmd.namespace+"."+cmd.name+"' is marked as 'viaFile' but" +
                  "there aren't enough <file>s in the <inFiles> element");
          
        ExternalCommand.File fileSpec = cmd.inFiles.get(fileIndex++);
        String requiredResType = fileSpec.resourceType;
        
        String resourceName = getExistingResourceName(argValue, requiredResType);
        boolean existingResourceAvail = (resourceName != null);
        if (!existingResourceAvail) {
          // create temporary resource by exporting the argValue
          resourceName = fileSpec.name;
          if (resourceName.equals("$unique$")) {
            resourceName = rm.uniqueTemporaryResourceName(requiredResType);
            fileSpec.temporary = true;
          }
          else
            resourceName = ResourceManager.filenameToResourceName( substVariables(cmd,resourceName) );
          
          rm.exportObjectToResource(argValue,requiredResType,resourceName);
        }

        fileSpec.actualName = rm.resourceNameToNativeFileName(resourceName);
        
        pinfo.value = resourceName;
      }
      
    } // for each param

    
    // now do substitutions on the output file names (if any)
    if (cmd.outFiles != null) {
      for(int fi=0; fi<cmd.outFiles.size(); fi++) {
        ExternalCommand.File fileSpec = cmd.outFiles.get(fi);
        String name = fileSpec.name;
        if (name.equals("$unique$")) 
          name = rm.uniqueTemporaryResourceName(fileSpec.resourceType);
        else
          name = ResourceManager.filenameToResourceName( substVariables(cmd,name) );
          
        fileSpec.actualName = rm.resourceNameToNativeFileName(name);
      }
    
    
      // before invoking the command, delete any existing output files in case the command
      //  will fail if they already exist
      for(ExternalCommand.File file : cmd.outFiles) { 
        try {
          rm.deleteResource( ResourceManager.filenameToResourceName(file.actualName) );
        } catch (IOException e) {}
      }
    }
    

    // setup command line
    String commandDir = Util.toNativePathSeparator( substVariables(cmd, cmd.externalDir) );
    String commandName = (cmd.executable!=null)? substVariables(cmd, cmd.executable ) : cmd.name;
    String cmdargs = (cmd.argumentTemplate!=null)?substVariables(cmd, cmd.argumentTemplate) : "";

    // gather into a string list
    List<String> cmdLineStrings = new LinkedList<String>();
    cmdLineStrings.add(Util.toNativePathSeparator(commandDir+"/"+commandName)+ResourceManager.exeSuffix); // exe
    String[] cmdArgStrings = cmdargs.split(" "); // split on space
    for(String argStr : cmdArgStrings) cmdLineStrings.add(argStr);

    if (cmd.debug)
      Notify.devInfo(this,"Executing command: "+commandName+ResourceManager.exeSuffix+" "+cmdargs);
    ProcessBuilder pb = new ProcessBuilder(cmdLineStrings); 
    
    // environment variables
    //  setup cmd specific ones first, followed by group-side ones that weren't overridden
    Map<String, String> env = pb.environment();
    if (cmd.env != null)
      for(ExternalFunction.Var envVar : cmd.env) {
        String name = substVariables(cmd,envVar.name);
        String value = substVariables(cmd,envVar.value);
        env.put(name,value);
      }
    if (cmd.group.env != null)
      for(ExternalFunction.Var envVar : cmd.group.env) {
        String name = substVariables(cmd,envVar.name);
        if (!env.containsKey(name)) {
          String value = substVariables(cmd,envVar.value);
          env.put(name,value);
        }
          
      }
    
    String workingDirStr = ((cmd.workingDir!=null) && (cmd.workingDir.length()>0))? cmd.workingDir : cmd.group.workingDir;
    if (workingDirStr == null) {
      workingDirStr = "";
      Notify.devWarning(this,"no workingDir specified in configuration file for command '"+cmd.namespace+"."+cmd.name+"'");
    }
    String workingDir = Util.toNativePathSeparator(substVariables(cmd, workingDirStr));
    pb.directory( new File(workingDir) );
    pb.redirectErrorStream( cmd.returnStderrText );
    
    Object result = null;
    
    // invoke command 
    try {
      Process process = pb.start();
      
      String outputText = null;
      
      Writer stdin = null;
      if (cmd.stdinText != null) {
        String stdinText = substVariables(cmd,cmd.stdinText);
        OutputStream stdinStream = process.getOutputStream();
        stdin = new BufferedWriter(new OutputStreamWriter(stdinStream));
        
        stdin.write(stdinText);
        stdin.flush();

        if (cmd.debug) Notify.devInfo(this,"Piped to command stdin:"+stdinText);
      }
      
      
      // periodically check for process termination
      final double timeout = 30.0; // secs !!! make this an arg
      boolean terminated = false;
/*      double time = 0.0;
      int exitValue=0;
      do {
        // check termination by trying to get the exit value - which
        //  it illegal if it is still running
        try {
          exitValue = process.exitValue();
          terminated = true;
        } catch (IllegalThreadStateException e) {
          // didn't terminate
        }
        
        if (!terminated) {
          Thread.sleep(1000);
          time += 1.0;
        }
        
      } while (!terminated && (time < timeout));
      

      if (exitValue != 0)
        Notify.devWarning(this,"external command "+commandName+ResourceManager.exeSuffix+" returned abnormal exit code:"+exitValue);
      
      
      if (!terminated) { // process termination timed-out
        try {
          process.destroy(); // kill it
        } catch (Throwable t) {}
      }
*/
terminated=true;//!!!
      
      if (cmd.returnStdoutText || (!terminated)) {
        
        // capture stdout[/stderr]
        InputStream cmdResultStream = process.getInputStream();
        InputStreamReader cmdResultReader = new InputStreamReader(cmdResultStream);
        
        StringBuilder str = new StringBuilder();
        
        int c = cmdResultReader.read();
        while (c != -1) {
          str.append((char)c);
          c = cmdResultReader.read();
        }
        
        outputText = str.toString();
      }

process.waitFor();//!!!

      if (stdin != null) stdin.close();
      
      
      // if process didn't terminate, throw an exception and dump the output
      if (!terminated) {
        if (outputText==null) outputText="";
        Notify.logError(this,"Output of failed external command "+commandName+ResourceManager.exeSuffix+" "+cmdargs+":\n"+outputText+"\n---\n");
        Notify.userError(this,"external command failed to terminate.  See log for details");
        return null;
      }
      
      
      // was stdout to be interpreted as a result resource stream?
      if (cmd.stdoutResType != null) {
        
        // handle case of TEXT type into string more efficiently as a special case
        if (cmd.stdoutResType.equals("TEXT")) {
          result = outputText;
        }
        else {
          Notify.unimplemented(this,"converting stdout to object via resource type "+cmd.stdoutResType);
          //rm.instantiateObjectFromResource()
        }
        
      }
      else if (cmd.returnStdoutText) {
        // display outputText on console stream
        if (outputText.length() > 0) {
          Notify.devWarning(this,"output to console stream not implemented.");
          System.out.println("stdout/stderr:\n"+outputText);
        }
      }
    
      
      // display command output to stdout
      if (cmd.debug && (cmd.returnStdoutText || cmd.returnStderrText)) {
        Notify.devInfo(this,"\n--- Command output:\n"+outputText+"\n---\n");
      }
      
      
      // read back first output file (if any) as the result
      if ((cmd.outFiles != null) && (cmd.outFiles.size() > 0)) {
        
        ExternalCommand.File fileSpec = cmd.outFiles.get(0);
        String resourceName = ResourceManager.filenameToResourceName(fileSpec.actualName);
        
        result = rm.instantiateObjectFromResource(resourceName, fileSpec.resourceType);
        
      }
    
    } catch (InterruptedException e) {
      throw new BilabException("function '"+cmd.namespace+"."+cmd.name+"' was interrupted.");
    } catch (IOException e) {
      throw new BilabException("An I/O error occured while executing function '"+cmd.namespace+"."+cmd.name+"' - "+e.getMessage());
    } finally {
      
        
      // first delete any temporary input files
      if (cmd.inFiles != null)
        for(ExternalCommand.File file : cmd.inFiles) {
          if (file.temporary) {
            try {
              rm.deleteResource( ResourceManager.filenameToResourceName(file.actualName) );
            } catch (Exception e) {
              Notify.devWarning(this,"unable to delete a resource - "+e.getMessage());
            }
          }
        }
      
      // and now *all* output files
      if (cmd.outFiles != null)
        for(ExternalCommand.File file : cmd.outFiles) {
          try {
            rm.deleteResource( ResourceManager.filenameToResourceName(file.actualName) );
          } catch (Exception e) {
            Notify.devWarning(this,"unable to delete a resource - "+e.getMessage());
          }
        }
      
    }
    
    return result;
  }
  
  
  
  
  // substitute variables of the form $varname$ for appropriate values
  //  defined variables are:
  //   $N$ - substitute the string value of parameter n
  //   $infileN$ - substitute the file name of command input file N
  //   $outfileN$ - substitute the file name of command output file N
  //   $pluginRoot$ - substitute the directory path of the plugin 'root' dir
  //   $?NAME:TEXT$ - if variable/parameter NAME is 'true' then substitute TEXT else the empty string
  //   $TEXT1[NAME]TEXT2$ - if parameter NAME is its default value, substitute the empty string,
  //                   else substitute TEXT1+parameter value+TEXT2
  //   $NAME$ - substitute the strng value of user defined variable NAME or environment variable
  //    in addition, if the string begins with '!', it will be stripped off prior
  //    to substitutions being made and the result will be treated as a path
  //    (i.e. seperators will be converted to appropriate characters for the native platform)
  //    A further substitution is also made using the variable value
  //    The search order is: 1) parameters 2) Command specific env variables 3) Group wide env variables
  //     4) Group wide variables 5) Platform environment variables
  private String substVariables(ExternalCommand cmd, String s)
  {
    String original = s;
    
    if ((s.length() > 0) && (s.charAt(0)=='!')) 
      return Util.toNativePathSeparator( substVariables(cmd,s.substring(1)) );
    
    if (s.indexOf('$') == -1) return s; // short circuit
    
    StringBuilder sb = new StringBuilder();
    int i = s.indexOf('$');
    while (i != -1) {
      sb.append( s.substring(0,i));
      s = s.substring(i+1);
      int i2 = s.indexOf('$');
      if (i2 == -1) {
        Notify.devWarning(this,"mismatched '$' in add-on configuration file for command '"+cmd.namespace+"."+cmd.name+"' in string '"+original+"'");
        return original; // abort substitution
      }  
      String varname = s.substring(0,i2);
      if (i2+1 < s.length())
        s = s.substring(i2+1);
      else
        s = "";
      
      String varvalue = "UNDEFINED";
      
      if (varname.equalsIgnoreCase("pluginRoot")) {
        try {
          varvalue = rm.getPluginFilesystemRoot();
        } catch (IOException e) {
          Notify.devError(this,"IO exception retrieving plugin root");
        }
      }
      else if (varname.startsWith("infile")) {
        int fileIndex = Integer.parseInt(varname.substring(6));
        if ((fileIndex <0) || (fileIndex >= cmd.inFiles.size())) {
            Notify.devWarning(this,"invalid infile index in add-on configuration file for command '"+cmd.namespace+"."+cmd.name+"' in string '"+original+"'");
            return original; // abort substitution
        }
        varvalue = cmd.inFiles.get(fileIndex).actualName;
      }
      else if (varname.startsWith("outfile")) {
        int fileIndex = Integer.parseInt(varname.substring(7));
        if ((fileIndex <0) || (fileIndex >= cmd.outFiles.size())) {
            Notify.devWarning(this,"invalid outfile index in add-on configuration file for command '"+cmd.namespace+"."+cmd.name+"' in string '"+original+"'");
            return original; // abort substitution
        }
        varvalue = cmd.outFiles.get(fileIndex).actualName;
      }
      else if (varname.charAt(0) == '?') { // conditional
        int ci = varname.indexOf(':');
        String condvalue = lookupVar(cmd, varname.substring(1,ci));
        if (condvalue.equals("true"))
          varvalue=varname.substring(ci+1);
        else
          varvalue="";
      }
      else if (varname.contains("[") && varname.contains("]")) { // if not default
        int li = varname.indexOf('[');
        int ri = varname.indexOf(']');
        String paramName = varname.substring(li+1,ri);
        ExternalCommand.ParamInfo paramInfo = null;
        for(ExternalCommand.ParamInfo pinfo : cmd.params) 
          if (pinfo.name.equals(paramName)) {
            paramInfo = pinfo;
            break;
          }
        if (paramInfo == null) {
          Notify.devWarning(this,"undefined parameter name '"+paramName+"' in add-on configuration file for command '"+cmd.namespace+"."+cmd.name+"' in string '"+original+"'");
          return original; // abort substitution
        }
        // now, is the value the default value? - if so this var evaulates to empty
        if (paramInfo.value.equals(paramInfo.defaultValue))
          varvalue = "";
        else
          varvalue = varname.substring(0,li) + paramInfo.value + varname.substring(ri+1);
      }
      else {
        
        // look for env or user variable
        varvalue = lookupVar(cmd,varname);
        
        if (varvalue==null) {
          Notify.devWarning(this,"invalid variable '"+varname+"' in add-on configuration file for command '"+cmd.namespace+"."+cmd.name+"' in string '"+original+"'");
          return original; // abort substitution
        }
      }
      
      sb.append( substVariables(cmd,varvalue) );
      
      i = s.indexOf('$');
    }
    
    sb.append(s);
    
    return sb.toString();
  }

  
  // helper for substVariables above
  //  lookup order is: param name; cmd specific env vars; group wide env vars; group wide vars; platform env vars
  private String lookupVar(ExternalCommand cmd, String varname)
  {
    // look for env or user variable
    //  order is: param name; cmd specific env vars; group wide env vars; group wide vars; platform env vars
    
    if (Character.isDigit(varname.charAt(0))) { // special parameter index notation
      int paramIndex = Integer.parseInt(varname);
      if ((paramIndex >= 0) || (paramIndex < cmd.params.size())) 
        return cmd.params.get(paramIndex).value;
    }
        
    for(ExternalCommand.ParamInfo pinfo : cmd.params) 
      if (pinfo.name.equals(varname)) 
        return pinfo.value;
    
    if (cmd.env != null)
      for(ExternalFunction.Var envVar : cmd.env) // in cmd specific config env variables
        if (envVar.name.equals(varname)) 
          return envVar.value;

    if (cmd.group.env != null)
      for(ExternalFunction.Var envVar : cmd.group.env) // in group wide config env variables
        if (envVar.name.equals(varname)) 
          return envVar.value;
    
    if (cmd.group.vars != null)
      for(ExternalFunction.Var var : cmd.group.vars) // in group wide variables
        if (var.name.equals(varname)) 
          return var.value;
    
    // in platform env
    String platformEnvVar = System.getenv(varname);
    if ((platformEnvVar!=null) && (platformEnvVar.length()>0)) 
      return platformEnvVar;
    
    return null; // not found
  }
  
  
  
  
  
  
  // if an existing resource is available in the required format, return the
  //  name, else return null
  private String getExistingResourceName(Object value, String requiredResourceFormat)
  {
    // currently, only seqences record the associated resources (from which they
    //  were created)
    if (!(value instanceof seq)) return null;
    
    seq sequence = (seq)value;
    String resourceName = sequence.get_AssociatedResource();
    if (resourceName == null) return null; // no resource associated with sequence
    List<String> typesForExtension = rm.getResourceTypesWithExtension(Util.extension(resourceName));
    if (typesForExtension.size() == 0) return null; // unknown format of associated resource
    if (typesForExtension.size() > 1) return null; // associated resource type is ambigious based on extension (don't risk using it in-case it is the wrong type)
    
    try {
      if (typesForExtension.contains(requiredResourceFormat))
        return (new URL(resourceName)).getFile();
    } catch (MalformedURLException e) {}
    
    return null;
  }
  
  
  

  
  
/*  
  protected ExternalFunction findExternalFunction(String name)
  {
    int i=0;
    ExternalFunction extFunc = null;
    
    while (i < addOns.externalFunctions.size() && (extFunc == null)) {
        ExternalFunction func = addOns.externalFunctions.get(i);
        if (func.name.equals(name))
            extFunc = func;
        i++;
    }
    
    return extFunc;
  }
*/  
  
  
  //!!!! for testing
  public void writeTestConfig(OutputStream configStream)
  {
    ExternalCommand cmd = new ExternalCommand();
    
    cmd.name = "antigenic";
    cmd.namespace = "bilab.emboss";
    cmd.summary = "predicts potentially antigenic regions of a protein sequence, using the method of Kolaskar and Tongaonkar";
    cmd.docTextOrURL = "file:EMBOSS/doc/html/antigenic.html";
    
    cmd.executable = "antigenic";
    cmd.externalDir = "$pluginRoot$/EMBOSS";
    cmd.workingDir = cmd.externalDir;
    
    cmd.env.add(new ExternalFunction.Var("{($platform$==//win//)?//EMBOSSWIN//://EMBOSS//}", cmd.externalDir));
    
    cmd.inFiles.add(new ExternalCommand.File("$unique1$","FASTA"));
    //cmd.outFiles.add(new String[] {"%unique2",""});
    
    ExternalCommand.ParamInfo param1 = new ExternalCommand.ParamInfo();
    param1.name = "sequence";
    param1.type = "bilab.seq";
    param1.required = true;
    param1.viaFile = true;
    param1.defaultValue = null;
    param1.allowedValues = null;
    
    cmd.params.add(param1);
    
    cmd.argumentTemplate = "-auto -rformat $1$ fasta::$file1$";
    
    ExternalFunctionGroup group = new ExternalFunctionGroup();
    group.name = "emboss";
    group.namespace = "bilab.emboss";
//!!!    group.variables.put
    group.externalFunctions.add(cmd);
    
    AddOns addOns = new AddOns();
    addOns.groups.add(group);
    
    try {
      
      Writer writer = new OutputStreamWriter(configStream);
      xstream.toXML(addOns, writer);
      
      writer.flush();
      configStream.close();
    } catch (IOException e) {
      Notify.userWarning(this,"unable to write external addon configuration stream - "+e.getMessage());
    }
    
  }
  
  
  
  private static class ExternalFunctionGroup
  {
    public ExternalFunctionGroup()
    {
      name = null;
      vars = null;
      namespace = null;
      workingDir = "";
      env = null;
      debug = false;
      externalFunctions = new LinkedList<ExternalFunction>();
    }
    
    // group wide variables
    public String name;
    public Set<ExternalFunction.Var> vars;

    // defaults for all ExternalFunctions in group
    public String namespace;
    public boolean debug;
    
    //  for commands only
    public String workingDir;
    public Set<ExternalFunction.Var> env;
    
    
    // the actual functions
    public List<ExternalFunction> externalFunctions;
    
  }
  
  
  
  private static abstract class ExternalFunction
  {
    public ExternalFunction()
    {
      // set defaults
      name = "untitled";
      namespace = "bilab";
      summary = "";
      docTextOrURL = "";
      sophistication = "normal";
      debug = false;
    }
    
    public String name;
    public String namespace;
    public String summary;
    public String docTextOrURL;
    public String sophistication;
    public boolean debug;
    
    public transient ExternalFunctionGroup group; // parent group back reference
    
    
    public static class Var
    {
      public Var() {}
      public Var(String name, String value)
      {
        this.name = name;
        this.value = value;
      }
      
      public String name;
      public String value;
    }

  }
  
  
  
  
  // information describing an external command-line application
  private static class ExternalCommand extends ExternalFunction
  {
    public ExternalCommand()
    { 
      // set defaults
      executable = name;
      externalDir = "/";
      workingDir = "/";
      
      env = new HashSet<Var>();
      
      stdinText = null;
      stdoutResType = null;
      returnStdoutText = true;
      returnStderrText = true;
      
      inFiles = new LinkedList<File>();
      outFiles = new LinkedList<File>();
      
      params = new LinkedList<ParamInfo>();
      
      argumentTemplate = "";
    }
    
    // description of a single prameter to an external function
    private static class ParamInfo
    {
      public String name;
      public String type;
      public boolean required;
      public boolean viaFile;
      public String[] allowedValues;
      public String defaultValue;
      public transient String value;
    }
    
    
    
    private static class File
    {
      public File() {}
      public File(String name, String resourceType)
      {
        this.name = name;
        this.resourceType = resourceType;
        actualName= null;
        temporary = false;
      }
      
      public String name;
      public String resourceType;
      
      public transient String actualName;
      public transient boolean temporary;
    }

    
    public String executable;   // executable name, without platform extension (e.g. .exe)
    public String externalDir;  // directory containing command executable
    public String workingDir;   // directory to be current when command is run
    
    public Set<Var> env; // set of environment variable name, value pairs
    
    public String stdinText;     // text to send to commands stdin (after variable substitution)
    public String stdoutResType; // resource type produced on stdout, or null
    
    public String preProcess;    // Scigol code to execute before command
    public String postProcess;   // Scigol code to execute after command
    
    public boolean returnStdoutText;   // true if stdout should be captured & returned
    public boolean returnStderrText;   // true if stderr should be captured & returned with stdout
    
    public List<File> inFiles;  // list of input file-name, resourceTypeName pairs
    public List<File> outFiles; // list of output file-name, resourceTypeName pairs
    
    public List<ParamInfo> params; // list of command parameters
    public String returns;         // scigol return type of function (null == any)
    
    public String argumentTemplate; // command arguments template, with format placeholders for actual parameters
   
  }
  
  
  private XStream xstream;
  
  // for serialization only
  private static class AddOns {
    
    public AddOns() 
    {
      groups = new ArrayList<ExternalFunctionGroup>();
    }
    
    public ArrayList<ExternalFunctionGroup> groups;
    
  }
  
  // Map from fully-qualified function name to external function specification
  private Map<String, ExternalFunction> externalFunctions;
  
  private ResourceManager rm;
  private ISourceExecutor executor;
}
