package nxm.sys.lib;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;

import nxm.sys.inc.Installation;
import nxm.sys.inc.InternalUseOnly;

/**
  Implements a user terminal in pure java code.
  It does not implement the command line recall features of an IODevice.

  @author Jeff Schoen / Neon Ngo
*/
public class Terminal implements Appendable {

  /** Input mode flags: Same as {@link #INPUT_NORMAL}, but do not allow escapes (arrow keys). */
  public static final int INPUT_NO_ESC = 2;
  /** Input mode flags: Special input mode used by {@link #getInputTabComplete(String,String)}. */
  public static final int INPUT_TAB_COMPLETE = 1;
  /** Input mode flags: Normal input. */
  public static final int INPUT_NORMAL = 0;
  /** Input mode flags: Single character input. */
  public static final int INPUT_CHAR = -1;
  /** Input mode flags: Same as {@link #INPUT_CHAR}, but clear char when entered. */
  public static final int INPUT_CHAR_CLEAR = -2;
  /** Input mode flags: Ignore anything except a return character. */
  public static final int INPUT_RETURN = -3;

  /** Midas context*/
  public Midas M;

  protected int morestat = -1;  // Since 3.5.0: made protected for Bug 1563
  private String morestr,morespaces;
  /** archive file to write out to */
  public TextFile archiver=null;
  /** NeXtMidas messaging stream to write out to */
  public Command messager=null;
  /** write out to leach terminal if present and echo settings turned on */
  public Terminal leach=null;
  /** determines whether or not to write out to leach, raw terminal, or neither */
  public boolean echo=true;

  protected static final int DEFAULT_ROW_COUNT = 32; // The default number of rows.
  protected static final int DEFAULT_COL_COUNT = 80; // The default number of columns.

  /** More function: Initialize a new more session.   */
  public static final int INIT  = 1;
  /** More function: Flush out current test.          */
  public static final int FLUSH = 2;
  /** More function: Flush out current text and exit. */
  public static final int EXIT  = 9;

  private int rows    = DEFAULT_ROW_COUNT; // The number of rows    (-1 = use terminal size).
  private int columns = DEFAULT_COL_COUNT; // The number of columns (-1 = use terminal size).

  /** Formatter used in {@link #format(String, Object...)}. */
  private Formatter formatter;

  /** The input stream to read user input. Since 3.3.0. */
  private InputStream in;
  /** The output stream to write/display text to user. Since 3.3.0. */
  private OutputStream out;
  /** Shell used to get history from (when set). Since 3.3.1. */
  private Shell shell = null;

  /** Creates a new instance. Defaults to using the System.in and System.out I/O streams.*/
  public Terminal () {
    this(null, System.in, System.out, DEFAULT_ROW_COUNT, DEFAULT_COL_COUNT);
  }

  /** Creates a new instance using the specified Midas context.
      Defaults to using the System.in and System.out I/O streams.
      @param midas The associated Midas context.
   */
  public Terminal (Midas midas) {
    this(midas, System.in, System.out, DEFAULT_ROW_COUNT, DEFAULT_COL_COUNT);
  }

  /** Creates a new instance using the specified input and ouput streams.
      @param in the input stream to use
      @param out the output stream to use
      @since NeXtMidas 3.3.0
   */
  public Terminal (InputStream in, OutputStream out) {
    this(null, in, out, DEFAULT_ROW_COUNT, DEFAULT_COL_COUNT);
  }

  /** Creates a new instance.
      @param midas The associated Midas context.
      @param rows  The number of rows    to use (-1 to set based on terminal size).
      @param cols  The number of columns to use (-1 to set based on terminal size).
      @since NeXtMidas 2.1.0
   */
  protected Terminal (Midas midas, int rows, int cols) {
    this(midas, System.in, System.out, rows, cols);
  }

  /** Creates a new instance.
      @param midas The associated Midas context.
      @param in the input stream to use
      @param out the output stream to use
      @param rows  The number of rows    to use (-1 to set based on terminal size).
      @param cols  The number of columns to use (-1 to set based on terminal size).
      @since NeXtMidas 3.3.0
   */
  public Terminal (Midas midas, InputStream in, OutputStream out, int rows, int cols) {
    M = midas;
    this.in = in;
    this.out = out;
    this.rows    = rows;
    this.columns = cols;
  }

