package nxm.sys.intr;

import java.io.File;
import java.io.FileFilter;
import java.util.Arrays;

import nxm.sys.lib.FileName;
import nxm.sys.lib.FileUtil;
import nxm.sys.lib.Foreign;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.OptionTree;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;

/** Sets the option path assignments for software trees.

    @author Jeff Schoen / J. Czechowski IV / Jay Hallam / Neon Ngo
*/
public class Option extends Intrinsic {
  /** the name of the NeXtMidas commands dictionary */
  public static final String COMMAND_FILE_NAME = OptionTree.COMMAND_FILE_NAME;

  private boolean debug = false;
  private boolean IsALink;  // Initialized in process to avoid problems when running
  private boolean InNmRoot;  // from a macro

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

  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  public int process() {
    IsALink  = false;
    InNmRoot = false;

    // Get parameters
    debug = MA.getState("/DEBUG", debug);
    String optName = MA.getU("NAME");
    String pathInOptTable = MR.getString("OPT."+optName.toUpperCase());
    String physPath = MA.getCS("PATH", null);
    boolean createOption = MA.getState("/CREATE", false);
    boolean deleteOption = MA.getState("/DELETE", false);
    String linkPath = MA.getCS("/LINK",null);
    boolean restartRequired = false; // for BUG 1795

    debug("OPTION: INPUTS - optName = [" + optName+"]  /create="+createOption);
    debug("                 phys path '"+physPath+"'");
    debug("                 link path '"+linkPath+"'");

    if (createOption && deleteOption) M.warning("/CREATE and /DELETE switches both present. Using /CREATE.");

    // Validate the physical path and/or link path inputs
    if (linkPath == null || linkPath.equals("")) {
      IsALink = false;  // Not a link
      if (createOption) {
        if (isValidPath(physPath) ) {
          physPath = FileUtil.terminatePath(physPath); // NTN: make sure it ends with path separator
          // Does it have nxm/opt? If not, add it.
          String newPhysPath = addNxmOpt(physPath, optName);
          if (newPhysPath != null) {
            M.warning("Option tree path entered '"+physPath
                 +"' is not valid. Modified to '"+newPhysPath+"'");
            physPath = newPhysPath;
          }
        } else {
          M.error ("Option path '"+physPath+"' not defined, or invalid");
        }
      }
    } else {
      // A link, see if it is a special type
      IsALink = true;
      if (linkPath.equalsIgnoreCase("NXM")) {
        InNmRoot = true;
        linkPath = getNMRootNxmDir(optName.toLowerCase());
      } else if (linkPath.equalsIgnoreCase("BACK")) {
        M.warning("Back link is TBD");
        return FINISH;
      } else {
        String str = FileName.terminatePath(linkPath).toLowerCase();
        if (!str.endsWith(optName.toLowerCase()+File.separator)) {
          M.info("Link '"+linkPath+"' does not end with option tree name, adding");
          linkPath = str+optName.toLowerCase();
        }
      }
    }

    if (createOption) {                // create option tree
      if (physPath == null && pathInOptTable != null) { // Use the path from the OPT table
        physPath = pathInOptTable;                      // if none is given in PATH=
      }
      debug("createOptionTree: optName "+optName+" physPath "+physPath+" linkPath "+linkPath);
      if (createOptionTree (optName, physPath, linkPath) ) {
        if (IsALink) {
          if (!InNmRoot) {
            restartRequired = OptionTree.defineOptionTree(M, optName, linkPath);
          }
        } else {
          restartRequired = OptionTree.defineOptionTree(M, optName, physPath);
        }
      } else {
        M.warning ("Unable to create option tree ["+optName+"] at path "+physPath);
      }
    }
    else if ((linkPath != null) && !linkPath.equals("")) { // Create link
      makeLink(M, verbose, linkPath, physPath);
    }
    else if (deleteOption) {           // logically delete option tree (remove OPT.name)
      if (optName.length()<=0) {
        M.warning("No option specified to delete.");
      } else {
        // logically delete the OPTION tree definition from the OPT results table
        // this will NOT delete the option tree's directories.
        if (OptionTree.removeOptionTree(M, optName)) {
          M.info ("Deleted option tree ["+optName+"] from mappings.");
        }
      }
    }
    else if (physPath != null && physPath.length()>0) {  // define option tree
      if ("*".equals(optName)) {       // if star syntax: define all option tree under specified path
        restartRequired = defineAllOptsUnder(physPath);
      } else if ("CLASSPATH".equals(physPath)) { // find & define option tree in CLASSPATH (since 3.3.0 for Bug 2484)
      String optPath = find(optName, MA.getU("/FIND"), true);
      restartRequired = OptionTree.defineOptionTree(M, optName, optPath);
      } else {                         // else (Set OPT.name = path)
        restartRequired = OptionTree.defineOptionTree(M, optName, physPath);
      }
    }
    else if ("*".equals(optName)) {    // no path given for defining all option via star syntax
      M.warning("Path not provided for definining all option tree via '*' (star syntax)");
    }
    else if (MA.isPresent("/FIND")) {  // Find option tree in CLASSPATH (since 3.3.0 for Bug 2484)
      find(optName, MA.getU("/FIND"), false); // no error (just warn) if not found
    }
    else if (MA.isPresent("/INFO")) {  // Get option tree information (since 3.3.0 for Bug 2484)
      info(optName, MA.getU("/INFO"));
    }
    else if (optName.length()>0) {     // Show SINGLE option tree path
      listOpt(optName);
    }
    else if (MA.isPresent("/GET")) {   // Get comma-separated list of all defined option trees
      getDefinedOptsList(MA.getU("/GET"));
    }
    else {                             // Show ALL option trees
      listAllOpts();
    }

    // Restart JVM if option tree in path has been redefined (since 2.9.3 for 1795)
    if (restartRequired) {
      if (!MA.getState("/NOEXIT",(M.macro!=null))
          && M.terminal.isExtCmdSupported() && !Shell.isOneShot()) {
        if (verbose) M.info("Restarting shell to reload redefined ["+optName+"] option tree");
        Foreign.runExternal(M,""); // Automatically restarts the shell
      }
      else M.warning("You must exit the shell to reload ["+optName+"] option tree");
    }

    return FINISH;
  }

