package nxm.sys.lib;

import java.util.ArrayList;
import java.util.regex.Pattern;

import nxm.sys.inc.AsciiMap;
import nxm.sys.inc.InternalUseOnly;

/** A class to handle parsing a string into tokens. All grouping pairs (i.e. <code>(...), [...],
    {...}</code>) must be balanced and closed before a delimiter is recognized, except when
    inside quotes.  Valid delimiters before cleaning are white space and the comma. Using white
    space as a delimiter may be disabled but is enabled by default. After cleaning, only the current
    delimiter (the comma by default) is recognized. <br>
    <br>
    Some of the most commonly used methods in this class are those used to parse lists of options
    or mask values:
    <pre>
      <u>String -&gt; Number</u>
      find(list,str,def)     - Finds the list value in the string with a given default.
      find(list,str,def,off) - Same as find(list,str,def) but with an index offset.
      mask(list,str,def)     - Gets the mask value for str from the list where the
                               default mask value is given.
      <u>Number -&gt; String</u>
      get(list,i)            - Gets the i'th element from the list.
      get(list,i,off)        - Same as get(list,i) but with an index offset.
      mask2s(list,mask)      - Converts a mask value to the associated string.
    </pre>

    @author Jeff Schoen / Jay Hallam
*/
public class Parser extends Object implements AsciiMap, java.io.Serializable {

  /** NOTE: this should be changed if the structure/fields of this class changes. */
  private static final long serialVersionUID = 2011101400310L; // for Serializable interface

  /** Bit-mask value indicating that no options are selected.
      Value: <code>{@value}</code> (no bits are set).
      @see #MASK_ALL
      @see #mask(String,String,int)
   */
  public static final int MASK_NONE = 0;

  /** Bit-mask value indicating that all options are selected.
      Value: <code>{@value}</code> (all bits are set).
      @see #MASK_NONE
      @see #mask(String,String,int)
   */
  public static final int MASK_ALL = -1;

  /** Do not cause exception or print a warning to the user if the match is not in the given list.
      @see #find(String, String, int, int, int)
      @since NeXtMidas 2.9.0
   */
  public static final int E_NONE = 0;

  /** Print a warning to the user if the match is not in the given list.
      @see #find(String, String, int, int, int)
      @since NeXtMidas 2.9.0
   */
  public static final int E_WARNING = 1;

  /** Throw an exception if the match is not in the given list.
      @see #find(String, String, int, int, int)
      @since NeXtMidas 2.9.0
   */
  public static final int E_ERROR = 2;

  /** Default exception option when match is not found */
  public static final int E_DEFAULT = E_NONE;

  /** Pre-compiled regular expression pattern to split strings based on commas ',' and/or pipes '|'. */
  static Pattern COMMA_PIPE_REGEX = Pattern.compile("[,|]"); // package-private for now


  /** The delimited string (in a char array).
      <b>Note:</b> The actual length of the string may or may not be the same as the length of the buffer.
      @see #length
   */
  public char buffer[];

  /** The actual length of the delimited string. */
  public int length;

  /** The index of the current element. */
  public int curindex=0;

  /** The buffer offset of the current element. */
  public int curoffset;

  /** The length of the current element. */
  public int curlength;

  /** The special switch handler length. */
  public int switchlen=0;

  /** The delimiter character used. This usually defaults to a comma. */
  public char delimiter = COMMA;

  /** Flag to determine whether white space is used as a delimiter in the {@link #lengthd(int)} function.  */
  private boolean useSpaceDelimiter=true;

  /** Flag to denote whether or not to ignore characters after a backslash. Used in methods like {@link #lengthd(int)} and {@link #clean()}
      @since NeXtMidas 2.9.0   (Bug 2292)
   */
  private boolean treatBSlashAsEscape = false;   // added for (Bug 2292)

  /** Initializes a parseable string object.
      @param input The string to parse.
      @todo JC4: This should just call <code>Parser(input.toCharArray())</code>.
   */
  public Parser (String input) {
    length = input.length();
    buffer = new char[length+1];
    input.getChars(0,length,buffer,0);
    buffer[length]=SPACE;
  }

  /** Initializes a parseable string object and clean it as needed. This will initialize the
      instance and then, if <code>doClean</code> is <code>true</code>, immediately call
      {@link #clean()}.
      @param input   The input string.
      @param doClean Indicates if {@link #clean()} should be called.
   */
  public Parser (String input, boolean doClean) {
    this(input,doClean,true);
  }

