package nxm.sys.intr;

import java.util.HashSet;

import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.FileName;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Terminal;
import nxm.sys.lib.TextFile;

/**
  List matching files on Midas auxiliary paths or any other path, if given.

  @author Jeff Schoen
*/
public class Files extends Intrinsic {

    // Elements of the file
  private String         path;   // path (not including filename)
  private String         name;   // filename (not including path)
  private String         ext;    // filename extension
  private String         aux;    // NeXtMidas AUX directory
  private String         uid;    // User ID (not implemented)

    // Parameters passed in via switches
  private boolean       erase;          // Erase the file
  private boolean       toFile;         // Filename to receive output
  private String        listRes;        // Name of the list to output to (null = no /LIST=  given)
  private StringBuilder listVal;        // Content of the list to output
  private boolean       verboseListing; // Lists extra file data (such as headers)
  private boolean       headerOnly;     // Lists file info
  private int           recurse;        // List recursively in the directory
  private boolean       debug;          // enable/disable debugging
  private boolean       nameOnly;       // Only include the name when listing
  private boolean       outputToText;   // Write to a text file vs a blue file

  /** The Recursive List Switch list of options and values */
  public  String           recurseList = "Full,LevelFind,FirstFind";
  private static final int R_NONE      = 0;
  private static final int R_FULL      = 1;
  private static final int R_LEVELFIND = 2;
  private static final int R_FIRSTFIND = 3;

  /** root name of filename */
  public static final int    ROOT = 1;
  /** filename extension */
  public static final int    EXT  = 2;

  private DataFile       df;            // DataFile for output into blue file format
  private TextFile       tf;            // TextFile for output into a text file format
  private Data           db;

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

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

    // Get parameters
    name  = MA.getS("NAME");
    FileName fn = new FileName(name);  // FileName parses out the root name and extension
    ext = MA.getS("EXT");
    boolean       strict = false; // enforce name with extension (e.g. *et.jpg|#v2.prm" )
    if ((ext==null || ext.isEmpty()) && MA.getState("/STRICT")) {
      strict=true;  // only true if no extension given in EXT= and /STRICT is present
    }
    else {
      if (ext==null || ext.isEmpty()) ext=parseComplexName(name, EXT); // if no ext given via EXT=, look for one in the file name
      name = parseComplexName(name, ROOT);                             // break apart the given file name in case there are multiple extensions (since 2.9.0 Bug 2170)
    }
     path = fn.getPath();                   // dir path
     /* since 2.9.0, the two lines below were changed to the lines above in order to break apart a complex name such as c*.prm|d*.tmp,
        which has multiple roots and multiple extensions that need to be parsed out. See Bug 2170         */
 // name = fn.getRoot();                   // Filename minus path and extension
 // ext   = MA.getS("EXT",fn.getExt());

    aux   = MA.getS("AUX");
    uid   = MA.getS("UID");
    debug = MA.getState("/DEBUG", false);

    if (debug) M.info("Name '"+name+"', ext '"+ext+"', aux '"+aux+"'");

    if (aux.equals("*")) aux = "";
    if (aux.equals("ALL")) aux = "";

    listRes = MA.getU("/LIST", null);
    toFile  = MA.find("/F");
    verboseListing = MA.getState("/V", false);
    headerOnly = (verboseListing)? false : MA.getState("/H", false); // V switch takes precedence over H switch
    erase      = MA.find("/E");
    recurse    = MA.getSelectionIndex("/R",recurseList,R_NONE);
    nameOnly   = MA.getState("/NAMEONLY");

