package nxm.sys.intr;

import static nxm.sys.lib.Generate.genExplainFromComments;
import static nxm.sys.lib.Generate.genExplainFromSource;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;

import nxm.sys.inc.AsciiMap;
import nxm.sys.inc.Installation;
import nxm.sys.lib.Args;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.FileName;
import nxm.sys.lib.FileUtil;
import nxm.sys.lib.FileUtil.FnFilter;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.JarFile;
import nxm.sys.lib.MaskValue;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.OptionTree;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.Util;
import tools.BuildProps;


/** Generate or modifies files of various formats (text, jars, etc.) for a named
    command or library function.
    @author Jeff Schoen / Neon Ngo / J. Czechowski IV
*/
public class Generate extends Intrinsic implements AsciiMap {
  private static final Name NAME_BUNDLE_VENDOR = new Name("Bundle-Vendor");

  /** option trees included with the official NeXtMidas baseline (MUST be sorted alphabetically and in lower-case) */
  private static String[] BASELINE_OPTION_TREES = {"audio", "database", "dsp", "hwc", "nettest", "sys", "ucl", "vnc", "webvnc", "xbc"};

  /** skip options enum for "JARS" function: <pre>
      JARSTANDARD - skip standard jar file creation (.class, .mm, data, etc.)
      JARNATIVE   - skip jarring up native library files for current OS (machine)
      COPYJARS    - skip copying and signing jar files from JAR area (directory) to destination
    For "BUNDLE" function: these options are available: (Since NeXtMidas 3.3.0)
      BUNSTANDARD - Skip standard OSGi bundle file creation
      BUNNATIVE   - Skip native libraries OSGi bundle file creation
      BUNSOURCE   - Skip source code OSGi bundle file creation
      </pre>
      WARNING: Rename/remove the constants in this enum with care as it will "break" previously
               valid options for the /SKIP= switch.
      @since NeXtMidas 2.9.0
   */
  private static enum SkipOptions { JARSTANDARD, JARNATIVE, COPYJARS, BUNSTANDARD, BUNNATIVE, BUNSOURCE};
  /** default skip options enum mask for "JARS" function */
  private static final MaskValue<SkipOptions> DEF_SKIP_OPTIONS = MaskValue.constant(SkipOptions.class); // default to NONE

  private static enum JarOption {
    JAR_STANDARD(   "standard JAR",    false, false), // Standard JAR file
    JAR_NATIVE(     "native   JAR",    true,  false), // Native JAR file (for all OS)
    BUNDLE_STANDARD("standard bundle", false, true),  // Standard OSGi bundle JAR
    BUNDLE_NATIVE(  "native   bundle", true,  true),  // Native OSGi bundle JAR
    BUNDLE_SOURCE(  "source   bundle", false, true);  // Source OSGi bundle JAR

    private final String  description;
    private final boolean nativeJAR;
    private final boolean osgiBundle;

    private JarOption (String description, boolean nativeJAR, boolean osgiBundle) {
      this.description = description;
      this.nativeJAR   = nativeJAR;
      this.osgiBundle  = osgiBundle;
    }

    @Override
    public String toString () {
      return description;
    }

    /** Is this an OSGI bundle? */
    public boolean isOSGIBundle () {
      return osgiBundle;
    }

    /** Is this a JAR with native libraries? */
    public boolean isNative () {
      return nativeJAR;
    }
  }

  // initialization of doWarn & debug via switch, waitingOnGui always false when write JnlpFile completes, optInfoCache used for jar manifest
  private boolean           doWarn       = true;  // Issue warnings if true
  private boolean           debug        = false; // Show debug output?
  private boolean           waitingOnGui = false; // Indicates if waiting for a GUI to make it's call back.
  private Map<String,Table> optInfoCache = new HashMap<>(); // since 3.3.0

  /** default constructor */
  public Generate () {}

