package tools;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.jar.JarOutputStream;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;

/** This class is a replacement for the <code>nmmake</code> script that is able to build NeXtMidas
    from scratch (or nearly so). <br>
    <br>
    The reasons for replacing the <code>nmmake</code> script with this class include:
    <ul>
      <li>This class is platform-independent whereas a version of
          <code>nmmake</code> had to be written for each platform.</li>
      <li>This class reads any configurable build parameters from
          <code>$NMROOT/nxm/sys/cfg/build.props</code> whereas using the
          <code>nmmake</code> script required that <u>every</u> parameter
          be hard-coded into <u>each</u> of the individual script files.</li>
      <li>This class passes the majority of the compilation work to NeXtMidas.
          Only the minimum number of classes required to start the build process
          in NeXtMidas are compiled here, everything else is left to NeXtMidas
          to do. This maximizes the consistency between a build run
          (entirely) in NeXtMidas and one initiated from the command line.</li>
    </ul>
    <br>
    <b>Implementation note:</b> This class does not require access to any classes outside of the
    <code>java.*</code> package other than the BuildProps and NmSaveFile classes. This permits the
    class to be compiled and run independent of other NeXtMidas classes.
    @author  J. Czechowski IV
    @since   NeXtMidas 1.9.2
 */
public final class NmMake {
  /** one or more white spaces (space,tab,return,newline) regular expression (compiled). Since NeXtMidas 3.6.0 */
  static final Pattern WHITESPACE_REGEX = Pattern.compile("\\s+");

  /** Filters for Jar files. */
  private static final FilenameFilter JAR_FILTER = new FilenameFilter() {
    public boolean accept(File dir, String name) {
      return name.endsWith(".jar");
    }
  };

  private static JavaCompiler compiler = null;
  /** @return JavaCompiler instance
      @since NeXtMidas 3.3.0 use JDK6 API
   */
  private static synchronized JavaCompiler getJavaCompiler () {
    if (compiler == null) {
      compiler = ToolProvider.getSystemJavaCompiler();
      if (compiler == null) {
        throw new RuntimeException("Unable to get Java compiler! Not using JDK?");
      }
    }
    return compiler;
  }

  /** This will compile the boot classes. During compilation it will run
      <code>javac</code> with the flags defined as <code>nmmake.javacflags</code>
      in <code>build.props</code> to compile the classes defined as
      <code>nmmake.bootclasses</code> in <code>build.props</code>.
   */
  private static void compileBootClasses () {
    // note regex "[ \n\t]+" is used to split based on white space regions.
//  final String[] javacFlags  = getPropArray("nmmake.javacflags");      // prior to 3.6.0
    final String[] javacFlags  = getSysPropAsArray("nmmake.javacflags"); // since 3.6.0 as part of BUG 2955
    final String[] bootClasses = getPropArray("nmmake.bootclasses");

    ArrayList<String> params = new ArrayList<String>();
    params.addAll(Arrays.asList(javacFlags));

    for (String bootClass : bootClasses) {
      final File file = new File(bootClass);

      if (file.isDirectory()) {
        final File[] files = file.listFiles();

        for (File aFile : files) {
          if (aFile.getName().endsWith(".java")) {
           params.add(aFile.getAbsolutePath());
          }
        }
      }
      else {
        params.add(file.getAbsolutePath());
      }
    }

    final String[] args = params.toArray(new String[params.size()]);

    int compileResult = getJavaCompiler().run(null, null, null, args);  // since 3.3.0: use JDK6 API
    if (compileResult != 0) {
      System.err.println("WARN: unable to compile boot classes: "+compileResult);
    }
  }