  /** Read from the terminal. This is the same as <code>getInput("","",INPUT_CHAR)</code>
      @return The user input.
      @see #getInput(String, String, int)
   */
  public String read () {
    return getInput ("","",INPUT_CHAR);
  }

  /** Read from the terminal. This is the same as <code>getInput("","",INPUT_NORMAL)</code>
      @return The user input.
      @see #getInput(String, String, int)
   */
  public String readln () {
    return getInput ("","",INPUT_NORMAL);
  }

  /** Write/display to the terminal.  Allowing for it to be forwarded to additional sources:<br>
      1. as a Message to a Command<br>
      2. and/or saved to a archive log<br>
      -and- intercepted:<br>
      3. not printed if echo is off<br>
      4. intercepted/leached by another attached terminal (e.g. SHELLGUI)<br>
      5. directly sent to stdout while in a debugger (when Installation.DEBUGGER is true)
      6. or display directly to the terminal using the print method (default behavior).<br>
      See the ARCHIVE;SYS command on how to enable some of the above options.
      @param text The text to write/print.
      @since NeXtMidas 3.1.1 to replace write(String) by using a more generic input type
   */
  public synchronized void write (CharSequence text) {
    // Since 3.1.1: This method was made synchronized so that when multiple threads write to the
    //              terminal via print(.), format(..), or write(.); they don't step over each other
    String textstr = (text==null)? null : text.toString(); // matches behavior when text is null.
    if (messager!=null) messager.processMessage("TEXT",0,textstr);
    if (archiver!=null) archiver.write(textstr);

    if (!echo)            ; // echo off, do not write to leach or raw terminal
    else if (leach!=null) leach.write(textstr);
    else                  print(textstr);
  }

  /** Write/display a line (appending a newline) to the terminal by calling
      <code>write(text+"\n")</code>.
      @param text The text to write/print.
      @see #write(CharSequence)
      @since NeXtMidas 2.7.1 to replace writeln(String) by using a more generic input type
   */
  public void writeln (CharSequence text) {
    write(text+"\n");
  }

  /** Print a line (appending a newline) directly to the terminal.
      Most Primitives/libraries/programs should call {@link #write(CharSequence)} or
      {@link #writeln(CharSequence)} instead of calling this directly.
      Note: Previous implementation (prior to 3.1.1) calls the write method,
            but (since 3.1.1) this now calls the print method for API consistency.
      @param text The text to print.
      @since NeXtMidas 2.7.1 to replace println(String) by using a more generic input type
   */
  public void println (CharSequence text) {
    print(text+"\n");
  }

  /** Print directly to the terminal.
      Most Primitives/libraries/programs should call {@link #write(CharSequence)} or
      {@link #writeln(CharSequence)} instead of calling this directly.
      @param text The text to print.
   */
  public synchronized void print (String text) {
    // Since 3.1.1: This method was made synchronized so that when multiple threads write to the
    //              terminal via print(.), format(..), or write(.); they don't step over each other
//    System.out.print(text); // prior to 3.3.0
    // begin code since 3.3.0
    try {
      out.write(text.getBytes());
      if (!Installation.DEBUGGER) { // in debugger (flush is too slow)
        out.flush();
      }
    } catch (IOException e) {
      // ignore for now since not sure what to do here
    }
    // end of code added since 3.3.0
  }

  /** Get input from the terminal with an optional prompt.
      This is the same as <code>getInput(prompt,seed,INPUT_NORMAL)</code>.
      @param prompt The prompt string.
      @param seed   Initial text to display (i.e. default input text).
      @return The user input string.
      @see #getInput(String, String, int)
   */
  public String getInput (String prompt, String seed) {
    return getInput (prompt,seed,INPUT_NORMAL);
  }