  @Override
  public int open () {
    String  nmrootJarsDir = FileName.terminatePath(Shell.getNmRoot()+"jars"); // The $NMROOT/jars/ dir
    String  func  = MA.getU("FUNC");
    String  name  = MA.getU("NAME");
    String  ffilt = MA.getCS("/FFILT");
    String  root  = MA.getCS("/ROOT", null); // since 3.3.0 for BUG 2648
    String  os    = MA.getCS("/OS",   null); // since 3.3.0 for BUG 2648
    String  arch  = MA.getCS("/ARCH", null); // since 3.3.0 for BUG 2648
    boolean noTest= MA.getState("/NOTEST", false);  // do not include test dirs in JAR (since 3.3.4 - Bug 2747)
    boolean noDocs= MA.getState("/NODOCS", false);  // do not include docs dirs in JAR (since 3.3.4 - Bug 2747)
    boolean noGraphics = MA.getState("/NOGRAPHICS", false);  // do not include libg, prim, intr, dirs in JAR (since 4.0.0 - NXM-3678)
    doWarn = MA.getState("/WARN", doWarn);
    debug  = MA.getState("/DEBUG",debug);

    // since 3.3.4 (Bug 2747), allow users to not add docs and test files to JAR
    String skipDirs = "";
    if (noTest) skipDirs += "|-test/*";  // remove test dir
    if (noDocs) skipDirs += "|-docs/*";  // remove docs dir
    if (noGraphics) {
      skipDirs += "|-libg/*|-prim/*|-intr/*|-dat/*|-net/*|-host/*|-mcr/*|-osgi/*"; // NXM-3678 since 4.0.0
      skipDirs = noTest ? skipDirs : skipDirs + filterTestNotJunit() + "|-test/junit/libg/*|-test/junit/net/*|-test/junit/prim/*";
    }

    if (MA.isPresent("/SIGN") || MA.isPresent("/KEEP")) {
      M.deprecate("The /KEEP and /SIGN switches were deprecated in NeXtMidas 2.5 and removed in NeXtMidas 3.3."
                + " Please remove them from your macro as they may be repurposed in the future.");
    }

    if (func.startsWith("JNLP")) {
      name = MA.getCS("NAME");
      if (func.equals("JNLP/GUI") || func.equals("JNLP/TABLE")) {
        nxm.sys.lib.Table table = MA.getTable("P3");
        if (func.indexOf("/GUI") >= 0) {
// JC4: The GUI option has been broken since at least when the os and arch were
//      added to the native JARs. Given the lack of DRs about this, and the known
//      issues dating to 2010 and earlier, it is probably safe to consider this
//      for deprecation.
          // switched to reflection in 4.0.0 to allow no graphics version
          Class<?> mjGenerateJnlpClass = null;
          Method createJnlpFileMethod = null;
          try {
            mjGenerateJnlpClass  = Class.forName("nxm.sys.libg.MJGenerateJnlp");
            createJnlpFileMethod = mjGenerateJnlpClass.getDeclaredMethod("createJnlpFile", Generate.class, String.class, String.class, Table.class);
            createJnlpFileMethod.invoke(null, this, name, null, table);
            waitingOnGui = true;
            pollTime = args.getD("/POLL", 0.25); // default to a quarter second poll time, since nothing is done in process()
          }
          catch (Exception ex) {
            M.error("GENERATE JNLP/GUI not supported without lib class MJGenerateJnlp");
          }
        } else {
          writeJnlpFile(name, null, table);
        }
      } else {
        String opts = MA.getCS("P3","SYS");
        String dest = MA.getCS("P4", nmrootJarsDir);
        String codebase = MA.getCS("P5", "http://localhost/nmroot/jars");
        String href = MA.getCS("P6", null);
        // 2010-05-10 TODO: explain file says that <name> is case-sensitive under JNLP section
        //                  BUT it is always getting lower cased!! I think we should conform to
        //                  the explain file in the next BETA (2.9.0)
        doJnlp(name.toLowerCase(), opts, dest, codebase, href);
      }
    }
    else if (func.equals("JARS")) {
      String dest              = MA.getCS("P3", "jars");
      Table  keystoreCfg       = MA.getTable("P4", null);           // since 3.5.1 for BUG 2803 (replaced P4, P5, P6, P7 args from 3.5.0)
      String moreJarsignerOptions = MA.getCS("/SIGNOPTIONS", null); // since 3.5.2 for BUG 2803
      String versionQualifier  = null; // not using version qualifier for regular jar file generation

      if (dest.equals("jars")) dest = nmrootJarsDir;

      MaskValue<Generate.SkipOptions> skipMask = args.getMaskValue("/SKIP", null, DEF_SKIP_OPTIONS);

      if (!skipMask.isSet(Generate.SkipOptions.JARSTANDARD)) {
        doJarFile(name, dest, ffilt, Generate.JarOption.JAR_STANDARD, root, os, arch, skipDirs,
            keystoreCfg, moreJarsignerOptions, versionQualifier);
      }
      if (!skipMask.isSet(Generate.SkipOptions.JARNATIVE)) { // Since 2.9.2 (replaces JAR_ALL)
        doJarFile(name, dest, ffilt, Generate.JarOption.JAR_NATIVE, root, os, arch, skipDirs,
            keystoreCfg, moreJarsignerOptions, versionQualifier);
      }
      if (!skipMask.isSet(Generate.SkipOptions.COPYJARS)) {
        copyJarFiles(name, dest, keystoreCfg, moreJarsignerOptions, debug);
      }
    }
    else if (func.equals("SIGN")) {
      Table  keystoreCfg       = MA.getTable("P3", null);           // since 3.5.2 for BUG 2803 (replaced P3, P4, P5, P6 args from 3.5.0)
      String moreJarsignerOptions = MA.getCS("/SIGNOPTIONS", null); // since 3.5.2 for BUG 2803
      String opt = null; // TODO: add switch or argument to allow user to specify option tree
      JarFile.signJarFile(M, MA.getFileName("NAME"), opt, keystoreCfg, moreJarsignerOptions, debug);
    }
    else if (func.equals("OPER")) {
      String path = M.io.name("libm","","","SYS");
      nxm.sys.libm.Generate.doOpers (path,name,true);
    }
    else if (func.startsWith("EXP")) {
      Args    targs   = Shell.parseCommand(M,name);
      boolean doStub  = (func.indexOf("/S") > 0);
      String  expName = MA.getCS("P3", null);
      boolean edit    = MA.getState("/EDIT");

      doExplain(targs.name.toLowerCase(), expName, targs, doStub, edit);
    }
    else if (func.startsWith("BUN")) { // BUNdle - since NeXtMidas 3.3.0 for Bug 2484
      String dest              = MA.getCS("P3", "bundle");
      Table  keystoreCfg       = MA.getTable("P4", null);           // since 3.5.1 for BUG 2803 (replaced P4, P5, P6, P7 args from 3.5.0)
      String moreJarsignerOptions = MA.getCS("/SIGNOPTIONS", null); // since 3.5.2 for BUG 2803
      String versionQualifier  = MA.getCS("/QUALVER", null);        // since 3.5.1 for BUG 2485

      if (dest.equals("bundle")) {
        dest = FileName.terminatePath(Shell.getNmRoot()+"bundle");
      }
      else if (dest.equals("jars")) {
        dest = nmrootJarsDir;
      }
      File file = new File(dest);
      if (!file.exists()) { // if directory does not exist
        file.mkdirs();      // create directory and any necessary parent dirs
      }

      MaskValue<Generate.SkipOptions> skipMask = args.getMaskValue("/SKIP", null, DEF_SKIP_OPTIONS);
      if (!skipMask.isSet(Generate.SkipOptions.BUNSTANDARD)) {
        doJarFile(name, dest, ffilt, Generate.JarOption.BUNDLE_STANDARD, root, os, arch, skipDirs,
                  keystoreCfg, moreJarsignerOptions, versionQualifier);
      }
      if (!skipMask.isSet(Generate.SkipOptions.BUNNATIVE)) {
        doJarFile(name, dest, ffilt, Generate.JarOption.BUNDLE_NATIVE, root, os, arch, skipDirs,
                  keystoreCfg, moreJarsignerOptions, versionQualifier);
      }
      if (!skipMask.isSet(Generate.SkipOptions.BUNSOURCE)) {
        doJarFile(name, dest, ffilt, Generate.JarOption.BUNDLE_SOURCE, root, os, arch, skipDirs,
                  keystoreCfg, moreJarsignerOptions,  versionQualifier);
      }

    } else {
      throw new MidasException("GENERATE: Unknown function FUNC='"+func+"'.");
    }
    return NORMAL;
  }