  /** Get comma-separated list of all defined option trees and save to results
      label. If in verbose, then also display to terminal.
      @param resLabel results label save results to (null or blank to skip)
      @since NeXtMidas 3.3.0 - code moved from process()
   */
  private void getDefinedOptsList(String resLabel) {
    nxm.sys.lib.Table options =  OptionTree.getAllOptions(M);
    String keyList = options.getKeyList();
    if (resLabel != null && resLabel.length() > 0) {
      MR.put(resLabel, keyList);
    }
    if (verbose) { // since 3.3.0
      M.println("All defined option trees: "+keyList);  // since 3.7.1 using M.println instead of M.terminal.writeln to allow logging - NXM-1813
    }
  }

  /** Find option tree in CLASSPATH. Useful from Java Web Start
      and nxm.sys.lib.NeXtMidas where JVM CLASSPATH is control externally
      verses by the NmClassLoader.
      @param optName name of option tree
      @param resLabel results label save results to (null or blank to skip)
      @param error Error if option tree is not specified or found
      @return Option tree's root path if found, null if not specified or not found
      @since NeXtMidas 3.3.0 for for Bug 2484
   */
  private String find (String optName, String resLabel, boolean error) {
    if (optName == null || optName.length() <= 0) {
      if (error) M.error("No option tree specified.");
      else       M.warning("No option tree specified.");
      return null;
    } else {
      String optRoot = OptionTree.findOptRoot(optName);
      if (optRoot == null) {
        if (error) M.error("Unable to find option tree ["+optName+"] in CLASSPATH.");
        else       M.warning("Unable to find option tree ["+optName+"] in CLASSPATH.");
      } else if (verbose) {
        M.println("Found option tree ["+optName+"] at "+optRoot);  // since 3.7.1 using M.println instead of M.terminal.writeln to allow logging - NXM-1813
      }
      if (resLabel != null && resLabel.length() > 0) {
        MR.put(resLabel, optRoot);
      }
      return optRoot;
    }
  }