  /** This will run a NeXtMidas command. This is the same as calling Shell(null,command,false)
      except that reflection is used when calling it to enable this class to be compiled and
      started before Shell has been compiled.
      @param command The NeXtMidas command to run.
   */
  private static void runNM (String command) {
    // Must use reflection to access nxm.sys.lib.Shell and nxm.sys.inc.ShellParent since they will
    // not be compiled prior to running this class.
    try {
      Class<?> shell  = Class.forName("nxm.sys.lib.Shell");
      Class<?> parent = Class.forName("nxm.sys.inc.ShellParent");
      Method runNew = shell.getMethod("runNew", new Class<?>[] { parent, String.class, boolean.class });

      runNew.invoke(null, new Object[] { null, command, Boolean.FALSE });

    } catch (Exception exception) {
      System.err.println("Unable to run NeXtMidas command '" + command + "': " + exception);
      exception.printStackTrace();
    }
  }

  /** This will get a <code>SYS</code> path property and split it to an array of parameters.
      Calls <code>BuildProps.getSysPath(name)</code>.
      @param name The name of the property to get.
      @return The property array or <code>null</code> if the property does not exist.
   */
  private static String[] getPropArray (String name) {
    String value = BuildProps.getSysPath(name);

    return (value == null)? null: WHITESPACE_REGEX.split(value);
  }

  /** This will get a <code>SYS</code> property and split it to an array of parameters.
      Calls <code>BuildProps.getSysPath(name)</code>.
      @param name The name of the property to get.
      @return The property array or <code>null</code> if the property does not exist.
      @since NeXtMidas 3.6.0
   */
  private static String[] getSysPropAsArray (String name) {
    String value = BuildProps.getPropsFor("SYS").getProperty(name, BuildProps.PREPEND_SUFFIXES, BuildProps.APPEND_SUFFIXES);

    return (value == null)? null: WHITESPACE_REGEX.split(value);
  }

  /** This will load any additional properties used when running NmMake from build.props so they
      don't need to be included on the command line.
      @see #getPropArray(String)
   */
  private static void loadNmMakeProperties () {
    try {
      final String[] props = getPropArray("nmmake.properties");

      for (String prop : props) {
        final String[] definition = prop.split("=");

        System.setProperty(definition[0].trim(), definition[1].trim());
      }
    } catch (Exception exception) {
      System.err.println("Error in loading system properties defined as 'nmmake.properties' in 'nxm/sys/build.props': " + exception);
    }
  }

  /** Converts an array of strings into a single string separated by spaces.
      @param strs The array of strings.
      @return A single string or <code>null</code> if <code>strs</code> is <code>null</code>.
   */
  private static String toString (String[] strs) {
    if (strs != null) {
      StringBuilder sb = new StringBuilder();
      boolean initial = true;
      for (String str : strs) {
        if (initial) { initial = false; } // skip adding space separator on initial String
        else         { sb.append(' ');  } // add space separator
        sb.append(str);
      } // end for loop
      return sb.toString();
    }
    return null;
  }

  /** This will list all known option trees under $NMROOT/nxm directory.
      @return All known option trees.
   */
  private static String[] getAllOpts () {
    final File   nxm   = new File(BuildProps.getSysPath("nmroot") + File.separator + "nxm");
    final File[] inNxm = nxm.listFiles();
    final ArrayList<String> opts  = new ArrayList<String>();

    for (File file : inNxm) {
      if (file.isDirectory()) {
        opts.add(file.getName());
      }
    }

    return opts.toArray(new String[opts.size()]);
  }