  private String filterTestNotJunit () {
    String filter = "";
    char start = 'a';
    char last = 'z';
    for (char i=start; i <= last; i++) {
      if (i == 'j') continue;
      filter += "|-test/"+i+"*";
    }
    start = 'A';
    last = 'Z';
    for (char i=start; i <= last; i++) {
      if (i == 'J') continue;
      filter += "|-test/"+i+"*";
    }
    return filter;
  }

  @Override
  public int process () {
    return (waitingOnGui)? NOOP: FINISH; // waitingOnGui is used by JNLP/GUI
  }

  /** Generates an explain file. */
  private void doExplain (String name, String expName, Args targs, boolean doStub, boolean edit) {
    String opt    = targs.option.toLowerCase();
    String editor = MR.getString("ENV.EDITOR");
    String source = (new FileName(targs.getDefaultSourceName())).toString();

    boolean overWrite = MA.getState("/NEW");
    // Generate an explain STUB or Generate the explain file from comments
    // in the source code
    if (expName == null) expName = M.io.name("exp",name.toLowerCase(),"exp",opt);
    int status;

    if (doStub) { status = genExplainFromSource (source, name, expName, overWrite, doWarn); }
    else        { status = genExplainFromComments (source, expName, overWrite); }
    if (status < 0) {
      if (status == -2) {
        if (doWarn) M.warning ("Explain file for "+name+" exists. Use /NEW switch to overwrite");
      } else {
        if (doWarn) M.warning ("Unable to create explain file for "+name+" ,status = "+status);
      }
    }

    if (edit) Shell.runCommand (M,editor+" "+source);
  }