  /** Get option tree information (from {@link OptionTree#getInfo(String, Midas, boolean, boolean)}
      and save to results label. If in verbose, then also display to terminal.
      @param optName name of option tree
      @param resLabel results label to save option tree info to (null or blank skip)
      @since NeXtMidas 3.3.0 for for Bug 2484
   */
  private void info (String optName, String resLabel) {
    if (optName == null || optName.length() <= 0) {
      M.warning("No option tree to get information on.");
    } else {
      nxm.sys.lib.Table optInfoTbl = OptionTree.getInfo(optName, M, verbose, true);
      if (optInfoTbl == null) {
        M.warning("Unable to get info for option tree ["+optName+"].");
      } else if (verbose) {
        String displayKey = ((resLabel != null && resLabel.length() > 0))? resLabel : optName+"_OPT_INFO";
        Results.doMultiple(M, displayKey, optInfoTbl, null, 0, true, 0, -1, false,verbose);
      }
      if (resLabel != null && resLabel.length() > 0) {
        MR.put(resLabel, optInfoTbl);
      }
    }
  }
  /* Add nxm/opt to input path.  Return null if it already has it.*/
  private String addNxmOpt(String pathIn, String optName) {
    String pathOut = null;
    String str = "nxm"+File.separator+optName.toLowerCase()+File.separator;
    if (pathIn.indexOf(str) < 0) {
      pathOut = FileName.terminatePath(pathIn)+str;
    }
    return pathOut;
  }

  /* Get $NMROOT/nxm/optName.  If optName is null get $NMROOT/nxm */
  private static String getNMRootNxmDir (String optName) {
    if (optName == null) { return Shell.getNmRoot()+"nxm"+File.separator; }
    else                 { return Shell.getNmRoot()+"nxm"+File.separator+optName; }
  }

  /** Is the option path valid?
   *  @param optionPath option tree path
   *  @return true if the path is valid
   */
  public static boolean isValidPath (String optionPath) {
    boolean validPath = false;
    if (optionPath == null || optionPath.length() == 0 ||
        optionPath.indexOf(Shell.getProperty("file.separator")) < 0) {
      validPath = false;
    }
    else {
      validPath = true;
    }
    return validPath;
  }

  /** Calls {@link #makeLink(Midas, boolean, String, String)} to make a link for an option package
      @param linkPath     the link path name
      @param physicalPath the physical path name
      @return true if the link was made
   */
  public static boolean makeLink (String linkPath, String physicalPath) {
    return makeLink (Shell.getSharedMidasContext(), false, linkPath, physicalPath);
  }
  /** Makes a link for an option package
      @param midas          The Midas context
      @param verbose        Show output?
      @param linkPath       The link name
      @param physicalPath   The physical path name
      @return true if link was made
  */
  public static boolean makeLink (Midas midas, boolean verbose, String linkPath, String physicalPath) {
    boolean madeLink = false;
    java.io.File linkFile = new java.io.File (linkPath);
    String ostype = (String) midas.results.get("ENV.OSTYPE");

    // Links are only used in UNIX (linux), so if the user wants to
    // use a link then lets make sure the OS type is UNIX.
    if (ostype.indexOf ("UNIX") >= 0) {
      // Look for parent directories
      java.io.File linkParentFile = new java.io.File (linkFile.getParent());
      if (!linkParentFile.exists() ) {
        //System.out.println ("Making directories "+linkParentFile+" for link");
        boolean mkdirResult = linkParentFile.mkdirs();
        if (!mkdirResult) midas.warning("Unable to create parent directory: "+linkParentFile);
      }
      // Make the link if possible
      if (linkFile.getParentFile().exists() ) {
        try {
          String cmd = "ln -s -n -f "+physicalPath+" "+linkFile.getPath();
          boolean wait = true;
          int status = Foreign.runInternal(midas,cmd,wait);
          if (status == 0) {
            if (verbose) midas.info("Created link "+linkFile.getPath()+" to "+physicalPath);
            madeLink = true;
          } else {
            midas.warning("Link command '"+cmd+"' FAILED");
            madeLink = false;
          }
        } catch (Exception ex) {
          midas.warning("Could not create link '"+linkFile.getPath()+" to '"+physicalPath+"'");
        }
      }

    } else {
      Shell.warning("Links not supported in os type "+ostype);
    }
    return madeLink;
  }

  /** Create option tree sub-directories
   *  @param treeRoot root of option tree
   *  @return true if successful
   */
  public boolean createOptionSubDirs (java.io.File treeRoot) {
    // True only if all sub-directories are created or already exist
    boolean createdSubDirs = true;
    Parser parseSubDirs = new Parser (OptionTree.SUBDIRNAMES.toLowerCase());
    for (int i = 1; i <= parseSubDirs.elements(); i++) {
      java.io.File subDir = new java.io.File (treeRoot, parseSubDirs.get(i));
      if (!subDir.exists()) {
        createdSubDirs = subDir.mkdir();
      }
    }
    return createdSubDirs;
  }