  /** This will clean out option tree(s) under the $NMROOT/nxm directory.
      This will go through each of the areas specified in "nmmake.clean.codeareas" in "build.props"
      and delete all files which end in one of the extensions listed as "nmmake.clean.types" in "build.props".
      Note that the extensions should be listed with a period before the extension.
      @param opt The name of the option tree to clean for (must be lower case).
   */
  private static void clean (final String opt) {
    final FilenameFilter filter = makeFilenameFilter(BuildProps.getSysProp("nmmake.clean.types"));

    if (opt.equalsIgnoreCase("all")) {
      /* Clean each option tree. */
      final String[] opts = getAllOpts();
      for (String optionTree : opts) {
        clean(optionTree);
      }

      /* Clean the Misc. files. */
      final String[] toClean  = getPropArray("nmmake.clean.locations");
      final String[] toRemove = getPropArray("nmmake.clean.remove");
      FilenameFilter otherFilter = makeFilenameFilter(BuildProps.getSysProp("nmmake.clean.locations.types")); // since 3.7.0 for BUG 2989

      System.out.println("Cleaning Misc Files");
      for (String toCleanFile : toClean) {
//      clean(false, new File(toCleanFile), filter);      // prior to 3.7.0
        clean(false, new File(toCleanFile), otherFilter); // since 3.7.0 for BUG 2989
      }

      for (String toRemoveFile : toRemove) {
        final File file = new File(toRemoveFile);

        if (!recursiveDelete(file)) {
          System.err.println("ERROR: Unable to delete " + file);
        }
      }
    } else {
      final String optUpperCase = opt.toUpperCase();
      System.out.println("Cleaning for " + optUpperCase);

      // 1. lookup option tree path in OPT table (since 3.6.0 for BUG 2955)
      String optPath = null;
      try {
        optPath = NmSaveFile.findKey(null, "OPT." + optUpperCase);
      } catch (FileNotFoundException fnfe) {
        // ignore and continue (since there are no nmsf_*.msf file initially on fresh nmstart)
      } catch (IOException e) {
        System.err.println("WARN: Problem reading NMSF file to get [" + optUpperCase + "] option tree path: " + e);
      }

      // 2. look for option tree under $NMROOT/nxm
      if (optPath == null) {
        optPath = BuildProps.getSysPath("nmroot") + File.separator + "nxm" + File.separator + opt + File.separator;
      }

      File dir = new File(optPath);
      if (!dir.isDirectory()) { // since 3.7.0: if optPath is not existing dir throw exception (BUG 2955)
        throw new RuntimeException("ERROR: [" + optUpperCase + "] option tree does not exist at directory: " + optPath);
      }
      System.out.println("Cleaning " + optUpperCase + " under " + optPath);

      String[] areas = getPropArray("nmmake.clean.codeareas"); // since 3.1.2 (BUG 2588)
      for (String area : areas)  {
        clean(true, new File(optPath + area), filter); // since 3.7.0, clean recursively (BUG 2955)
      }
    }
  }

  /** This will clean out a directory. This will delete all files which end in one of the extensions
      listed as "nmmake.clean.types" in "build.props". Note that the extensions should be listed
      with a period before the extension.
      @param recursive Indicates if it should recursively enter any sub-directories and clean them.
      @param dir       The directory to clean.
      @param filter    The filter which indicates which files/directories to remove.
   */
  private static void clean (boolean recursive, File dir, FilenameFilter filter) {
    final String genFiles = BuildProps.getSysProp("nmmake.clean.genfileslst");

    if (dir.exists()) {
      final File[] files = dir.listFiles();

      for (File file : files) {
        /* Enter sub-directories as needed. */
        if (recursive && file.isDirectory()) {
          clean(recursive, file, filter);
        }

        /* If the file lists auto-generated files, delete each of the files it lists. */
        if (file.getName().equals(genFiles)) {
          try {
            BufferedReader in = new BufferedReader(new FileReader(file));
            String genFile;

            while ((genFile = in.readLine()) != null) {
              if (!(new File(file.getParentFile(), genFile)).delete()) {
                System.err.println("ERROR: Unable to delete " + file);
              }
            }
            in.close();
          } catch (Exception e) {
            System.err.println("ERROR: Unable to delete files listed in " + file + ": " + e);
          }
        }

        /* If it matches delete it. */
        if (filter.accept(dir, file.getName())) {
          if (!recursiveDelete(file)) {
            System.err.println("ERROR: Unable to delete " + file);
          }
        }
      } // end for loop
    }
  }