  /** Generates a JNLP file based on a table of properties. <br>
      <br>
      The table should specify the following properties.
      <pre>
        JNLPHREF   - The "href" for the JNLP file.          [Required]
        CODEBASE   - The "codebase" for the JNLP jars.      [Required]
        COMMAND    - The name of the command to run.        [Required]
        USENATIVE  - Enables native libraries (TRUE/FALSE). [Default=FALSE]
        OPTS       - The option trees required.             [Default=SYS]
        TITLE      - Title of the application.              [Default="MyApp"]
        DESCR      - Description of the application.        [Default=title]
        SHORTDESCR - Short description of the application.  [Default=descr]
        VENDOR     - The ID of the vendor.                  [Default="ICE"]
        HOMEPAGE   - The homepage of the application.       [Default="nextmidas.html"]
        JAVAVER    - The Java versions that can be used.    [Default="1.8+"]
      </pre>

      @param fileName     The file name of the JNLP file (can not be null).
      @param templateName The name of the template file to save (usually null).
      @param table        The table of properties.
  */
  public void writeJnlpFile (String fileName, String templateName, nxm.sys.lib.Table table) {
    if ((fileName == null) && (templateName == null)) {
      M.info("No file written.");
    }

    // Since 3.4.2 for BUG 2799
    String propPrefix = "";
    if (Installation.ApplyBugFix.GENERATE_JNLP_PREPEND_JNLP_TO_PROPERTIES) {
        propPrefix = "jnlp.";
    }

    if (fileName != null) {
      String  codebase   = table.getS("CODEBASE");
      String  href       = table.getS("JNLPHREF");
      String  command    = table.getS("COMMAND");
      boolean useNative  = table.getState("USENATIVE", false);
      String  opts       = table.getS("OPTS",       "SYS");
      String  title      = table.getS("TITLE",      "MyApp");
      String  descr      = table.getS("DESCR",      title);
      String  shortDescr = table.getS("SHORTDESCR", descr);
      String  vendor     = table.getS("VENDOR",     "ICE");
      String  homepage   = table.getS("HOMEPAGE",   "nextmidas.html");
      String  javaVer    = table.getS("JAVAVER",    "1.8+");

      try {
        ////////////////////////////// List the Jars
        String[] optionTrees = opts.split("[|]"); // Split on '|'
        ArrayList<File> jars = new ArrayList<>(); // List of the required jars.
        File jarDir = new File(Shell.getNmRoot(), "jars");

        // Always do SYS first
        jars.add(new File(jarDir, "nxm-sys.jar"));
        if (useNative) { jars.add(new File(jarDir, "nxm-sys-all.jar")); } // TODO: fix this
        jars.addAll(Arrays.asList(OptionTree.getJarFiles("SYS")));

        for(int i = 0; i < optionTrees.length; i++) {
          String opt = optionTrees[i].toLowerCase().trim();

          if (!opt.equals("sys") && (opt.length() > 0)) {
            jars.add(new File(jarDir, "nxm-"+opt+".jar"));
            if (useNative) { jars.add(new File(jarDir, "nxm-"+opt+"-all.jar")); } // TODO: fix this

            jars.addAll(Arrays.asList(OptionTree.getJarFiles(opt)));
          }
        }

        ////////////////////////////// Write the file
        File file = new File(fileName);
        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));

        out.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        out.println ("<!-- This file is generated using NeXtMidas "+Installation.NM_VERSION+" -->");
        out.println("<jnlp spec=\"1.0+\" href=\""+href+"\"");
        out.println("      codebase=\""+codebase+"\">");
        out.println();
        out.println("  <information>");
        out.println("    <title>"+title+"</title>");
        out.println("    <vendor>"+vendor+"</vendor>");
        out.println("    <homepage href=\""+homepage+"\"/>");
        out.println("    <description>"+descr+"</description>");
        out.println("    <description kind=\"short\">"+shortDescr+"</description>");
        out.println("    <offline-allowed/>");
        out.println("  </information>");
        out.println();
        out.println("  <!-- Set the permissions that we'll request from the host. -->");
        out.println("  <security>");
        out.println("    <all-permissions/>");
        out.println("  </security>");
        out.println();
        out.println("  <resources>");
        out.println("    <j2se version=\""+javaVer+"\"/>");
        for (File jarfile : jars) {
          if ("nxm-sys.jar".equals(jarfile.getName())) {
            out.println("    <jar href=\""+jarfile+"\" main=\"true\"/>");
          } else {
            out.println("    <jar href=\""+jarfile+"\"/>");
          }
        }
        out.println("    <property name=\""+propPrefix+"OPTS\" value=\""+opts+"\"/>");
        out.println("    <property name=\""+propPrefix+"NONATIVE\" value=\""+useNative+"\"/>");
        out.println("  </resources>");
        out.println();
        out.println("  <application-desc main-class=\"nxm.sys.lib.Webstart\">");
        out.println("    <argument>"+command+"</argument>");
        out.println("  </application-desc>");
        out.println("</jnlp>");

        out.close();
        M.info("JNLP file written to " + file.getCanonicalPath());
      } catch(Exception e) {
        throw new MidasException("Error writing file "+fileName, e);
      }
    }

    if (templateName != null) {
//TODO write table to file
      M.warning("WRITING OF TEMPLATE TO FILE NOT IMPLEMENTED");
    }

    waitingOnGui = false;
  }

  /** Write A JNLP file for the given command.
      @param cmdname  The NeXtMidas macro that will be run as a Java Web Start (JNLP) application.
      @param optStr   A String specifying the names of the option trees (other than SYS) that are
                      to be included in the application. Multiple option trees are to be separated
                      by the pipe character (<code>'|'</code>).
      @param dest     The directory where the generated files are to be stored.
      @param codebase The URL for the JNLP codebase. Codebase is also used as the base URL for all
                      relative URLs in href attributes.
      @param jnlphref The URL for the JNLP file (optional).
                      If null, the "href" attribute will be the set to "<codebase>/<cmdName>.jnlp".
                      If blank, the "href" attribute will NOT be written (useful when your jnlp
                      file is created dynamically AND arguments or properties change from
                      request to request (user to user)). Note, that Java Web Start needs href to
                      list your application in the Web Start application cache viewer.
  */
  private void doJnlp (String cmdname, String optStr, String dest, String codebase, String jnlphref) {
    //jph -- TBD separate READ and WRITE aux setting
    String auxStr = "HOME HOME|RAM|DAT"; // WRITE READ aux list

    msg("Generating JNLP files for " + cmdname+" in " + dest);

    // See if the dest directory exists, if it doesn't create it.
    File destFile = new File(dest);
    if ( !destFile.exists() ) {
      destFile.mkdir();
    }

    // Since 3.4.2 for BUG 2799
    String propPrefix="";
    if (Installation.ApplyBugFix.GENERATE_JNLP_PREPEND_JNLP_TO_PROPERTIES) {
        propPrefix = "jnlp.";
    }

    String jnlpName = cmdname + ".jnlp";
    FileName jnlpPath = new FileName (dest, cmdname, "jnlp");

    String href = (jnlphref==null)? codebase + "/" + jnlpName : jnlphref;
    boolean hasHref = href.trim().length() > 0; // if not blank after trimming

    String[] opts = StringUtil.cleanSplit ( optStr, "|" );

    msg("Writing " + jnlpPath );

    try {
      PrintWriter jnlp = new PrintWriter(new FileWriter(jnlpPath.getFullName()));

      jnlp.println ("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
      jnlp.println ("<!-- This file was autogenerated using NeXtMidas's GENERATE JNLP -->");
      jnlp.println ("<jnlp spec=\"1.0+\" codebase=\"" + codebase + "\"" + (!hasHref? ">": ""));
      if (hasHref) { jnlp.println ("      href=\"" + href + "\">"); }
      jnlp.println ("");
      jnlp.println ("  <information>");
      jnlp.println ("    <title>" + cmdname + "</title>");
      jnlp.println ("    <vendor>ICE</vendor>");
      jnlp.println ("    <homepage href=\"../htdocs/index.html\"/>");
      jnlp.println ("    <description>Run " + cmdname + "</description>");
      jnlp.println ("    <description kind=\"short\">"+" Run " + cmdname + " </description>");
      jnlp.println ("    <offline-allowed/>");
      jnlp.println ("  </information>");
      jnlp.println ("");
      jnlp.println ("  <security>");
      jnlp.println ("    <all-permissions/>");
      jnlp.println ("  </security>");
      jnlp.println ("");
      jnlp.println ("  <resources>");
      jnlp.println ("    <j2se version=\"1.8+\"/>");
      jnlp.println ("    <jar href=\"nxm-sys.jar\" main=\"true\"/>");
      boolean doNative = false; //jph -- tbd make this a switch     // TODO: fix this
      if (doNative) {
        jnlp.println ("    <nativelib href=\"nxm-sys-all.jar\"/>"); // TODO: fix this
      }
      for (String optname : opts) {

        // BEGIN Bug 2780 CHANGES (since 3.3.4)
        String[] jarFiles = getJarFiles(optname);  // get list of jar files in option tree
        if (jarFiles!=null) {
          for (String file : jarFiles) {             // add each jar file to the JNLP
            jnlp.println ("    <jar href=\""+file+"\" download=\"lazy\"/>");
          }
        }
       // END Bug 2780 CHANGES (since 3.3.4)

        if ( !"SYS".equalsIgnoreCase(optname)) {
          String jarname = "nxm-" + optname.toLowerCase() + ".jar";
          jnlp.println ("    <jar href=\"" + jarname + "\" download=\"lazy\"/>");
          String njarname = "nxm-" + optname.toLowerCase() + "-all.jar"; // TODO: fix this
          if (doNative) {
            jnlp.println ("    <nativelib href=\"" + njarname + "\"/>");
          }
        }
      }
      jnlp.println ("    <property name=\""+propPrefix+"NONATIVE\" value=\""+(!doNative)+"\"/>");
      jnlp.println ("    <property name=\""+propPrefix+"OPTS\" value=\"" + optStr + "\"/>" );
      jnlp.println ("    <property name=\""+propPrefix+"AUX\" value=\"" + auxStr + "\"/>" );
      jnlp.println ("  </resources>");
      jnlp.println ("");
      jnlp.println ("  <application-desc main-class=\"nxm.sys.lib.Webstart\">");
      jnlp.println ("    <argument>" + cmdname + "</argument>");
      jnlp.println ("  </application-desc>");
      jnlp.println ("</jnlp>");

      jnlp.close();

    } catch (IOException e) {
      throw new MidasException("GENERATE: Error writing JNLP file.", e);
    }
  }

  /** get a list of all the jar files in a given option tree
      @since NeXtMidas 3.3.4
   */
  private String[] getJarFiles (String opt) {
    ArrayList<String> filesList = new ArrayList<>();
    File f = new File(Shell.getOptPath(opt, null)+"/jars/");
    ArrayList<String> list = new ArrayList<>();
    try {
      list = new ArrayList<>(Arrays.asList(f.list()));
    } catch (NullPointerException npe) {
      return null;  // no jars in option tree
    }
    if (list.isEmpty())  return null;  // no jars in option tree

    for (Object file : list) {
      String str = (String)file;
      if (str.endsWith("jar")) {
        //System.out.println("Adding JAR file "+opt+"/"+str);
        filesList.add(str); // add the filename to the return array
      }
    }
    return filesList.toArray(new String[filesList.size()]);
  }

  /** Trys to load a manifest from disk, if not found, it creates a new blank manifest. */
  private Manifest loadManifest (FileName fname) {
    Manifest manifest = null;

    if (fname != null) {
      BaseFile bf = new BaseFile(M,fname);
      if (bf.exists()) {
        try {
          if (bf.open()) { // open file
            manifest = new Manifest(bf.getResource().getInputStream());
            bf.close();    // close file
          }
        } catch (Exception e) {
          M.printStackTrace("Unable to read manifest file '"+bf.getName()+"', using default manifest.", e);
        }
      }
    }
    if (manifest == null) {
      manifest = new Manifest();
      manifest.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0"); // REQUIRED!
    }
    return manifest;
  }

  /** Lists the jar files for a given option tree. */
  private File[] listJarFiles (String opt) {
    opt = opt.toLowerCase();
    File     dir  = new File(OptionTree.getOptPath(opt,"jars"));
    FnFilter filt = FileUtil.createFnFilter(null,"*.jar",null);
    return FileUtil.findFiles(dir,filt);
  }

  /** Copies the required jars files from nxm/<opt>/jars to the dest directory and signs them. */
  private void copyJarFiles (String opt, String dest, Table customKeystoreCfg, String customJarsignerOptions, boolean debug) {
    File[] jars = listJarFiles(opt);
    for (File file : jars) {
      String   fname   = file.getName();
      FileName inName  = new FileName(file.getPath(),true);
      FileName outName = new FileName(dest, fname, null, true);

      if (verbose) M.info("Copying & Signing "+opt+"'s "+fname+" to "+outName);
      FileUtil.copyBaseFile(M, inName, outName);
      JarFile.signJarFile(M, outName, opt, customKeystoreCfg, customJarsignerOptions, debug);
    }
  }

  /** Only used by doJarFile(..) */
  private String toClasspath (File[] jars) {
    StringBuilder jarFiles = new StringBuilder();

    for (File jar : jars) {
      jarFiles.append(jar.getName()).append(' ');
    }

    return jarFiles.toString();
  }

  /** Create JAR file for specified option tree.
      Since NeXtMidas 3.3.0, this can create OSGi bundles.
      Note: Info from option tree's cfg/manifest.mf takes precedence over gleaned/parsed
            info from version.txt or generated).
      @param opt    option tree
      @param dest   destination path (SHOULD be path terminated).
      @param filter filename filter (to put in jar file)
      @param type   type of jar task
      @param root   The root path to search for files in (null = use option tree path).
      @param os     The OS name for native libraries (null = use local OS name).
      @param arch   The architecture for native libraries (null = use local arch name).
      @param skipDirs Extra text added to default filter when skipping test and / or docs dirs
      @param customKeystoreCfg custom keystore configuration table (keys are:
               KEYSTOREFILE=, KEYSTOREPASS=, KEYALIAS=, KEYPASS=). since 3.5.1.
      @param customJarsignerOptions optional options to append to jarsigner command line before. since 3.5.1.
      @param customVersionQualifier custom version qualifier to use (null for none) since 3.5.1.
   */
  private void doJarFile (String opt, String dest, String filter, Generate.JarOption type,
                          String root, String os, String arch, String skipDirs,
                          Table customKeystoreCfg, String customJarsignerOptions, String customVersionQualifier) {
    opt = opt.toLowerCase();
    //This is the value used to separator imports and exports.
    String separator = ",";
    if( M.io.getOptions().indexOf("UseSemiColonsInGenerateManifest")>-1) {
      separator = ";";
    }
    if ((os   == null)   ||   os.isEmpty()) os   = Util.getOSName(null);
    if ((arch == null)   || arch.isEmpty()) arch = Util.getArchName(null);
    if ((root == null)   || root.isEmpty()) root = OptionTree.getOptPath(opt,null);
    if ((skipDirs==null) )                  skipDirs="";

    Manifest manifest    = loadManifest(new FileName("nxm."+opt+".cfg.manifest.mf"));
    Attributes mfMainAttributes = manifest.getMainAttributes();
    Table    optInfo     = getCachedOptInfo(opt);
    String   version     = optInfo.getS("VERSION");
    String   defFilter   = BuildProps.getPropFor(opt, "jnlp.file_filter", Shell.getCurrent());
    String   jarName;    // set below in the switch(type) { ... }
    String   jarFiles;   // set below in the switch(type) { ... }
    String   bundleVendor = mfMainAttributes.getValue(NAME_BUNDLE_VENDOR);
    if (bundleVendor == null && Arrays.binarySearch(BASELINE_OPTION_TREES, opt) != -1) {
      // since 3.5.2: use SYS's Bundle-Vendor for option trees in baseline when not specified
      Manifest sysManifest = loadManifest(new FileName("nxm.sys.cfg.manifest.mf"));
      bundleVendor = sysManifest.getMainAttributes().getValue(NAME_BUNDLE_VENDOR);
    }
    version = OptionTree.replaceVersionQualifier(version, customVersionQualifier); // since 3.5.1 for BUG 2485

    switch (type) {
      case JAR_STANDARD:
        jarName   = "nxm-"+opt+".jar";
        defFilter = BuildProps.getPropFor(opt, "jnlp.file_filter", Shell.getCurrent());
        jarFiles  = toClasspath(listJarFiles(opt));
        break;
      case BUNDLE_STANDARD: // since 3.3.0 for BUG 2484
        jarName   = "nxm."+opt+"_"+version+".jar";
        defFilter = BuildProps.getPropFor(opt, "jnlp.file_filter", Shell.getCurrent());
        jarFiles  = null;
        break;
      case JAR_NATIVE:
        manifest  = loadManifest(null);    // start with empty Manifest for native libraries jar/bundle
        mfMainAttributes = manifest.getMainAttributes();
        String savedImplVendor = mfMainAttributes.getValue(Name.IMPLEMENTATION_VENDOR);
        putIfNotPresent(mfMainAttributes, Name.IMPLEMENTATION_VENDOR, savedImplVendor); // since 3.3.4: propagate to native jar
        jarName   = "nxm-"+opt+"-native-"+os+"-"+arch+".jar";
        defFilter = "*"+Shell.getSharedObjExt();
        jarFiles  = null;
        break;
      case BUNDLE_NATIVE: // since 3.3.0 for BUG 2484
        manifest  = loadManifest(null);    // start with empty Manifest for native libraries jar/bundle
        mfMainAttributes = manifest.getMainAttributes();
        jarName   = "nxm."+opt+"."+os+"."+arch+"_"+version+".jar";
        defFilter = "*"+Shell.getSharedObjExt();
        jarFiles  = null;
        break;
      case BUNDLE_SOURCE:   // since 3.3.0 for BUG 2484
        manifest  = loadManifest(null);    // start with empty Manifest for source code bundle
        mfMainAttributes = manifest.getMainAttributes();
        jarName   = "nxm."+opt+".source"+"_"+version+".jar";
        defFilter = "*.java|*.h|*.c";
        jarFiles  = null;
        break;
      default:
        throw new IllegalArgumentException("Unknown JAR type '"+type+"'");
    }

    defFilter += skipDirs;  // since 3.3.4, Bug 2747

    FnFilter filt = FileUtil.createFnFilter(root, filter, defFilter);

    if (jarFiles != null) {
      // 2010-11-02: append jar files to existing "Class-Path" entry (if exists) for BUG 2277
      String manifestClasspath = mfMainAttributes.getValue(Name.CLASS_PATH);
      String classpath = (manifestClasspath==null)? jarFiles : manifestClasspath + " " + jarFiles;
      mfMainAttributes.put(Name.CLASS_PATH, classpath);
    }

    // add some manifest attributes / metadata (if not present)
    putIfNotPresent(mfMainAttributes, Name.IMPLEMENTATION_VERSION, version); // since 3.3.0
    putIfNotPresent(mfMainAttributes, "Permissions", "all-permissions");  // since 3.5.0: for Java 7u25 (warn), 7u51 (blocked) - BUG 2767
    putIfNotPresent(mfMainAttributes, "Codebase", "*");                   // since 3.5.0: for Java 7u25 (warn), 7u51 (blocked) - BUG 2767
    putIfNotPresent(mfMainAttributes, "Application-Name", "NeXtMidas");   // since 3.5.0

    // since 3.3.0 for BUG 2484
    if (type.isOSGIBundle()) {
      // add OSGi bundle specification version
      putIfNotPresent(mfMainAttributes, "Bundle-ManifestVersion", "2");
      putIfNotPresent(mfMainAttributes, "Bundle-Version", version);
      String symname = putIfNotPresent(mfMainAttributes, "Bundle-SymbolicName", "nxm."+opt); // +"; singleton:=true"?
      int iSemicolon = symname.indexOf(';');
      if (iSemicolon > 0) {
        symname = symname.substring(0, iSemicolon); // prune directives and attributes after bundle symbolic name
      }
      putIfNotPresent(mfMainAttributes, "Bundle-Description", optInfo.getS("DESCRIPTION"));
      putIfNotPresent(mfMainAttributes, "Bundle-Name", "NeXtMidas Option Tree (" + type +") - "+opt.toUpperCase());
      putIfNotPresent(mfMainAttributes, "Bundle-ActivationPolicy", "lazy");
      putIfNotPresent(mfMainAttributes, NAME_BUNDLE_VENDOR, bundleVendor); // since 3.5.4: set Bundle-Vendor if not null for all bundle types
      if ((type == Generate.JarOption.BUNDLE_NATIVE) || (type == Generate.JarOption.BUNDLE_SOURCE)) {
        String fragmentBSN; // BSN = bundle symbolic name
        if (type == Generate.JarOption.BUNDLE_NATIVE) {
          fragmentBSN = symname+"."+os+"."+arch;
          mfMainAttributes.putValue("Bundle-SymbolicName", fragmentBSN);
          // The fragment host SHOULD be the primary plugin's BSN
          mfMainAttributes.putValue("Fragment-Host", symname+"; bundle-version=\""+version+"\"");
          // Add the Eclipse Platform-Filter
          mfMainAttributes.putValue("Eclipse-PlatformFilter", "(&(osgi.os="+os+")(osgi.arch="+arch+"))");
        }
        else if (type == Generate.JarOption.BUNDLE_SOURCE) {
          fragmentBSN = symname+".source";
          mfMainAttributes.putValue("Bundle-SymbolicName", fragmentBSN);
          // Eclipse-SourceBundle: <bundle-id>;version=<version>;roots:="root1, root2"
          //   where the bundle-id and version indicate the bundle that the included source code corresponds to.
          mfMainAttributes.putValue("Eclipse-SourceBundle", symname + "; version=\""+version+"\"; roots:=\".\"");
        }
      }
    }

    FileName   jarFile   = new FileName(dest, jarName, null);
    File       dir       = new File(root);
    File[]     files     = FileUtil.findFiles(dir, filt);
    ArrayList<String> namesInJar = new ArrayList<>(files.length);   // name of file(s) to put in jar file (i.e. jar entry name)
    ArrayList<FileName>   fnames = new ArrayList<>(files.length); // actual filename(s) on disk to put in jar file
    StringBuilder nativeCode     = new StringBuilder(); // since 3.3.0 for OSGi native bundle
    StringBuilder exportPackages = new StringBuilder(); // since 3.3.0 for OSGi standard bundle

    if (!type.isNative()) {
      // need directory structure in jar, so entries can be search at that level
      namesInJar.add("nxm/");
      fnames.add(null);
      namesInJar.add("nxm/"+opt+"/");
      fnames.add(null);
      exportPackages.append("nxm.").append(opt);
    }

    for (File file : files) { // loop through files to add to JAR
      fnames.add(new FileName(file));
      if (!type.isNative()) {
        String  relDir = FileUtil.getRelativeDir(dir,file.getParentFile()).replace('\\', '/');
        boolean isDir  = file.isDirectory();
        if (isDir) {
          namesInJar.add("nxm/"+opt+"/"+relDir + file.getName() + "/");
          exportPackages.append(separator+" nxm.").append(opt).append(".").append(file.getName());
        } else {
          namesInJar.add("nxm/"+opt+"/"+relDir + file.getName());
        }
      }
      else { // since 3.3.0
        String filename = file.getName(); // get filename with extension
        if (filename.endsWith(".so") && filename.endsWith(Shell.getSharedObjExt())) {
          // Within Java Web Start and OSGi for System.loadLibrary(libname) to work the actual file
          // must be lib<libname>.so for UNIX platforms
          filename = "lib"+filename;
        } // else - don't need to prepend "lib" for Windows DLLs
        namesInJar.add(filename);
        if (type == Generate.JarOption.BUNDLE_NATIVE) { // since 3.3.0: add native libraries into native bundle metadata
          nativeCode.append(filename).append("; ");
        }
      }
    }

    if ("sys".equals(opt)) { // since 3.3.0: add $NMROOT/tools to generated SYS jar file
      if (!type.isNative()) {
        // need directory structure in jar, so entries can be search at that level
        namesInJar.add("tools/");
        fnames.add(null);

        File   toolsDir   = new File(Shell.getNmRoot()+"tools/");
        String fnFilter = (type == JarOption.BUNDLE_SOURCE)? "BuildProps*.java" : null; // <-- since 3.3.2
        File[] toolsFiles = FileUtil.findFiles(toolsDir, FileUtil.createFnFilter(fnFilter, "BuildProps*.class"));
        for (File file : toolsFiles) {
          fnames.add(new FileName(file));
          namesInJar.add("tools/"+file.getName()); // can have two class files for BuildProps.java or one just for src
        }

        // since 3.3.4, if sys/test was left out (via /notest), then add the unit test master class back in
        if (skipDirs.contains("|-test/") && !skipDirs.contains("|-test/junit")) {
          namesInJar.add("nxm/sys/test/junit/");
          fnames.add(null);

          File   testDir   = new File(Shell.getNmRoot()+"nxm/sys/test/junit/");
          File[] junitFiles = FileUtil.findFiles(testDir, FileUtil.createFnFilter(fnFilter, "NMTestCase*.class"));
          for (File file : junitFiles) {
            fnames.add(new FileName(file));
            namesInJar.add("nxm/sys/test/junit/"+file.getName());
          }
        }
      }
    }

    // since 3.3.0: more metadata for OSGi bundles (BUG 2484)
    if (type == Generate.JarOption.BUNDLE_STANDARD) {
      // specify Export-Package (calculated above)
      putIfNotPresent(mfMainAttributes, "Export-Package", exportPackages.toString());
      // This Eclipse bundle buddy policy allows NXM to load classes from the option trees
      if ("sys".equals(opt)) {
        mfMainAttributes.putValue("Eclipse-BuddyPolicy", "registered");
      } else {
        mfMainAttributes.putValue("Eclipse-RegisterBuddy", "nxm.sys");
        // import main SYS packages for option trees
        putIfNotPresent(mfMainAttributes, "Import-Package",
            "nxm.sys.inc" + separator
                + " nxm.sys.lib"  + separator
                + " nxm.sys.libg" + separator
                + " nxm.sys.libm" + separator
                + " nxm.sys.net"  + separator
                + " nxm.sys.test.junit; resolution:=optional");
      }
      putIfNotPresent(mfMainAttributes, "DynamicImport-Package", "*"); // cheat by dynamically importing every package

      if (MA.getState("/EMBEDJARS",true)) {
        // embed <opt>/jars/*.jar into this bundle
        File[] jarfiles = listJarFiles(opt);
        if (jarfiles.length > 0) {
          StringBuilder bundleClasspath = new StringBuilder("."); // include "." default
          for (File jarfile : jarfiles) {
            String jarfilename = jarfile.getName();
            if ("sys".equals(opt) && "osgi.core.jar".equals(jarfilename)) {
              continue; // ignore osgi.core.jar (OSGi frameworks should have this already)
            }
            fnames.add(new FileName(jarfile));
            namesInJar.add(jarfilename);
            bundleClasspath.append(',').append(jarfilename);
          }
          mfMainAttributes.putValue("Bundle-ClassPath", bundleClasspath.toString());
        }
      }
    }
    else if (type == Generate.JarOption.BUNDLE_NATIVE) { // since 3.3.0: specify NativeCode libraries bundle metadata
      if (nativeCode.length() > 0) {  // if there are any native libraries
        nativeCode.append("osname=").append(os).append("; ");
        nativeCode.append("processor=").append(arch);
        mfMainAttributes.putValue("Bundle-NativeCode", nativeCode.toString());
      }
    }

    boolean makeEmptyNativeJar = MA.getState("/ALLOWEMPTYNATIVEJAR", false); // since 3.5.4 for BUG 2902
    if (namesInJar.isEmpty() && makeEmptyNativeJar && (type == Generate.JarOption.JAR_NATIVE)) {
      // JAR must have 1 entry, so create a blank ".keepme" file for this case. This is common
      // for native JARs when an option tree has no native files. However, create the JAR anyhow
      // so that users don't need to modify their JNLP files if they add native files later on.
      fnames.add(null);
      namesInJar.add(".keepme");
    }
    else if (files.length == 0) { // otherwise SKIP creating JAR if no files (maybe option tree does not exists?)
      if (verbose) {
        M.info("Skipping "+type+" (no files found) for "+opt.toUpperCase());
      }
      return;
    }
    if (verbose) {
      M.info("Creating "+type+" file "+jarFile);
      if (debug) {
        M.info("  file filter      : "+filt);
        M.info("  number of files  : "+namesInJar.size());
      }
    }
    String[]   nameArray  = namesInJar.toArray(StringUtil.EMPTY_STRING_ARRAY);
    FileName[] fnameArray = fnames.toArray(new FileName[0]);
    boolean verbose = debug;
    JarFile.createJarFile(M, jarFile, manifest, nameArray, fnameArray, verbose, true,
                          opt, customKeystoreCfg, customJarsignerOptions, debug);
  }

  /** Put into specified attribute name/value into specified Manifest Attribute object if it does
      exists presently.
      @param mfAttributes manifest attributes to check/put attribute into (cannot be null)
      @param name name of attribute to check/put into manifest attributes (cannot be null)
      @param value value (when not null) to put into specified manifest attribute object
      @return current value in manifest attribute (either previous value or input value if attribute
              name was not present).
      @since NeXtMidas 3.3.0
   */
  private String putIfNotPresent (Attributes mfAttributes, String name, String value) {
    String currentVal = mfAttributes.getValue(name);
    if ((currentVal == null) && (value != null)) {
      mfAttributes.putValue(name, value);
      return value;
    }
    return currentVal;
  }

  /** same as {@link #putIfNotPresent(Attributes, String, String)}, but with Attributes.Name for name parameter
      instead of String. since 3.5.2
   */
  private String putIfNotPresent (Attributes mfAttributes, Name name, String value) {
    String currentVal = mfAttributes.getValue(name);
    if ((currentVal == null) && (value != null)) {
      mfAttributes.put(name, value);
      return value;
    }
    return currentVal;
  }

  /** Determine option tree info (cached to prevent multiple warnings).
      @param opt option tree name
      @return Table with version string to use in "_VERSION_" key, with other keys/values
              as documented in {@link OptionTree#getInfo(String, Midas, boolean, boolean)}.
      @see OptionTree#getInfo(String, Midas, boolean, boolean)
   */
  private synchronized Table getCachedOptInfo (String opt) {
    // get option tree info (including version info)
    Table optInfo = optInfoCache.get(opt); // using cached optInfo entry prevents multiple warnings
    if (optInfo == null) {
      optInfo = OptionTree.getInfo(opt, M, verbose, doWarn);
      optInfoCache.put(opt, optInfo);
    }

    return optInfo;
  }

  /** Display info message of specified text if either debug or verbose are true. */
  private void msg (String txt) {
    if (debug || verbose) M.info (txt);
  }
}