  private void debug (String str) {
    if (debug) M.type(str);
  }

  /** Get an option tree sub-directory by short sub-dir name
   *  @param treeRoot parent (root) directory
   *  @param subName  short sub-directory name
   *  @return option tree sub-directory
   */
  public java.io.File getOptionSubDirFile (java.io.File treeRoot, String subName) {
    // Returns NULL if not found
    java.io.File subDirFile  = null;
    String lowerCaseSubName = subName.toLowerCase();
    Parser parseSubDirs = new Parser(OptionTree.SUBDIRNAMES.toLowerCase());
    for (int i = 1; i <= parseSubDirs.elements(); i++) {
      java.io.File subDir = new java.io.File(treeRoot, parseSubDirs.get(i));
      if (subDir.toString().indexOf(lowerCaseSubName) >= 0) {
        subDirFile = subDir;
        break;
      }
    }
    return subDirFile;
  }

  /** Create option tree and subdirectories
      @param treeName     - The name of the option tree
      @param physicalPath - The physical path where cfg, mcr, prim,... areas exist
      @param linkPath     - The link path to the physical path
      @return true if option tree created
  */
  public boolean createOptionTree (String treeName, String physicalPath, String linkPath) {
    boolean optionTreeCreated = false; // Return this

    // Add required package naming conventions to option name
    debug ("createOptionTree: Option physical path name "+physicalPath);
    debug ("createOptionTree: Option link path name "+linkPath);

    // Is the option path name a link?
    debug ("createOptionTree: IsALink? "+IsALink);

    boolean problemMakingLink = false;
    if (IsALink) { problemMakingLink = !makeLink (M, verbose, linkPath, physicalPath); }

    // If OK, create tree root directory and sub directories, if necessary
    if (!problemMakingLink) {
      java.io.File optTreeRootFile = new java.io.File (physicalPath);
      // Create tree root
      if (optTreeRootFile.exists()) {
        M.warning ("Tree root "+optTreeRootFile+" already exists");
      }
      else {
        if (optTreeRootFile.mkdirs()) {
          debug ("Created tree root "+optTreeRootFile);
        }
        else {
          M.warning ("Unable to create option tree root "+optTreeRootFile);
        }
      }
      debug ("Option tree root "+optTreeRootFile);

      if (optTreeRootFile.exists()) {
        boolean problemCreatingSubDirs = !createOptionSubDirs(optTreeRootFile);

        if (!problemCreatingSubDirs) {
          optionTreeCreated = true;
        }

        if (optionTreeCreated) {
          // Now make an empty commands.cnf if one does not exist in the target option
          java.io.File cfgDir = getOptionSubDirFile (optTreeRootFile, "CFG");
          java.io.File cmdFile = new java.io.File (cfgDir, COMMAND_FILE_NAME);

          if (cmdFile.exists() ) {
            M.info (COMMAND_FILE_NAME+" already exists in "+cfgDir);

          } else {
            // Get the system option tree name
            String sysCfgDirName = Shell.getOptPath ("SYS","CFG");
            java.io.File cmdTemplateFile = new java.io.File (sysCfgDirName, "commands.template");

            try {
              FileUtil.copyBaseFile(M, cmdTemplateFile, cmdFile);
            } catch (Exception e) {
              M.printStackTrace("Problem copying "+cmdTemplateFile+" to "+cmdFile, e);
            }

            M.info ("Created option tree ["+treeName+"] in path "+physicalPath);
          }
        }
      }
    } else {
      optionTreeCreated = false;
    }

    return optionTreeCreated;
  }

  /** Lists the location of an option tree.
      @param name The name of the option tree.
      @since NeXtMidas 2.1.0
  */
  private void listOpt (String name) {
    listAllOpts(name);
  }

  /** Lists all available option trees.
      @since NeXtMidas 2.1.0
  */
  private void listAllOpts() {
    listAllOpts(null);
  }