  /** Makes a filter that accepts files that match any of the specified (space-separated) types.
      Example:
      <pre>
        makeFilenameFilter("*.class Foo*")  --&gt; matches any files that end
                                                in '.class' or start with 'Foo'
      </pre>
      @param accepts The types the filter accepts.
      @return The filter.
   */
  private static FilenameFilter makeFilenameFilter (final String accepts) {
    final Pattern acceptPattern = Pattern.compile("("
                                + accepts.replaceAll("\\s",")|(")/*' ' separates types  */
                                         .replaceAll("[.]","[.]")/*'.' matches '.'      */
                                         .replaceAll("[*]",".*") /*'*' matches any chars*/
                                         .replaceAll("[?]",".")  /*'?' matches one char */
                                + ")");
    return new FilenameFilter () {
      public boolean accept(File dir, String name) {
        return acceptPattern.matcher(name).matches();
      }
    };
  }

  /** Makes a filter that accepts files that match any of the specified (space-separated) types or
      any of the types recognized by the basic filter. This is the same as
      <code>makeFilenameFilter(basic,makeFilenameFilter(accepts))</code>.
      @param basic   The basic filter.
      @param accepts The types the filter accepts.
      @return The filter.
   */
  private static FilenameFilter makeFilenameFilter (FilenameFilter basic, String accepts) {
    return makeFilenameFilter(basic,makeFilenameFilter(accepts));
  }

  /** Makes a filter that accepts files that match any file matched by either of the two file name filters provided.
      @param basic   The basic filter.
      @param adv     The "advanced" filter to check against if it does not match the basic one.
      @return The filter.
   */
  private static FilenameFilter makeFilenameFilter (final FilenameFilter basic, final FilenameFilter adv) {
    return new FilenameFilter() {
      public boolean accept(File dir, String name) {
        return basic.accept(dir, name) || adv.accept(dir, name);
      }
    };
  }

  /** This will recursively delete a directory and everything inside it.
      @param dir The directory to delete.
      @return <code>true</code> if successful and <code>false</code> if not.
   */
  private static boolean recursiveDelete (File dir) {
    if (dir.exists()) {
      if (dir.isDirectory()) {
        boolean success = true;

        final File[] files = dir.listFiles();
        for (File file : files) {
          success = success && recursiveDelete(file);
        }

        return success && dir.delete();

      } else { /* is file */
        return dir.delete();
      }
    } else {
      return true;
    }
  }

  /** This will copy the Jar files from a specific option tree (that is under $NMROOT/nxm/) to "$NMROOT/jars".
      @param opt The name of the option tree to copy the Jars from.
   */
  private static final void copyJarFiles (String opt) {
    if (opt.equalsIgnoreCase("all")) {
      /* Copy for each option tree. */
      String[] opts = getAllOpts();
      for (String optionTree : opts) {
        copyJarFiles(optionTree);
      }
    } else {
      File sourceDir = new File(BuildProps.getSysPath("nmroot") + File.separator + "nxm"
                              + File.separator + opt + File.separator + "jars");
      if (sourceDir.exists() && sourceDir.isDirectory()) {
        File[] optTreeJars = sourceDir.listFiles(JAR_FILTER);
        String destPath    = BuildProps.getSysPath("nmroot") + File.separator + "jars" + File.separator;

        for (File optionTreeJar : optTreeJars) {
          final File destFile = new File(destPath + optionTreeJar.getName());

          if (!destFile.exists()) {
            if (!copyFile(optionTreeJar, destFile)) {
              System.err.println("ERROR: Unable to copy " + optionTreeJar + " to " + destFile);
            }
          }
        } // end for loop
      }
    }
  }

  /** Close specified Closeable I/O stream object (if not null).
      Copied from IOResource.java
      @param closeable object to close (null will be ignored)
      @return true if there was no exceptions, false otherwise
      @since NeXtMidas 3.7.0
   */
  private static boolean closeIO (Closeable closeable) {
    try {
      if (closeable != null) closeable.close();
    } catch (IOException e) {
      return false; // Ignore the exception and return false
    }
    return true;
  }