  /** Get input from the terminal with an optional prompt.
      @param prompt The prompt string.
      @param seed   Initial text to display (i.e. default input text).
      @param flags  Input mode flag ({@link #INPUT_NO_ESC}, {@link #INPUT_TAB_COMPLETE},
                    {@link #INPUT_NORMAL}, {@link #INPUT_CHAR}, {@link #INPUT_CHAR_CLEAR}).
      @return The user input string.
   */
  public String getInput (String prompt, String seed, int flags) {
    if (leach!=null) return leach.getInput(prompt,seed,flags);
    int i=0, n=0, maxbuf=1024;
    char[] buffer = new char[maxbuf];
    print(prompt);
    for (;;) {
      try {
        i = in.read();
      } catch (IOException e) { }
      if (i == -1);
      else if (i == 13) ;
      else if (i == 10 || n >= maxbuf) return new String(buffer,0,n);
      else buffer[n++] = (char)i;
    }
  }

  /** Get input from the terminal with an optional prompt. Also allows use of tab-complete.
      @param prompt The prompt string.
      @param seed   Initial text to display (i.e. default input text).
      @return The user input string.
      @since NeXtMidas 2.3.0
   */
  public final String getInputTabComplete (String prompt, String seed) {
    boolean complete = true;  // Enable tab-complete?
    boolean autolist = true;  // Enable autolist when multiple options found?

    String input = null;

    if (!complete) {
      input = getInput(prompt, seed, INPUT_NORMAL); // Can't do tab completion.
    }
    else {
      input = getInput(prompt, seed, INPUT_TAB_COMPLETE);

      while ((input != null) && input.endsWith("\t")) {
        String   str         = input.substring(0, input.length()-1);
        String[] tabComplete = tabComplete(str);

        if (tabComplete == null) {
          input = getInput(prompt, str, INPUT_TAB_COMPLETE);
        } else {
          if (autolist) {
            for (int i = 1; i < tabComplete.length; i++) {
              print("    "+tabComplete[i]+"\n");
            }
          } else {
            Shell.beep();
          }
          input = getInput(prompt, tabComplete[0], INPUT_TAB_COMPLETE);
        }
      }
    }

    return input;
  }

  /** Check for user interrupt from the terminal (Control-C). <br>
      <br>
      Due to Java restrictions most implementations will not be able to handle interrupts
      from inside Java (i.e. any Control-C will cause the Java VM to exit).
      @return The number of interrupt signals caught.
   */
  public int getInterrupt () {
    return 0;
  }

  /** Set the interrupt. Usually used to reset the interrupt back to 0.
      @param value  The value for interrupt
      @since neXtMidas 2.9.0
   */
  public void setInterrupt (int value) {
    //do nothing here. Override in NTerminal
  }

  /** Gets the number of displayable rows in the terminal. If the value can not be obtained
      directly from the terminal then the default will be returned (see {@link #setRows(int)}).
      @return The number of displayable rows in the terminal.
   */
  public int getRows () {
    int rowCount = rows;

    if (rowCount <= 0) { rowCount = getRowCount();     } // rows=-1     --> get based on term size
    if (rowCount <= 0) { rowCount = DEFAULT_ROW_COUNT; } // termSize=-1 --> use default

    return rowCount;
  }

  /** Gets the number of displayable columns in the terminal. If the value can not be obtained
      directly from the terminal then the default will be returned (see {@link #setColumns(int)}).
      @return The number of displayable rows in the terminal.
   */
  public int getColumns () {
    int colCount = columns;

    if (colCount <= 0) { colCount = getColCount();     } // cols=-1     --> get based on term size
    if (colCount <= 0) { colCount = DEFAULT_COL_COUNT; } // termSize=-1 --> use default

    return colCount;
  }

  /** Gets the row count for the terminal.
      @return The row count for the terminal or -1 (use default).
      @since NeXtMidas 2.1.0
   */
  protected int getRowCount () { return -1; }

  /** Gets the column count for the terminal.
      @return The column count for the terminal or -1 (use default).
      @since NeXtMidas 2.1.0
   */
  protected int getColCount () { return -1; }

  /** Sets the default number of rows to display in the terminal.
      @param r The default number of rows for the terminal (-1 to set based on terminal size).
   */
  public void setRows (int r) {
    rows = r;
  }

  /** Sets the default number of columns to display in the terminal.
      @param c The default number of columns for the terminal (-1 to set based on terminal size).
   */
  public void setColumns (int c) {
    columns = c;
  }