  /** Lists all available option trees.
      @param listOnly The name of the **ONE** option tree to list, if this is null all are listed.
      @since NeXtMidas 2.3.0
  */
  private void listAllOpts (String listOnly) {
    nxm.sys.lib.Table options = OptionTree.getAllOptions(M);
    String optPath = MR.getS("OPT.PATH");

    /* Output the list. */
    String[] keys = options.getKeys();
    Arrays.sort(keys);

    if (listOnly == null) {
      M.println("Available Option Trees (<n> = index on path):");  // since 3.7.1 using M.println instead of M.terminal.writeln to allow logging - NXM-1813
    } else {
      if (!options.containsKey(listOnly.toUpperCase())) {
        M.warning("Option tree ["+listOnly+"] not found.");
      }
    }

    int  max  = Parser.findIndex(optPath, "SYS");
    for (int i = 0; i < keys.length; i++) {
      int    index  = Parser.findIndex(optPath, keys[i]);
      String marker = "    ";

           if (index >= 10)                 { marker = "<"+index+">";  }
      else if ((index >= 0) && (max >= 10)) { marker = "< "+index+">"; }
      else if (index >= 0)                  { marker = " <"+index+">"; }

      if (listOnly == null) {
        marker =  "  " +marker + " " ;
      } else {
        marker = ""; // Don't show extra info for just one option.
      }

      nxm.sys.lib.Table opt = options.getTable(keys[i]);
      String  name = StringUtil.padRight(keys[i], 12);
      String  path = opt.getS("PATH");
      String  base = opt.getS("CFGROOT");
      String  root = opt.getS("NXMROOT");
      String  type = opt.getS("TYPE");
      String  dir  = null;
      boolean link = opt.getState("LINK");

      if (type.equals(OptionTree.TYPE_UNDER_NMROOT)) {
        dir = base;
      } else if (type.equals(OptionTree.TYPE_JAR_FILE)) {
        dir = root;
      } else if (link) {
        dir = path + " -> " + base;
        if (type.equals(OptionTree.TYPE_BACK_LINK)) { dir = dir + " <UnusedBackLink>"; }
      } else {
        dir = base;
        if (type.equals(OptionTree.TYPE_BACK_LINK)) { dir = dir + " <UsesBackLink>"; }
      }

      if ((listOnly == null) || (listOnly.equalsIgnoreCase(keys[i]))) {
        M.println(marker + name + " = " + dir);  // since 3.7.1 using M.println instead of M.terminal.writeln to allow logging - NXM-1813
      }
    }
    for (String optname : keys) {
      nxm.sys.lib.Table opt = options.getTable(optname);
      String path = opt.getS("PATH");
      // String root = opt.getS("NXMROOT");
      String type = opt.getS("TYPE");

      if ((type == null) || type.equals(OptionTree.TYPE_ERROR)) {
        if ((listOnly == null) || (listOnly.equalsIgnoreCase(optname))) {
          M.warning("OPTION: The path "+path+" may not be a valid option tree path for ["+optname
            +"]. Could not find " +path+"nxm"+File.separator+optname.toLowerCase()+File.separator+"cfg.");
        }
      }
    }
  }

  /** Define all option trees under the specified path.
      The sub-directory (one level down) will be used as the option tree name.
      Sub-directories with a dot will be ignored.
      @param pathstr directory to look for option trees and define them.
      @return true if user should restart the JVM since an option tree in PATH is redefined
      @since NeXtMidas 2.9.3
   */
  private boolean defineAllOptsUnder (String pathstr) {
    boolean restartRequired = false;
    pathstr = FileUtil.terminatePath(pathstr); // make sure it ends with path separator
    File path = new File(pathstr);
    File[] files;
    files = path.listFiles(DIR_FILTER);
    if (files==null) {
      throw new MidasException("OPTION: No files found for ["+pathstr+"].");  // added in 3.3.2 to give more descriptive error.
    }

    for (File dir : files) {
      String optName = dir.getName().toUpperCase();
      String optPath = dir.getPath() + File.separator;
      restartRequired |= OptionTree.defineOptionTree(M, optName, optPath);
      if (verbose) M.info("Defined option ["+optName+"] under path "+optPath);
    }
    return restartRequired;
  }

  /** FileFilter to only allow directories without a dot through. */
  private static class DirectoryFilter implements FileFilter {
    @Override
    public boolean accept (File pathname) {
      return  pathname.isDirectory() &&               // only accept directories
             (pathname.getName().indexOf('.') == -1); // ignore dirname with dots
    }
  }
  static final DirectoryFilter DIR_FILTER = new DirectoryFilter();
}