  /** Simply copies a file.
      @param source       The existing file.
      @param destination  The location where the copy should go.
      @return <code>true</code> if successful, <code>false</code> otherwise.
   */
  private static boolean copyFile (File source, File destination) {
    FileChannel src  = null;
    FileChannel dest = null;
    FileInputStream  fis = null;
    FileOutputStream fos = null;

    try {
      fis  = new FileInputStream(source);
      src = fis.getChannel();
      fos  = new FileOutputStream(destination);
      dest = fos.getChannel();

      long srcSize = src.size();
      long bytes = src.transferTo(0, srcSize, dest);
      if (bytes != srcSize) { // since 3.7.0, print warning if number bytes written to dest is not same as source file
        System.err.println("WARN: only able to copy " + bytes + " of " + srcSize + " bytes from " + source + " to " + destination);
      }
      src.close();
      dest.close();

      return true;
    }
    catch (IOException e) {
      return false;
    }
    finally {
      closeIO(fis);
      closeIO(fos);
    }
  }

  /** This will package up an option tree (or the entire NeXtMidas tree) for distribution to others.
      @param includeJars  Indicates if Jar files (specified as <code>nmmake.dist.skiptypes</code> in
                          <code>build.props</code>) should be skipped over.
      @param archiveName  The name of the archive (the extension <code>.zip</code> or
                          <code>.jar</code> indicates the type.
      @param opt          The name of the option tree to archive (or <code>"nxm"</code>,
                          <code>"all"</code>, <code>"nmroot"</code>, <code>"nextmidas"</code> for
                          the entire tree), if <code>opt="SYS"</code> and
                          <code>forInstaller=true</code> special auxiliary files are included for
                          the baseline.
   */
  private static final void makeInst (boolean includeJars, String  archiveName, String  opt) {
    makeDist(includeJars, true, archiveName, opt, "");
  }

  /** This will package up an option tree (or the entire NeXtMidas tree) for distribution to others.
      <br><br>
      The main portion of this method that adds the files to a zip/jar is
      based on a code sample provided by Sun
      (http://java.sun.com/developer/technicalArticles/Programming/compression/).
      @param includeJars Indicates if Jar files (specified as "nmmake.dist.skiptypes" in
                         "build.props") should be skipped over.
      @param archiveName The name of the archive (type is indicated by a ".zip" or ".jar" extension)
      @param opt         The name of the option tree to archive (or "nxm"/"all" for the entire tree).
   */
  private static final void makeDist (boolean includeJars, String  archiveName, String  opt) {
    String fileRoot = archiveName.substring(0, archiveName.length() - 4) + File.separator;

    makeDist(includeJars, false, archiveName, opt, fileRoot);
  }