    if (listRes != null) {
      if (toFile)                M.error("Cannot combine /F and /LIST=");

      listVal = new StringBuilder();
    }
    else if (toFile) {
      FileName outfn = MA.getFileName("/F", "files.tmp", "files.tmp");
      String form = MA.getS("/FORM", "XA");

      /** Case tested for user option to create a text file or a blue file */
      if (outfn.getExt().equalsIgnoreCase("txt")) {     // Since 3.1.1. for Bug 2004
          tf = new TextFile(M, outfn);
          tf.open (BaseFile.OUTPUT);
          outputToText = true;
          if (verbose) M.info (" ... writing to TextFile '" + tf.filename + "'");
      } else {
          df = new DataFile();
          df.init (M, outfn, "1000", form,0);
          df.open (BaseFile.OUTPUT);
          db = df.getDataBuffer(1);
          if (verbose) M.info (" ... writing to DataFile '" + df.filename + "'");
      }
    }
    else M.terminal.more(Terminal.INIT);  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813

    /** if the path is given, ignores the aux's */
    if (path.length() > 0) {
      listFiles("NOT", path, name, ext,strict);
    }
    else if (aux.isEmpty()) {
      String excludeList = MA.getS("/EXCLUDE");  // get any excluded AUX's defined by the switch
      HashSet<String> xset = null;
      if (excludeList!=null && !excludeList.isEmpty()) {
        excludeList = excludeList.toUpperCase();
        Parser xauxes = new Parser("|"+excludeList+"|",'|');
        xset = new HashSet<>();
        for (String aux : xauxes.getArray(0)) {
          xset.add(aux);
        }
      }
      String raux = MR.getString("AUX.READ");
      Parser auxes = new Parser(raux,'|');
      int naux = auxes.elements();
      for (int i=1; i<=naux; i++) {
        String iaux = auxes.get(i);

        // /EXCLUDE switch has precedence
        if (xset!=null) {
          if (xset.contains(iaux)) continue;                       // Bug 2482 (3.1.0)
        }
        else if (Shell.getCurrent().isExcludedAux(iaux)) continue; // Bug 2482 (3.1.0)

        String ipath = M.io.getAuxPath(iaux);
        String match = null;
        for (int j=1; j<i && match==null; j++) {
          String jaux = auxes.get(j);
          String jpath = M.io.getAuxPath(jaux);
          if (jpath.equals(ipath)) match = jaux;
        }
        if (match==null) {
          listFiles (iaux,ipath,name,ext,strict);  // new path
        }
        else if (!toFile && (listRes==null)) {  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
          M.more("Auxiliary ["+iaux+"] = " + ipath + " matches "
                + "Auxiliary [" + match + "] (skipping)");
        }
      }
    }
    else {
      String auxPath = M.io.getAuxPath(aux);
      if (auxPath!=null) listFiles (aux,auxPath,name,ext,strict);
      else listFiles ("NOT",aux,name,ext,strict);
    }
    if (listRes != null) {
      MR.put(listRes, listVal.toString());
    }
    else if (toFile)     {
      if ((df != null) && df.isOpen) {
        df.close();
      }
      if ((tf != null) && tf.isOpen) {
        tf.close();
      }
    }
    else {
      M.terminal.more(Terminal.EXIT);
    }