  /** Initializes a parseable string object and clean it as needed. This will initialize the
      instance and then, if <code>doClean</code> is <code>true</code>, immediately call
      <code>clean(upCase)</code>.
      @param input   The input string.
      @param doClean Indicates if {@link #clean()} should be called.
      @param upCase  Indicates whether to upcase during clean
   */
  public Parser (String input, boolean doClean, boolean upCase) {
    this(input);
    if (doClean) clean(upCase);
  }

  /** Initializes a parseable string object using the specified delimiter.
      @param input     The string to parse.
      @param delimiter The delimiter to use.
   */
  public Parser (String input, char delimiter) {
    this(input);
    setDelimiter(delimiter);
  }

  /** Initializes a parseable string object using the specified delimiter and
      calling {@link #clean(boolean)} which upcases chars outside quotes.
      @param input     The string to parse.
      @param delimiter The delimiter to use.
      @param doClean Indicates if {@link #clean()} should be called.
      @since NeXtMidas 3.1.0
   */
  public Parser (String input, char delimiter, boolean doClean) {
    this(input,delimiter);
    if (doClean) clean(true);
  }

  /** Initializes a parseable string object.
      @param input     The string to parse, as an array.
   */
  public Parser (char input[]) {
    buffer = input;
    length = input.length;
  }

  /** Initializes a parseable string object.
      @param input       The string buffer holding the string to parse.
      @param inputlength The actual length of the string.
   */
  public Parser (char input[], int inputlength) {
    buffer = input;
    length = inputlength;
  }

  /** Set the "use-space-as-delimiter" flag.
      @param flag Flag that indicates if a space is to be interpreted as a
                  delimiter (<code>true</code>) or not (<code>false</code>).
   */
  public void setUseSpaceDelimiter (boolean flag) {
    useSpaceDelimiter=flag;
  }

  /** Get the "use-space-as-delimiter" flag.
      @return The flag that indicates if a space is to be interpreted as a
              delimiter (<code>true</code>) or not (<code>false</code>).
   */
  public boolean getUseSpaceDelimiter () {
    return useSpaceDelimiter;
  }

  /** Sets the delimiter character to use.
      @param ch The delimiter character to use.
   */
  public void setDelimiter (char ch) {
    delimiter = ch;
  }

  /** Cleans the string by compressing white space, converting delimiters,
      and uppercasing all characters outside of quotes. <br>
      <br>
      This is identical to <code>clean(true)</code>.
      @see #clean(boolean)
   */
  public void clean () { clean(true); }


  /** Cleans the string by compressing white space and converting delimiters.
      @param upcase Indicates if all characters outside of quotes should be converted to uppercase.
   */
  public void clean (boolean upcase) {
    char c;              // current char
    char clast = SPACE;  // last char
    int j,paren=0,curly=0;
    boolean tableWillCleanWhiteSpace = false;  // We are in a Table and the Table class will clean the white space

    for (int i=j=0; i<length; i++) {
      c = buffer[i];
      // Check to see if the equals is inside the curly brace (i.e. we are in a table) since 3.8.1 for NXM-3143
      if ((curly > 0) && (c == '=')) {
        // If consistent handling of whitespace is being done in Table, let Table.fromBytes handle the cleaning of this Table
        tableWillCleanWhiteSpace =  (Table.getDefaultFlagsInt() & Table.DISABLE_CONSISTENT_HANDLING_OF_WHITESPACE) == 0;
      }
      if (c <= SPACE &&  paren == 0) {
        if (clast != SPACE  &&  clast != delimiter) {
          buffer[j++] = delimiter;
          clast = SPACE;
        }
      }
      else if (c == delimiter) {
          if (tableWillCleanWhiteSpace || (clast != SPACE)) {  // since 3.8.1, also keep all chars if table is going to do the cleaning
            buffer[j++] = c;
          }
          clast = c;
      }
      else if (((c == QUOTE) || (c == BTIC))&& !tableWillCleanWhiteSpace) {

        char cqt = c;
        buffer[j++] = cqt;
        boolean escaped = false;
        while (i<length) {
          c = buffer[++i];
          buffer[j++] = c;
          //// START BUG 2292 CHANGES   ///
          if (escaped) {
            escaped = false;
            if (c==QUOTE)   continue;   // skip quote if it was escaped
          }
          else if (c==BSLASH && treatBSlashAsEscape)  escaped = true;
          ///   END BUG 2292 CHANGES     ///
          if (c == cqt) break;
        }
        clast = cqt;
      }
      else {    /* all other characters */
        if (c >= LOWERA  &&  c <= LOWERZ) {
          if (upcase) c = java.lang.Character.toUpperCase(c);
        }
        else if (c==LPAREN || c==LBRACE || c==LCURLY) {
          paren++;
          if (c==LCURLY) curly++;
        }
        else if (c==RPAREN || c==RBRACE || c==RCURLY) {
          if (paren > 0) paren--;
          if (c==RCURLY) curly--;
        }
        if(curly==0) {
          // Definitely no longer in a Table
          tableWillCleanWhiteSpace = false;
        }
        buffer[j++] = c;
        clast = c;
      }
    }
    // make sure trailing spaces don't signal an extra argument
    if (j>0 && buffer[j-1]==delimiter && clast==SPACE) j--;
    length = j;
  }