  /** This will package up an option tree (or the entire NeXtMidas tree) for distribution to others.
      <br><br>
      The main portion of this method that adds the files to a zip/jar is
      based on a code sample provided by Sun
      (http://java.sun.com/developer/technicalArticles/Programming/compression/).
      @param includeJars  Indicates if Jar files (specified as <code>nmmake.dist.skiptypes</code> in
                          <code>build.props</code>) should be skipped over.
      @param forInstaller Indicates if the files are to be passed to the installer.
      @param archiveName  The name of the archive (the extension <code>.zip</code> or
                          <code>.jar</code> indicates the type.
      @param opt          The name of the option tree to archive (or <code>"nxm"</code>,
                          <code>"all"</code>, <code>"nmroot"</code>, <code>"nextmidas"</code> for
                          the entire tree), if <code>opt="SYS"</code> and
                          <code>forInstaller=true</code> special auxiliary files are included for
                          the baseline.
   */
  private static final void makeDist (boolean includeJars, boolean forInstaller, String  archiveName,
                                      String  opt, String fileRoot) {
    File nmroot = new File(BuildProps.getSysPath("nmroot"));
    File[] baseDirs;

    FilenameFilter skipFilter = makeFilenameFilter(BuildProps.getSysProp("nmmake.zip.skiptypes"));
    FilenameFilter specialFilter;

    opt = opt.toLowerCase();

    if (includeJars) {
      specialFilter = makeFilenameFilter(archiveName);
    } else {
      specialFilter = makeFilenameFilter(BuildProps.getSysProp("nmmake.dist.skiptypes") + " " + archiveName);
    }

    if (opt.equals("nxm") || opt.equals("all") || opt.equals("nmroot") || opt.equals("nextmidas")) {
      if (!forInstaller) {
        baseDirs = new File[] { new File(BuildProps.getSysPath("nmroot")) };
      } else {
        System.err.println("ERROR: Use 'SYS' not '" + opt + "' to make installer file for SYS.");
        return;
      }
    }
    else if (opt.equals("sys") && forInstaller) {
      File[] files  = nmroot.listFiles(new FilenameFilter() {
        public boolean accept(File dir, String name) {
          return !name.equals("nxm");
        }
      });

      baseDirs = new File[files.length + 1];
      for (int i = 0; i < files.length; i++) {
        baseDirs[i] = files[i];
      }

      baseDirs[baseDirs.length-1] = new File(BuildProps.getSysPath("nmroot")
                                           + File.separator + "nxm"
                                           + File.separator + "sys");

    } else {
      File dir = new File(nmroot, "nxm" + File.separator + opt);

      if (dir.exists() && dir.isDirectory()) {
        // Use this directory
      } else {
        String path = null;

        try {
          path = NmSaveFile.findKey(null, "OPT."+opt.toUpperCase());
        } catch (IOException e) {
          throw new RuntimeException("ERROR: Problem reading NMSF file "+e, e);
        }

        if (path == null) {
          throw new RuntimeException("ERROR: Can not find location of OPT="+opt.toUpperCase());
        } else {
          dir = new File(path);
        }
      }

      baseDirs = new File[] { dir };
    }

    try {
      FileOutputStream     dest = new FileOutputStream(archiveName);
      BufferedOutputStream bos  = new BufferedOutputStream(dest);
      ZipOutputStream      out;

      if (archiveName.endsWith(".jar")) { out = new JarOutputStream(bos); }
      else                              { out = new ZipOutputStream(bos); }

      for (File baseDir : baseDirs) {
        File root = baseDir;
        if (root.getAbsolutePath().startsWith(nmroot.getAbsolutePath())) {
          root = nmroot;
        }

        addToDistZip(out, baseDir, baseDir, skipFilter, specialFilter, fileRoot); // 2010-08-02 TODO: this might be an error? (maybe 3rd param should be root?)
      }
      out.close();

    } catch (Exception e) {
      System.err.println("ERROR: Unable to create " + archiveName + ": " + e);
      e.printStackTrace();
    }
  }