    return FINISH;
  }

  /** Check a given String array for an entry that should be excluded
      @return true if the entry is found, else false
      @since NeXtMidas 3.1.0
   */
  private boolean checkForExcludedAux (String aux, String[] list) {
    // this method added for Bug 2482
    for (String xaux : list) {
      if (aux.equals(xaux)) return true;
    }
    return false;
  }

  private int listFiles (String aux, String path, String name, String ext, boolean strict) {
    if (strict) {
      if (name.indexOf('|')==0) name = name.substring(1);
      int barPos = name.indexOf('|');
      int count = 0;
      if (barPos>0) {
        while (barPos>0) {
          FileName fn = new FileName(name.substring(0,barPos));
          String tmpExt = fn.getExt();
          String tmpName = fn.getRoot();
          count += Math.max(0, listFiles(aux,path,tmpName,tmpExt));
          name = name.substring(barPos+1)+"|";
          barPos = name.indexOf('|');
        }
      } else {
        FileName fn = new FileName(name.substring(0,barPos));
        String tmpExt = fn.getExt();
        String tmpName = fn.getRoot();
        count += Math.max(0, listFiles(aux,path,tmpName,tmpExt));
      }
      return count;
    }
    else {
      return listFiles(aux,path,name,ext);
    }
  }

  /** Get multiple extensions or root names from a single String. Usual case is when filenames
      with extensions are separated by '|'. Leading and trailing '|' are removed
      @param name The input String to parse
      @param type The type of String to return ({@link #ROOT} or {@link #EXT})
      @return     A string with the root(s) or extension(s) separated by a '|' if there are multiple
      @since      NeXtMidas 2.9.0
   */
  private static String parseComplexName (String name, int type) {
    // There may be other classes who could use this method.
    //  If so, we can make it static and put it in FileName
    if ((type != ROOT && type != EXT) || name==null || name.isEmpty()) return "";
    StringBuilder retStr = new StringBuilder();
    int len = name.length();
    while (name.indexOf('|')==0) name = name.substring(1);  // remove leading '|'
    while (name.lastIndexOf('|')==len-1) name = name.substring(0,len-1);  // remove trailing '|'
    int barPos = name.indexOf('|');
    if (barPos >= 0) {   // indicates multiple file name sub-strings
      retStr.append(parseComplexName(name.substring(0,barPos), type));  // get the lead sub-string
      retStr.append("|");
      name = name.substring(barPos+1);                                  // remove the lead sub-string from the input string
      barPos = name.indexOf('|');                                       // check for another sub-string
      if (barPos > 0) {
        retStr.append(parseComplexName(name, type));                    // parse the remainder (recursive)
      }
    }
    FileName fn = new FileName(name);                       // FileName(String) does the parsing for root and ext
    if      (type==ROOT) retStr.append(fn.getRoot());
    else if (type==EXT)  retStr.append(fn.getExt());

    return retStr.toString();
  }

  /** Lists all the files in a given aux.

      @param aux   The aux to list.
      @param path  The path (directory) to list files from.
      @param name  The name of the files to list (may contain wildcards).
      @param ext   The extension of the files to list (may contain wildcards).
      @return The number of files listed or a negative value if unsuccessful.
   */
  @SuppressWarnings("deprecation")  // for M.terminal - since 3.7.1 for NXM-2813
  private int listFiles (String aux, String path, String name, String ext) {
    String[] files = M.io.listFiles(path,name,ext); // NOTE: this returns filenames without PATH
    FileName fn = null;
    if (files==null) {  // if aux not found, check for case sensitive version (Bug 1691)
      fn = new FileName(path,true);
      files = M.io.listFiles(fn,name,ext);
    }
    if (files==null) {
      if (!toFile && !erase && (listRes == null)) {
        M.more("Auxiliary ["+aux+"] = "+path+" (not a directory)"); // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
      }
      return -1;
    }
    int n = files.length;
    if (recurse==R_FIRSTFIND && n>0) {
      recurse=R_NONE;
      n = 1;
    }

    if (listRes != null) {
      for (String filename : files) {
        if (listVal.length() > 0) listVal.append(',');
        if (!nameOnly)            listVal.append(path);
                                  listVal.append(filename);
      }
    }
    else if (toFile) {
      for (String filename : files) {
        String str = (nameOnly ? filename : (path+filename));

        if (outputToText) {
          tf.writeln(str);    // Since 3.1.1 for BUG 2004
        } else {
          db.fromString(str);
          df.write(db);
        }
      }
    }
    else if (erase) {
      for (String filename : files) {
        BaseFile bf = BaseFile.getInstanceFor(this,path+filename);
        nxm.sys.intr.Erase.eraseFile(M,bf,verbose,true,false);
      }
    }
    else {
      M.more("Auxiliary ["+aux+"] = "+path+"   ("+n+" files)");  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
      if (headerOnly && n > 0) {
        M.more(" Type  Form   Elements   Data Bytes  Filename");
        M.more(" ------------------------------------------------------");
      }

      for (String filename : files) {
        fn = new FileName(path+filename,true);
        if (verboseListing) {
          BaseFile bf = BaseFile.getInstanceFor(this,fn);
          if (!bf.open()) {
            // return (0); // prior to 3.5.2 (TODO remove in 3.9+)
            n--; continue; // decrement count and continue to next file
          }
          M.print( bf.listHeader() );
          bf.close();
          M.more("");
        }
        else if (headerOnly) {
          BaseFile bf=null;
          try {
            bf = BaseFile.getInstanceFor(this,fn);
          } catch (Exception e) {
            M.printStackTrace("Exception when getting Instance for file " + filename, e); // since 2.9.0 for BUG 2171, BUG 2172
            n--; continue; // decrement count and continue to next file
          }
          String  typeAndForStr = "";
          String  elemStr = "";
          String  sizeStr = "";
          boolean dfOpenError = false; // Error in opening the file.

          if (bf instanceof DataFile) {
            try {
              if (!bf.open()) {
                // return (0); // prior to 3.5.2 (TODO remove in 3.9+)
                n--; continue; // decrement count and continue to next file
              }
              DataFile hcb = (DataFile)bf;
              typeAndForStr += hcb.getType()+"  "+hcb.getFormat();
              elemStr += hcb.size;
              sizeStr += (long)hcb.getDataSize();
              hcb.close();
            } catch (Exception e) {
              // This is quite common since X-Midas 3.8.X creates an xmhistory_log.prm file that
              // is not a valid Blue file.
              dfOpenError = true;
              bf = new BaseFile(this,fn);
            }
          }

          if (!(bf instanceof DataFile)) {
            try {
              if (!bf.open()) {
                // return (0); // prior to 3.5.2 (TODO remove in 3.9+)
                n--; continue; // decrement count and continue to next file
              }
            } catch (Exception e) {
              M.printStackTrace("Exception when attempting to open file "+bf.filename, e); // since 2.9.0 for BUG 2171
            }
            String type  = bf.getClass().getName();
            int    index = Math.max(type.lastIndexOf('.'), type.lastIndexOf('$'));
            if (index > 0)             { type = type.substring(index+1); }
            if (type.endsWith("File")) { type = type.substring(0,type.length()-4); }

            typeAndForStr += type.toUpperCase();
            sizeStr += (long)bf.getSize();
            bf.close();
          }
          if (dfOpenError) { typeAndForStr = "ERROR"; }
          StringBuilder sb = new StringBuilder(120);
          sb.append(' ');  StringUtil.padRight(sb, typeAndForStr, 10, ' ');
          sb.append("  "); StringUtil.padRight(sb, elemStr, 10, ' ');
          sb.append("  "); StringUtil.padRight(sb, sizeStr, 10, ' ');
          sb.append("  "); sb.append(filename);
          M.more(sb);  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
        }
        else {
          M.more("  "+filename,20);  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
        }
      } // end for loop
      if (headerOnly && n > 0) { M.more(""); }  // since 3.7.1 using M.more instead of M.terminal.more - NXM-1813
      M.more(Terminal.FLUSH);
    }
    if (recurse!=R_NONE && !(recurse==R_LEVELFIND && n>0) ) {
      String[] dirs = M.io.listFiles(path,"*","dir");
      if (dirs==null && fn!=null) {   // handle case where path is case sensitive (Bug 1691)
        fn = new FileName(path,true);
        dirs = M.io.listFiles(fn,"*","dir");
      }
      for (int i=0; i<dirs.length && recurse!=R_NONE; i++) {
        listFiles (aux+".", FileName.addPathDir(path,dirs[i]), name, ext);
      }
    }
    return n;
  }
}