  /** Returns the length to the next delimiter, starting at a given offset.
      @param index The offset to start searching for the next delimiter from (<b>zero-based</b>).
      @return The number of characters that occurs before the next delimiter is
              reached. (If the <code><i>index</i></code>'th character is a
              delimiter this will return <code>0</code>.)
      @todo JPH: This prohibits 0-9 and A-Z  from being delimiters and is only
                 here for speed. Recommend removing this after the 1.8x release.
   */
  public int lengthd (int index) {
    char c;
    int i, len, tic=0, quote=0, paren=0;
    boolean inswitch = false;
    boolean escaped  = false;

    for (i=index; i<length; i++) {
      c = buffer[i];
      len = i-index;


      if (c==BSLASH && treatBSlashAsEscape) {
        escaped = !escaped;  // account for escaping a BackSlash itself
        continue;
      }
      if (escaped) {
        escaped = false;   // Check for escape char added for Bug 2292
        continue;
      }

      /* if not a delimiter, keep going.
       NOTE: JPH: This prohibits 0-9 and A-Z from being delimiters and is only
       here for speed.   Recommend removing this after the 1.8x release. */
      if ( (c >= ZERO   && c <= NINE) ||
           (c >= UPPERA && c <= UPPERZ) ) continue;

      /* in tic|quotes, only another tic|quote is important */
      if (tic == 1) { if (c == BTIC) tic=0; continue; }
      if (quote == 1) { if (c == QUOTE) quote=0; continue; }

      /* check for the delimiter */
      if (c == delimiter && paren == 0) return (len);

      /* switch on other special characters */
      switch (c) {
      case SPACE:
        if (useSpaceDelimiter && paren == 0) return (len);
        break;
      case BTIC:
        tic = 1-tic;
        break;
      case QUOTE:
        quote = 1-quote;
        break;
      case LPAREN: case LBRACE: case LCURLY:
        paren++;
        break;
      case RPAREN: case RBRACE: case RCURLY:
        if (paren > 0) paren--;
        break;
      case SLASH:
        if (inswitch && quote == 0 && paren == 0) return (len);
        if (i == index && i<switchlen) inswitch = true;
        break;
      default:
        if (useSpaceDelimiter && c <= SPACE && paren == 0) return (len);
      }
    }

    /* no trailing delimiter */
    if (paren == 0) return (length-index);

    /* trim trailing white space */
    for (i=length-1; i >= 0 && buffer[i] <= SPACE; i--) { }

    return (i-index+1);
  }

  /** Set whether or not to ignore characters after a {@link AsciiMap#BSLASH}
      @param val true if should ignore characters after an ascii slash
      @since NeXtMidas 2.9.0
   */
  public void setTreatBSlashAsEscape (boolean val ) {
    treatBSlashAsEscape = val;  // added for Bug 2292
  }

  /** Get whether or not to ignore characters after a back slash
   *  @return whether or not to ignore characters after a {@link AsciiMap#BSLASH}
   *  @since NeXtMidas 2.9.0
   */
  public boolean isTreatBSlashAsEscape () { return treatBSlashAsEscape; }

  /** Returns the entire parseable string, as it currently exists.
      @return The entire string.
   */
  public String getAll () {
    return ( new String(buffer,0,length) );
  }

  /** Returns an element as a string.
      @param element The element to get (<b>one-based</b>).
      @return The <code><i>element</i></code>'th element in the parsed string.
   */
  public String get (int element) {
    return get(element, false);
  }