  /** Handle the more filter open/close/test functions
      @param func The more function to perform ({@link #INIT}, {@link #FLUSH}, {@link #EXIT}).
      @return true if there are still available lines of output for the more filter.
   */
  public boolean more (int func) {
    if (func==INIT) {
      morestat = getRows()-1;
      morestr = ""; morespaces = "                              ";
    }
    else if (func==FLUSH) {
      if (morestr.length()>0) more(morestr);
      morestr = "";
    }
    else if (func==EXIT) {
      if (morestr.length()>0) more(morestr);
      morestat = -1;
    }
    return (morestat >= 0);
  }

  /** Determines if there are more lines to output with more.
      @return true if there are still available lines of output for the more filter.
   */
  public boolean more () {
    return (morestat >= 0);
  }

  /** Handle the more filter text output.
      @param text The text to display.
   */
  public void more (String text) {
    if (morestat<0);
    else if (morestat==0) {
      String ans = getInput("Press RETURN=NextLine, SPACE=NextPage, or Q=Quit: ", "", INPUT_CHAR_CLEAR);
           if (ans.startsWith("q")) morestat = -1;           // Quit
      else if (ans.startsWith("Q")) morestat = -1;           // Quit
      else if (ans.equals("\n"))    morestat = 1;            // One-Line
      else                          morestat = getRows()-1;  // Full Page
    }
    if (morestat>0) {
      morestat--;
      writeln (text);
      if (M.logger != null) M.logger.processMessage(M,null,text,null);
      }
  }

  /** Handle the more filter text output for a CharSequence.
      @param text the text to display (null will be converted to "null")
      @since NeXtMidas 3.5.2
   */
  public void more (CharSequence text) {
    more("" + text); // convert null to "null" which matches writeln(text)
  }

  /** Handle the more columnator filter text output.
      @param text        The text to display (cannot be null).
      @param columnwidth The column width to use.
   */
  public void more (String text, int columnwidth) {
    int i; int width = getColumns();
    if (morestr.length()+Math.max(columnwidth,text.length()) > width) {
      if (morestr.endsWith(" ")) {
        int len = morestr.length();
        while((len > 0) && (morestr.charAt(len-1) == ' ')) { len--; }
        morestr = morestr.substring(0, len);
      }
      more (morestr);
      morestr="";
    }
    i = columnwidth-text.length();
    while (i<=0) i += columnwidth;
    morestr = morestr + text + morespaces.substring(0,i);
  }

  /** Write out the entire string using the more routine.
      This is identical to <code>moreString(StringUtil.cleanSplit(text, "\n"))</code>
      @param text The text to output to the Terminal.
   */
  public void moreString (String text) {
    moreString(StringUtil.cleanSplit(text, "\n"));
  }

  /** Write out a number of lines using the more routine.
      This is identical to <code>moreString(lines,0,lines.length-1)</code>.
      @param lines The set of lines to output.
      @since NeXtMidas 2.1.0
   */
  public final void moreString (String... lines) {
    moreString(lines,0,lines.length-1);
  }

  /** Write out a number of lines using the more routine.
      @param lines The set of lines to output.
      @param start The first line to output (inclusive).
      @param end   The last line to output (inclusive).
      @since NeXtMidas 2.1.0
   */
  public final void moreString (String[] lines, int start, int end) {
    if (lines == null) return;
    start = Math.max(start, 0);
    end   = Math.min(end, lines.length);

    more ( INIT );
    for (int i = start; more() && (i <= end); i++) { more(lines[i]); }
    more( EXIT );
  }

  /** Indicates if the terminal supports the running of external commands. This will return
      <code>true</code> if the running of external commands is supported by this terminal and
      <code>false</code> if the running of external commands is not supported.
      @return By default {@link Terminal} returns <code>true</code>.
      @see Foreign#runExternal
      @see Foreign#runInternal
   */
  public boolean isExtCmdSupported () {
    return true;
  }