  /** This will help package up an option tree by adding files to a zip archive. <br>
      <br>
      The main portion of this method that adds the files to a zip/jar is
      based on a code sample provided by Sun
      (http://java.sun.com/developer/technicalArticles/Programming/compression/).
      @param out           The archive to write to.
      @param file          The file to add to the archive (if the file is a
                           directory all of its contents are added too).
      @param skipFilter    Indicates that the file should be skipped if this filter accepts it.
      @param specialFilter Indicates that the file should be skipped if this filter accepts it
                           and that such should be reported to the console.
      @param fileRoot      The file root to append to the file in place of "$NMROOT/" (must end
                           with a file separator).
   */
  private static void addToDistZip (ZipOutputStream out, File file, File root,
                                    FilenameFilter skipFilter, FilenameFilter specialFilter,
                                    String fileRoot) throws IOException {
    String rootPath = root.getAbsolutePath();
    String filePath = file.getAbsolutePath();
    int    length   = rootPath.length();
    String fileName = filePath.substring(Math.min(filePath.length(), length));
    byte[] buffer   = new byte[1024];

    if (fileName.startsWith(File.separator)) {
      fileName = fileName.substring(1);
    }

    if (skipFilter.accept(file.getParentFile(), file.getName())) {
      /* Skip the file. */
    }
    else if (specialFilter.accept(file.getParentFile(), file.getName())) {
      skipFile(fileName, "COTS/Binary");
    }
    else if (file.isDirectory()) {
      final File[] files = file.listFiles();

      for (File fileInDir : files) {
        if (isSymbolicLink(file, fileInDir.getName())) {
          /* Skip symbolic links. */
          skipFile(fileName, fileInDir.getName(), "Symbolic Link");
        }
        else {
          /* Determine if the file should be skipped (deleted) or "cleaned." */
          String[] delFiles  = getPropArray("nmmake.clean.remove");
          String[] cleanDirs = getPropArray("nmmake.clean.locations");

          boolean isDelFile   = false;
          boolean isCleanFile = false;

          for (String delFile : delFiles) {
            isDelFile = isDelFile || fileInDir.equals(new File(delFile));
          }

          for (String cleanDir : cleanDirs) {
            isCleanFile = isCleanFile || fileInDir.equals(new File(cleanDir));
          }

          /* Skip any necessary files. */
          if (isDelFile) {
            skipFile(fileName, fileInDir.getName(), "Temp Directory");
          }
          else if (isCleanFile) {
//          FilenameFilter filter = makeFilenameFilter(skipFilter, BuildProps.getSysProp("nmmake.clean.types"));           // prior to 3.7.0
            FilenameFilter filter = makeFilenameFilter(skipFilter, BuildProps.getSysProp("nmmake.clean.locations.types")); // since 3.7.0 for BUG 2989
            addToDistZip(out, fileInDir, root, filter, specialFilter, fileRoot);
          }
          else {
            addToDistZip(out, fileInDir, root, skipFilter, specialFilter, fileRoot);
          }
        }
      }
    }
    else {
      BufferedInputStream in = new BufferedInputStream(new FileInputStream(file), buffer.length);
      out.putNextEntry(new ZipEntry(fileRoot + fileName));

      int byteCount;
      while ((byteCount = in.read(buffer, 0, buffer.length)) != -1) {
        out.write(buffer, 0, byteCount);
      }
      in.close();
    }
  }

  /** Prints out a message indicating that a file is being skipped while making
      a distribution jar/zip. Used only by the {@link #addToDistZip} method.
      @param file   The file being skipped.
      @param reason The reason for skipping it.
   */
  private static void skipFile (String file, String reason) {
    StringBuilder sb = new StringBuilder(file);
    for (int i = file.length(); i < 40; i++) {
      sb.append(' ');  // right pad with spaces if less than 40 chars
    }
    System.out.println("  Skipping: " + sb + " (" + reason + ")");
  }

  /** Prints out a message indicating that a file is being skipped while making
      a distribution jar/zip. Used only by the {@link #addToDistZip} method.
      @param dir    The directory the file is in.
      @param file   The file being skipped
      @param reason The reason for skipping it.
   */
  private static void skipFile (String dir, String file, String reason) {
    skipFile(dir + File.separator + file, reason);
  }

  /** This will determine if a file is a symbolic link. This determination is independent of whether
      or not the directory of the file (<code>dir</code>) contains any symbolic links.
      @param dir  The directory the file is in.
      @param name The name of the file.
      @return <code>true</code> if <code><i>dir</i>/<i>name</i></code> is a symbolic link and
              <code>false</code> if it is not. <code>false</code> will also be returned if the file
              does not exist yet and <code>true</code> will be returned if Java is unable to
              determine the canonical path of the directory or the file.
   */
  private static boolean isSymbolicLink (final File dir, final String name) {
    final File file = new File(dir, name);

    if (dir.exists() && file.exists()) {
      try {
        File parent = dir.getCanonicalFile();
        File child  = file.getCanonicalFile();

        return !child.getAbsolutePath().equals(parent.getAbsolutePath() + File.separator + child.getName());
      } catch (IOException e) {
        e.printStackTrace();
        return true;
      }
    } else {
      return false;
    }
  }