  /** Returns an element as a string.
      @param element        The element to get (<b>one-based</b>).
      @param addLeadingChar Include the leading comma/space?
      @return The <code><i>element</i></code>'th element in the parsed string.
      @since NeXtMidas 3.3.2
   */
  @InternalUseOnly // Only used by Args
  synchronized String get (int element, boolean addLeadingChar) {
    if (element==0) throw new MidasException("Parser class is 1 based");
    if (curindex == 0 || element < curindex) {
      curindex  = 1;
      curoffset = 0;
      curlength = lengthd(curoffset);
    }
    while (curindex < element) {
      curindex++;
      curoffset += (curlength+1);
      if (curoffset > length) curlength = 0;
      else curlength = lengthd(curoffset);
    }

    if ((curoffset >= length) || (curlength <  0)) {
      return ""; // not found
    }
    else if (addLeadingChar && (curoffset > 0)) {
      // Note that this *may* include a case there curlength=0, but the inclusion
      // of the leading char means the resulting string is not empty.
      return new String(buffer,curoffset-1,curlength+1);
    }
    else if (curlength == 0) {
      return ""; // skip extra object creation
    }
    else {
      return new String(buffer,curoffset,curlength);
    }
  }

  /** Gets a portion of the string being parsed.
      @param offset The index (<b>zero-based</b>) of the first character to get.
      @param length The number of characters to get.
      @return A portion of the string being parsed.
   */
  public String get (int offset, int length) {
    return (new String(buffer,offset,length) );
  }

  /** Returns whether there are more elements to parse.
      @return <code>true</code> if there are more elements to parse, and
              <code>false</code> if there are no more elements to parse.
      @see #next()
      @see #reset()
   */
  public boolean more() {
    return (curoffset+curlength < length);
  }

  /** Resets the filter to start at front of string again. This will reset
      {@link #more()} and {@link #next()}.
      @see #more()
      @see #next()
   */
  public void reset() {
    curindex=0; curoffset=0; curlength=0;
  }

  /** Returns the next element of the parsed string.
      @return The next element of the parsed string.
      @see #more()
      @see #reset()
   */
  public String next() {
    return get(curindex+1);
  }

  /** Get the number of elements
   *  @return the number of elements*/
  public synchronized int elements() {
    int elem=0;
    for  (int i=0; i<length; i+=(lengthd(i)+1)) elem++;
    return elem;
  }

  /** Return an array of all the elements of the string up to a maximum number of specified elements.
      @param maxelem The maximum number of elements to return (0 for exact size of current)
      @return An array of all the elements of the string.
   */
  public String[] getArray (int maxelem) {
    ArrayList<String> list = new ArrayList<>();
    int n=0; curoffset=0;
    while (more()) list.add(get(++n));
    if (maxelem==0) maxelem=n;
    String s[] = new String[maxelem];
    for (int i=0; i<n && i<maxelem; i++) s[i]=list.get(i);
    for (int i=n; i<maxelem; i++) s[i]="";
    return s;
  }

  /** Return a delimited element by index (with indexing offset).
      This method is identical to <code>get(string,element-offset)</code>.
      @param string  The string to get the indexed element of.
      @param element The number of the element to get (<b>one-based</b>).
      @param offset  Alters the element indexing. (Example: Setting this to
                     -1 effectively makes <code>element</code> zero-based.)
      @return The specified element in the string.
   */
  public static String get (String string, int element, int offset) {
    return get(string,element-offset);
  }

  /** Return a delimited element by index. This method always uses a comma as the delimiter.
      @param string  The string to get the indexed element of.
      @param element The number of the element to get (<b>one-based</b>).
      @return The specified element in the string.
   */
  public static String get (String string, int element) {
    if (element<=0) return "";
    int i=-1,j,n;
    for (n=1; n<element; n++) {
      i = string.indexOf(',',i+1);
      if (i<0) return "";
    }
    j = string.indexOf(',',i+1);
    if (j<0) j= string.length();
    return string.substring(i+1,j);
  }

  /** Return a delimited element by index that has been converted to uppercase. This method is
      identical to <code>get(string,element)</code> except that it will convert any string to
      uppercase.
      @param string  The string to get the indexed element of.
      @param element The number of the element to get (<b>one-based</b>).
      @return The specified element in the string.
   */
  public static String getUC (String string, int element) {
    String tmp = get(string,element);
    if (tmp != null) tmp = tmp.toUpperCase();
    return tmp;
  }