  /** Formats a printf-style format string and writes it to the terminal.
      This mirrors the standard Java <code>System.out.format(..)</code> except that it writes to
      the applicable Midas terminal (which could be a SHELLGUI); see {@link java.util.Formatter} and
      {@link java.util.Formattable} for details on the format options available.

      @param format A printf-style format string as described in Format string syntax.
      @param args   Arguments referenced by the format specifiers in the format string.
                    If there are more arguments than format specifiers, the extra arguments
                    are ignored.
      @return this Terminal (allows calls to be chainable)
      @see java.util.Formatter#format(String, Object...)
      @since NeXtMidas 3.1.1
   */
  public synchronized Terminal format (String format, Object ... args) {
    // Since 3.1.1: This method was made synchronized so that when multiple threads write to the
    //              terminal via print(.), format(..), or write(.); they don't step over each other
    if (formatter == null) {
      formatter = new Formatter(this); // writes output to this Terminal (implements Appendable)
    }
    formatter.format(format, args);
    return this;
  }

  /** Formats a printf-style format string and writes it to the terminal.
      Calls {@link #format(String, Object...)}.
      @param format A printf-style format string as described in Format string syntax.
      @param args   Arguments referenced by the format specifiers in the format string.
                    If there are more arguments than format specifiers, the extra arguments
                    are ignored.
      @return this Terminal (allows calls to be chainable)
      @see java.util.Formatter#format(String, Object...)
      @since NeXtMidas 3.1.1
   */
  public Terminal printf (String format, Object ... args) {
    return format(format, args);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // METHODS FOR java.lang.Appendable interface
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** @since NeXtMidas 3.1.1 */
  @Override
  public Appendable append (CharSequence csq) throws IOException {
    write(csq);
    return this;
  }

  /** @since NeXtMidas 3.1.1 */
  @Override
  public Appendable append (CharSequence csq, int start, int end) throws IOException {
    CharSequence cs = (csq == null ? "null" : csq); // matches impl in java.io.PrintStream
    write(cs.subSequence(start, end).toString());
    return this;
  }

  /** @since NeXtMidas 3.1.1 */
  @Override
  public Appendable append (char c) throws IOException {
    write(String.valueOf(c));
    return this;
  }
  //////////////////////////////////////////////////////////////////////////////////////////////////
  // end of METHODS FOR java.lang.Appendable interface
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Get the current input stream
   *  @return the current input stream for user input.
   *  @since NeXtMidas 3.3.0
   */
  public InputStream getInputStream () {
    return in;
  }

  /** Set the input stream for user input.
      @param instream Input Stream
      @return previous Input Stream (null if none)
      @since NeXtMidas 3.3.0
   */
  public InputStream setInputStream (InputStream instream) {
    InputStream prevInStream = this.in;
    this.in = instream;
    return prevInStream;
  }

  /** Get the output stream
   *  @return the output stream to write/display text to user.
   *  @since NeXtMidas 3.3.0
   */
  public OutputStream getOutputStream () {
    return out;
  }

  /** Set the output stream to write/display text to user.
      @param outstream Output Stream
      @return previous Output Stream (null if none)
      @since NeXtMidas 3.3.0
   */
  public OutputStream setOutputStream (OutputStream outstream) {
    OutputStream prevOutStream = this.out;
    this.out = outstream;
    return prevOutStream;
  }

  /** Get Shell used to get history from if not null.
      @since NeXtMidas 3.3.2
      @return the Shell to get history from, null if no Shell has been set on this terminal
   */
  public Shell getShell () {
    return shell;
  }

  /** Set Shell used to get history from.
      @param shell Shell to get history from (null to reset to none)
      @since NeXtMidas 3.3.2
   */
  @InternalUseOnly
  public void setShell (Shell shell) {
    this.shell = shell;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // SUPPORT METHODS FOR TAB-COMPLETE
  //////////////////////////////////////////////////////////////////////////////////////////////////
  private static final String[] CWD_ONLY = { "CWD" }; // Array with only CWD, used by findFiles(..)

  /** Tries to tab-complete a command string.
      @param input The command string.
      @return Returns null if no substitution is found. Otherwise returns an array where element 0
              is the substitution string. If the array has length greater than 1 then the other
              elements are the fill-in suggestions.
   */
  private String[] tabComplete (String input) {
    List<String> matches  = null;  // Possible matches for the last element entered
    String[]     results  = null;  // Possible sub-substitutions for output or output string
    boolean      keepCase = false; // Keep case?

    Parser parser = new Parser(input.trim());
    int    index  = parser.elements();
    String last   = null;

    if ((index < 1) || input.endsWith(" ")) {
      // Don't even bother... they need to put in at least one letter.
    } else {
      last = parser.get(index);

      if (index == 1) { // Command name
        if (last.startsWith("%")) {
          matches = findMatches(Dictionary.findPercentMacros(M), last.toUpperCase(), null);
        } else {
          matches = M.dictionary.findMatches(M, last.toUpperCase());
          if (matches.size() == 1) {
            String cmdName = matches.get(0);
            if (last.indexOf(';') < 0) {
              cmdName = M.dictionary.findName(cmdName); // Simplify name when appropriate
            }
            matches.set(0, cmdName + " ");
          }
        }
      } else if (last.length() == 0) {
        // Ignore this case.
      } else if (last.lastIndexOf("^") > 0) {
        // Not supported at this time
      } else if (last.startsWith("^")) {
        matches = findMatches(M.results, last, ""); // A result with a caret
      } else {
        // Note if the first character in a command is a '/' it is probably the complete
        // path to a Unix command (e.g. "/bin/ls").
        String  first           = parser.get(1).toUpperCase();
        String  command         = (first.indexOf('/') <= 1)? first: first.substring(0,first.lastIndexOf('/'));
        boolean osCommand       = (command.charAt(0) != '%') && (M.dictionary.findMatch(command) < 0);
        boolean url             = (last.indexOf(":") > 2); // Is URL, not Windows drive letter.
        boolean firstIsLetter   = Character.isLetter(last.charAt(0));
        boolean qualsOrTrimmers = (last.indexOf('{') >= 0) || (last.indexOf('(') >= 0);
        boolean nxmDot          = last.startsWith("nxm.");

        matches  = new ArrayList<>();
        keepCase = true;

        if (firstIsLetter && !url && !osCommand && !qualsOrTrimmers) {
          matches.addAll(findMatches(M.results, last, "")); // Try results
        }

        if ((matches.size() == 0) && !qualsOrTrimmers) { // try file name
          if (nxmDot && !osCommand) { // try nxm.<opt>.<area>.<name>.<ext> syntax
            // TBD
          } else {
            String fname = last;
            String ext   = null;
            if (fname.indexOf('.') > 0) {
              ext   = fname.substring(fname.indexOf('.')+1);
              fname = fname.substring(0,fname.indexOf('.'));
            }
            matches.addAll(findFiles(fname, ext, osCommand));    // Try with ext.
            if ((matches.size() == 0) && (ext != null)) {
              matches.addAll(findFiles(last, null, osCommand));  // Try without ext.
            }
          }
        }
      }
    }

    if ((matches != null) && (matches.size() > 0) && (last != null)) {
      int    end    = input.lastIndexOf(last);
      String prefix = (end>=0)? input.substring(0, end): null;

      if (matches.size() == 1) {
        results = new String[] { prefix + matches.get(0) };

      } else if (prefix != null) {
        results = new String[matches.size() + 1];
        int count = findCommonCharCount(matches, last.length(), !keepCase);

        results[0] = prefix + matches.get(0).substring(0,count);

        for (int i = 1; i < results.length; i++) {
          results[i] = matches.get(i-1);
        }
      }
    }

    return results;
  }

  /** Given an list of 1 or more strings, this finds the number of common characters.
      @param strs       The list of strings.
      @param startLen   The start length (>=0).
      @param ignoreCase Ignore case while checking.
      @return The index of the last common character or start if none found.
   */
  private int findCommonCharCount (List<String> strs, int startLen, boolean ignoreCase) {
    String str = strs.get(0);

    for (int ii = 1; (ii < strs.size()) && (str.length() > startLen); ii++) {
      String next = strs.get(ii);

      if (str.length() > next.length()) { str  =  str.substring(0, next.length()); }
      else                              { next = next.substring(0, str.length() ); }

      while ((str.length() > startLen) && !equals(str,next,ignoreCase)) {
        next = next.substring(0, str.length()-1);
        str  =  str.substring(0, str.length()-1);
      }
    }

    return str.length();
  }

  /** Checks string equality with option to ignore case. */
  private boolean equals (String str1, String str2, boolean ignoreCase) {
    return (ignoreCase)? str1.equalsIgnoreCase(str2): str1.equals(str2);
  }

  /** Finds all keys in a string array matching a given prefix.
      @param keys   The String array to find matches for the given prefix.
      @param prefix The prefix.
      @param name   Name to prepend to match result(s) (if not null).
      @return The list of matches.
   */
  private List<String> findMatches (String[] keys, String prefix, String name) {
    ArrayList<String> matches  = new ArrayList<>();
    String            prefixUC = prefix.toUpperCase();

    for (String key : keys) {
      if (key.startsWith(prefixUC)) {
        if (name == null) { matches.add(key+" ");      }
        else              { matches.add(name+key+" "); }
      }
    }
    return matches;
  }

  /** Finds all keys in a table matching a given prefix.
      @param tbl    The table.
      @param prefix The prefix.
      @return The list of matches.
   */
  private List<String> findMatches (Table tbl, String prefix, String name) {
    boolean caret = prefix.startsWith("^");
    if (caret) { prefix = prefix.substring(1); }
    String prefixUC = prefix.toUpperCase();
    List<String> matches = null;

    if (prefix.indexOf('.') > 0) {
      String tblName = prefixUC.substring(0, prefixUC.indexOf('.'));
      String tagName = prefixUC.substring(prefixUC.indexOf('.')+1);
      Table  table   = tbl.getTable(tblName);

      if (table != null) {
        String tname = (caret)? ("^"+name): name;
        matches = findMatches(table, tagName, tname+tblName+".");
      } else {
        matches = new ArrayList<>(); // Empty list.
      }
    } else {
      String[] keys = tbl.getKeys();
      matches = new ArrayList<>();
      for (int i = 0; i < keys.length; i++) {
        if (keys[i].startsWith(prefixUC)) {
          String tname = name+keys[i];
          if (caret)                                { tname = "^"+tname;   }
          if (!(tbl.get(keys[i]) instanceof Table)) { tname = tname + " "; }
          matches.add(tname);
        }
      }
    }
    return matches;
  }

  /** Finds any matching files in the read aux.
      @param fname  The file name.
      @param ext    The file extension (can be null).
      @return The list of matches.
   */
  private List<String> findFiles (String fname, String ext, boolean noAux) {
    if (ext == null) { fname = fname+"*"; ext = "*"; }
    else             { ext = ext + "*";              }

    ArrayList<String> matches = new ArrayList<>();
    boolean isRootDir = fname.startsWith(File.separator); // Indicates root directory

    if ((fname.length() > 2) && Character.isLetter(fname.charAt(0)) && (fname.charAt(1) == ':')) {
      isRootDir = true; // This is Windows.
    }

    if (isRootDir) {
      int index = fname.lastIndexOf(File.separatorChar);
      String path = fname.substring(0,index+1);
      String file = fname.substring(index+1);
      addFilesToList(matches, path, file, ext, null, true);
    } else {
      String[] keys = (noAux)? CWD_ONLY : M.io.getReadAux();
      for (String key : keys) {
        if (Shell.getCurrent().isExcludedAux(key))  continue;  // Bug 2482 (3.1.0)
        String append = (matches.size() == 0)? null: "{AUX="+key+"}";
        addFilesToList(matches, M.io.getAuxPath(key), fname, ext, append, false);
      }
    }
    return matches;
  }

  /** Adds files to a list. Only used by findFiles(..).
      @param matches The list of matching files.
      @param path    The file path.
      @param fname   The file name.
      @param ext     The file extension ("dir" for a directory - matches CoreIO.listFiles(..)).
      @param append  Extra information to append to the file name when noAux=false.
      @param noAux   Not listing in an AUX, allow following of directories.
   */
  private void addFilesToList (List<String> matches, String path, String fname, String ext,
                              String append, boolean noAux) {
    String[] files = M.io.listFiles(path, fname, ext);
    if (files != null) {
      for (int j = 0; j < files.length; j++) {
        String file = files[j];
        if (noAux) {
          file = path + file;
          if (!CoreIO.isDirectoryName(file)) { file = file + " "; }
        } else if (append != null) {
          file = file + append + " ";
        } else {
          file = file + " ";
        }
        matches.add(file);
      }
    }
    if (noAux && ext.equals("*")) {
      addFilesToList(matches, path, fname, "dir", append, noAux); // Also include any directories.
    }
  }
}