  /** Runs the make tool.
      @param args Any command line arguments.
   */
  public static void main (String[] args) {
    final String allTargets = "all|boot|clean|dist|doclink|gen|jar|zip";

    if ((args == null) || (args.length < 1) || args[0].equals("")) {
      System.out.println("ERROR: No build target specified.");
      System.out.println("Targets should be one of '" + allTargets + "'.");
    }
    else if ((args.length == 1) && (args[0].indexOf(' ') >= 0)) {
      /* If the first argument is really multiple arguments in one, split it up. */
      main(WHITESPACE_REGEX.split(args[0]));
    }
    else if (args[0].equalsIgnoreCase("make")) {
      /* If the first argument is make remove it and start over. */
      String[] newArgs = new String[args.length - 1];
      System.arraycopy(args, 1, newArgs, 0, newArgs.length);

      main(newArgs);
    }
    else {
      loadNmMakeProperties(); /* Load the properties for use when running nm */

      String target = args[0].toLowerCase();
      String opt    = "sys";

      if ((args.length >= 2) && (!args[1].trim().equals(""))) {
        opt = args[1].toLowerCase();
      }

      /* ---------- THIS NEXT SECTION MIMICS SHELL SCRIPT NMMAKE ---------- */
      if (target.equals("tools")) {
        /* Tools build by shell script - nothing left to do. */
      }
      else if (target.equals("boot")) {
        System.out.println("Compiling boot classes");
        copyJarFiles("sys");
        compileBootClasses();
      }
      else if (target.equals("zip") || target.equals("jar") || target.equals("dist") || target.equals("inst")) {
        String fileName = null;

        if (args.length == 3) {
          fileName = args[2];
        } else if (args.length == 2) {
          SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
          fileName = args[1].toLowerCase() + "_" + dateFormat.format(new Date());
        }

        if (fileName == null) {
          if (!target.equals("inst")) {
            System.err.println("ERROR: Wrong number of arguments: should be in "
                             + " the form of '" + target + " nxm nxmXXX'.");
          } else {
            System.err.println("ERROR: Wrong number of arguments: should be in "
                             + " the form of '" + target + " opt jarfile'.");
          }
        } else {
          boolean isDistSet = target.equals("dist");
          boolean isInstSet = target.equals("inst");

          if (!(fileName.endsWith(".jar") || fileName.endsWith(".zip"))) {
            if (isDistSet) {
              fileName = fileName + ".zip";
            } else if (isInstSet) {
              fileName = fileName + ".jar";
            } else {
              fileName = fileName + "." + target;
            }
          }

          System.out.println("Making distribution set " + fileName);
          if (!isInstSet) {
            makeDist(!isDistSet, fileName, args[1]);
          } else {
            makeInst(!isDistSet, fileName, args[1]);
          }
        }
      }
      else if (target.equals("clean")) {
        clean(opt);
      }
      else if (target.equals("doclink")) {
        System.err.println("DEPRECATED: 'nm make doclink' is deprecated. This command never "
                         + "worked correctly on all platforms and has been removed.");
        System.err.println("On Unix, you can use the following command to create the link:");
        System.err.println("    ln -s ${JAVAHOME}/docs ${NMROOT}/htdocs/jdkdocs");
      }
      else { /* By default pass it into the MAKE macro. */
        final String bldSwitches = BuildProps.getSysProp("nmmake.bldswitches");

        main(new String[] { "boot" });

        System.out.println("Copying Jar Files");
        if (!"sys".equals(opt)) copyJarFiles(opt); // since 3.3.3: for other option trees only, since "sys" is done in main(boot) above

        final String cmd = "MAKE" + bldSwitches + " " + toString(args);
        System.out.println("Running '" + cmd + "'");
        runNM(cmd);
      }
    }
  }
}