  /** Finds a token that starts with the given match string and return the index of that token.
      This is identical to <code>find(list,match,defindex,0)</code>.
      @param list     The list of possible values.
      @param match    The value (or first part of the value) to search for.
      @param defindex The default index to return if <code>match</code> is the empty string.
      @return The index of the string to find (<b>one-based</b>).
   */
  public static int find (String list, String match, int defindex) {
    return find(list,match,defindex,0,E_DEFAULT);
  }

  /** Finds a token that starts with the given match string and return the index of that token.
      This method uses a two-pass search strategy where the first pass is case-sensitive and if
      no match is found a case-insensitive search is used.
      @param list     The list of possible values.
      @param match    The value (or first part of the value) to search for.
      @param defindex The default index to return if <code>match</code> is the
                      empty string.
      @param offset   The basis offset for the value returned (<code>0</code>
                      for a one-based return value, <code>-1</code> for a
                      zero-based return value, etc.).
      @return The index of the string to find, or<br>
              <code><i>offset</i>-1</code> if no match is found, or<br>
              <code>defindex</code> if <code>match</code> is the empty string.
      @see #findIndex(String, String, int, int, String, boolean) for a version that does
           NOT do a starts with string comparison.
   */
  public static int find (String list, String match, int defindex, int offset) {
    return find(list,match,defindex,offset,E_DEFAULT);
  }

  /** Finds a token that starts with the given match string and return the index of that token.
      This method uses a two-pass search strategy where the first pass is case-sensitive and if
      no match is found a case-insensitive search is used.
      @param list     The list of possible values.
      @param match    The value (or first part of the value) to search for. Since 3.3.0, if there
                      are multiple possible matches found, a warning is issued in the terminal.
      @param defindex The default index to return if <code>match</code> is the
                      empty string.
      @param offset   The basis offset for the value returned (<code>0</code>
                      for a one-based return value, <code>-1</code> for a
                      zero-based return value, etc.).
      @param notify   How to notify the user of the match is not found in the list.
                      Must be either {@link #E_NONE}, {@link #E_WARNING}, or {@link #E_ERROR}
      @return The index of the string to find, or<br>
              <code><i>offset</i>-1</code> if no match is found, or<br>
              <code>defindex</code> if <code>match</code> is the empty string.
      @see #findIndex(String, String, int, int, String, boolean) for a version that does
           NOT do a starts with string comparison.
      @since NeXtMidas 2.9.0
   */
  public static int find (String list, String match, int defindex, int offset, int notify) {
    if (match==null || match.length()==0) return defindex;
    if (notify!=E_NONE && notify!=E_WARNING && notify!=E_ERROR) notify = E_DEFAULT;
    int numMatches = 0;
    int firstMatch = -1;
    for (int i=-1,j=0,k=match.length(); i<list.length(); i++) {
      if (i == -1 || list.charAt(i)==',' || list.charAt(i)=='|') {
        j++;
        if (match.regionMatches(0,list,i+1,k)) {
          if (++numMatches==1) {
            firstMatch = offset+j;  // in the future we may want to make this the 'best' match, not just the first one...
            if (perfectMatch(match, list.substring(i+1), k) ) return firstMatch;  // perfect match so exit loop
          }
        }
      }
    }
    if (numMatches > 0) {
      if (numMatches > 1) {  // since 3.3.0 (Bug 2680)
        Shell.getSharedMidasContext().warning("Ambiguous items match '"+match+"'. Returning first match in list ["+list+"]");
      }
      return firstMatch;
    }
    // not found, try upper case
    match = match.toUpperCase();
    list = list.toUpperCase();
    for (int i=-1,j=0,k=match.length(); i<list.length(); i++) {
      if (i == -1 || list.charAt(i)==',' || list.charAt(i)=='|') {
        j++;
        if (match.regionMatches(0,list,i+1,k)) {
          if (++numMatches==1) {
            firstMatch = offset+j; // in the future we may want to make this the 'best' match, not just the first one...
            if (perfectMatch(match, list.substring(i+1), k)) return firstMatch;  // perfect match so exit loop
          }
        }
      }
    }
    if (numMatches > 0) {
      if (numMatches > 1) {  // since 3.3.0 (Bug 2680)
        Shell.getSharedMidasContext().warning("Ambiguous items match '"+match+"'. Returning first match in list ["+list+"]");
      }
      return firstMatch;
    }
    // still not found, if it is a number then return that number value
    // NTN TODO: Ask Jeff if he is using this "undocumented feature"
    char c = match.charAt(0);
    if ( (c>='0' && c<='9') || c=='-') {
      // put conversion in try/catch block for BUG 2259
      try { return Convert.s2l(match); }
      catch (MidasException e) { } // ignore (i.e. not found)
    }
    if      (notify==E_ERROR)   Shell.getSharedMidasContext().error("Item: "+match+" not found in list: "+list);
    else if (notify==E_WARNING) Shell.getSharedMidasContext().warning("Item: "+match+" not found in list: "+ list);
    else if (notify==E_NONE)  {}    // do nothing
    return offset-1; // not found
  }

  /** returns true if the match String exactly matches the list, or an item in the list  */
  private static boolean perfectMatch (String match, String list, int len) {
    // since 3.3.0, added for Bug 2680
    if (match.length()==list.length()) {
      if (match.equals(list)) return true;
    }

    else if (match.length()<list.length()) {
      char c = list.charAt(len);
      if (match.equals(list.substring(0,len)) && (c==',' || c=='|')) return true;
    }

    return false;
  }

  /** Create a mask from a given list and a zero-bit-indexed list of options.
      @param list      List of comma-separated (<code>','</code>) mask options
                       that are available to be selected. The following implicit
                       options are also available for selection:
                       <pre>
                         DEF     --&gt; Equal to <i>defmask</i>.
                         DEFAULT --&gt; Equal to <i>defmask</i> (since 2.3.0).
                         ALL     --&gt; Equal to all options being set.
                         NONE    --&gt; Equal to {@link #MASK_NONE} = {@value #MASK_NONE}.
                       </pre>
      @param matchlist Sublist of pipe-separated (<code>'|'</code>) masks set.
                       An item in this list may take the following forms:
                       <pre>
                         +OPTION  --&gt; Default  plus  OPTION.
                         -OPTION  --&gt; Default  less  OPTION.
                         ~OPTION  --&gt; Default toggle OPTION (inverts the
                                      inclusion of OPTION in the default mask).
                       </pre>
                       Note: item/option can be a truncated string of those in the list.
                             However, the first startsWith match from the list will be used.
                       <br>
                       For legacy reasons it is possible to manually set the
                       mask value to a specified hexadecimal value if given
                       in the form <code>0X...</code> (this functionality is deprecated).
      @param defmask   The default mask to use.
      @return The integer mask for the given match list, or <code><i>defmask</i></code> if
              <code>matchlist</code> is empty or <code>null</code>.
  */
  public static int mask (String list, String matchlist, int defmask) {
    int mask=0,mode=-2,i,j,k; String match;
    if (matchlist==null || matchlist.length()==0) return defmask;
    matchlist = matchlist.toUpperCase();
    if (matchlist.startsWith("0X")) {
      mask = Convert.o2l(matchlist);
      //TODO: In 2.7.0 we will add in this warning...
      //Shell.deprecate("Use of hex values (via OX..) for masks are deprecated since NeXtMidas 3.?.?, use "
      //               +Parser.mask2s(list, mask)+" in place of "+matchlist+".");
      return mask;
    }
    list = list.toUpperCase();
    while (matchlist.length()>0) {
      i = matchlist.indexOf('|');
      if (i<0) { match=matchlist; matchlist=""; }
      else { match=matchlist.substring(0,i); matchlist=matchlist.substring(i+1); }
      if (i==0) continue;
           if (match.startsWith("+")) { match=match.substring(1); mode=1; }
      else if (match.startsWith("-")) { match=match.substring(1); mode=0; }
      else if (match.startsWith("~")) { match=match.substring(1); mode=-1; }
      k = MASK_NONE;
      if (list.startsWith(match)) k = 0x1;
      else {
        i = list.indexOf(","+match)+1;
        if (i>0) {
          for (j=0; i>0; i--) if (list.charAt(i)==',') j++;
          k = (1<<j);
        }
        else if (match.equals("DEF")) k = defmask;
        else if (match.equals("DEFAULT")) k = defmask;
        else if (match.equals("ALL")) k = maskAll(list);
        else if (match.equals("NONE")) k = MASK_NONE;
        else Shell.warning("Illegal mask value: "+match+" in "+list);
      }
           if (mode==1) mask = defmask = defmask|k;
      else if (mode==0) mask = defmask = defmask&(~k);
      else if (mode==-1) mask = defmask = defmask^k;
      else mask = mask | k;
    }
    return mask;
  }

  /** Converts a mask value into a printable format. This can be combined with
      {@link #mask(String,String,int)}, i.e. <code>mask2s(list,mask(list,matchlist,defmask))</code>.
      @param list List of comma-separated (<code>','</code>) mask options that are available.
      @param mask The mask which indicates the selected mask options.
      @return A printable string indicating the selected mask options. This
              string will always begin and end with a <code>'|'</code> and will
              be equal to <code>"|"</code> if <code><i>mask</i></code> is zero.
   */
  public static String mask2s (String list, int mask) {
    StringBuilder sb = new StringBuilder("|");
    for (int i=0; i<32; i++) {
      if ( ((mask>>i)&0x1) == 1) {
        String item = Parser.get(list,i+1);
        if (item!=null) sb.append(item).append('|');
      }
    }
    return sb.toString();
  }

  private static int maskAll (String list) {
    int mask = 0x1;
    for (int i=0; i<list.length(); i++) if (list.charAt(i)==',') mask = (mask<<1) | 0x1;
    return mask;
  }

  /** Find a token that starts with the given string and return that token.
      This is identical to using {@link #find(String,String,int)} except that
      it returns the value of the match rather than the index of the match.
      @param string   The list of possible values.
      @param match    The value (or first part of the value) to search for.
      @param defvalue The default value to return if <code>match</code> is the
                      empty string.
      @return The value found or <code><i>defvalue</i></code> if <code>match</code> is empty.
   */
  public static String find (String string, String match, String defvalue) {
    int item = find (string,match,0);
    if (item==0) return defvalue;
    else return get (string,item);
  }

  /** Find a token that starts with the given string and return that token.
      This is identical to using {@link #find(String,String,String)} except that
      it converts it's return value to uppercase.
      @param string   The list of possible values.
      @param match    The value (or first part of the value) to search for.
      @param defvalue The default value to return if <code>match</code> is the empty string.
      @return The value found or <code>defvalue.toUpperCase()</code> if <code>match</code> is the empty string.
      @todo JC4: Replace this method with a call to <code>find(string,match,defvalue).toUpperCase()</code>.
   */
  public static String findUC (String string, String match, String defvalue) {
    int item = find (string,match,0);
    if (item==0) return defvalue.toUpperCase();
    else return getUC (string,item);
  }

  /** Returns the index of the first occurrence of the token, ignoring case.
      This is identical to <code>text.toUpperCase().indexOf(token)</code>.
      @param token The token to look for.
      @param text  The text to look for the token in.
      @return The indexed of the token (<b>zero-based</b>).
      @exception NullPointerException If <code>text</code> is <code>null</code>.
  */
  public static int indexOfIgnoreCase (String token, String text)
               throws NullPointerException {
     String upperCaseText = text.toUpperCase();
     return upperCaseText.indexOf(token.toUpperCase());
   }


  /** Finds the index of the first occurrence of the character token that occurs outside of
      quoted text.
      @param token The character to find the index of.
      @param text  The text to search in.
      @return The index of the token (<b>zero-based</b>) or <code>-1</code> if
              the <code>token</code> is not found.
      @todo JC4: Why does the second line of this method assume that
                 <code>token</code> is a {@link #SCOLON}??? <br>
                 <br>
            JC4: Why does this method assume only a {@link #QUOTE} can
                 be used to quote text when {@link #clean(boolean)} and
                 {@link #lengthd(int)} also allow a {@link #BTIC} to be used?
   */
  public static int findUnquoted (char token, String text) {
    int i;
    i = text.indexOf(SCOLON);
    if (i == -1)
      return i;   // no token in the string
    else if (text.indexOf(QUOTE) == -1)
      return i;   // no double quotes exist, return the token

    // Since double quotes and the token occur on the string,  parse each
    // character to see if the token occurs outside of quoted text
    char c;
    int numQuotes = 0;
    boolean found = false;
    int length = text.length();
    char buffer[] = new char[length];
    text.getChars(0,length,buffer,0);    // put string into char array
    int lastBufferIndex = length-1;
    for (i=0; i < length && !found; i++) {
      c = buffer[i];
      if (c == QUOTE) {
        if (i != lastBufferIndex && buffer[i+1] == QUOTE)
            i++;  // ignore two double quotes side-by-side (escaped)
        else {
            numQuotes++;
            if (numQuotes == 2) numQuotes = 0;   // we just finished quoted text
        }
      }
      else if (c == token) {
        if (numQuotes != 1)  // we are not inside quotes so this is the
          found=true;        // unquoted token we want
      }
    } // end for
    if (!found) i = -1;
    return i;
  }

  /** Converts the part of a string not enclosed by quotes to uppercase.
      @param string The string to convert the un-quoted characters of to uppercase.
      @return A copy of the string where any alphabetic characters not inside
              quotes have been converted to uppercase.
      @todo JC4: Why does this method assume use only a {@link #QUOTE} can
                 be used to quote text when {@link #clean(boolean)} and
                 {@link #lengthd(int)} also allow a {@link #BTIC} to be used?
   */
  public static String toUpperCaseWithQuotes (String string) {
    char c;
    char buffer[]   = null;
    boolean inQuote = false;
    for (int i=0; i<string.length(); i++) {
      c = string.charAt(i);
      if (c==QUOTE) inQuote = !inQuote;
      if (c>=LOWERA && c<=LOWERZ && !inQuote) {
        c += LOWER2UPPER;
        if (buffer==null) buffer = string.toCharArray();
      }
      if (buffer!=null) buffer[i]=c;
    }
    if (buffer!=null) string = new String(buffer);
    return string;
  }

  /** Loads the values of all switches in the given string into a table. For each switch an entry
      will be made in the table using <code>Table.put(key,value)</code> where <code>key</code> is
      the name of the switch (converted to uppercase, without the <code>'/'</code>) and
      <code>value</code> is the value given for the switch (or <code>"TRUE"</code> for a state switch).
      @param string The string containing the switches.
      @param table  The table to put the switches in.
      @return The index of the first switch in <code>string</code> or
              <code>-1</code> if there are no switches in the string.
      @see Table#put(String,Object)
      @todo JC4: Why doesn't this permit a switch to have a quoted value (that
                 may or may not include a <code>'/'</code>)?
   */
  public static int switchString (String string, Table table) {
    int i,j,isw=string.indexOf('/');
    while ( isw>=0 && (i=string.lastIndexOf('/'))>=isw) {
      String sw = string.substring(i+1);
      j = sw.indexOf('=');                                                // is it keyed ?
      if (j<0) table.put (sw.toUpperCase(),"TRUE");                       // state switch present
      else table.put (sw.substring(0,j).toUpperCase(),sw.substring(j+1)); // value switch
      string = string.substring(0,i);
    }
    return isw;
  }

  /** Finds the token index of the given match string (case-insensitive) within
      the list of possible values (ones base). This is identical to
      <code>findIndex(list, match, -1, 0, null, false)</code>.
      @param list     The list of possible values.
      @param match    The value to search for (case-insensitive)
      @return The ones-based index of the string to find, or<br>
              <code>-1</code> if no match is found or match is null or the empty string.
      @since NeXtMidas 2.5.5
   */
  public static int findIndex(String list, String match) {
    return findIndex(list, match, -1, 0, null, false);
  }

  /** Finds the token index of the given match string within the list of possible values.
      @param list     The list of possible values.
      @param match    The value to search for.
      @param defindex The default index to return if <code>match</code> is the
                      empty string or null.
      @param boffset  The basis offset for the value returned (<code>0</code>
                      for a one-based return value, <code>-1</code> for a
                      zero-based return value, etc.).
      @param delims   The delimiters to use to split tokens (defaults to ",|" if null)
      @param cs       True for a case-sensitive string comparison.
      @return The index of the string to find, or<br>
              <code><i>offset</i>-1</code> if no match is found, or<br>
              <code>defindex</code> if <code>match</code> is the empty string.
      @since NeXtMidas 2.5.5
      @see #find(String, String, int) for a startsWith version of this.
   */
  public static int findIndex(String list, String match, int defindex, int boffset,String delims, boolean cs) {
    Pattern regex;
    int foundIndex = -1;
    if (delims==null) { regex = COMMA_PIPE_REGEX;                }
    else              { regex = Pattern.compile("["+delims+"]"); }

    if (match==null || match.length()==0) return defindex;

    String strs[] = regex.split(list);
    for (int i=0; i<strs.length; i++) {
      String str = strs[i];
      boolean found = (cs)? str.equals(match) : str.equalsIgnoreCase(match);
      if (found) {
        foundIndex = i+1;
        break;
      }
    }

    return boffset+foundIndex;
  }

  // enable below toString to help with debugging parsed line
  // public String toString() { return new String(buffer); }
}
