/* $RCSfile$
 * $Author: hansonr $
 * $Date: 2007-04-26 16:57:51 -0500 (Thu, 26 Apr 2007) $
 * $Revision: 7502 $
 *
 * Copyright (C) 2005  The Jmol Development Team
 *
 * Contact: jmol-developers@lists.sf.net
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package javajs.util;

import java.lang.reflect.Array;
import java.util.Map;
import java.util.Map.Entry;

import javajs.J2SIgnoreImport;
import javajs.api.JSONEncodable;

/**
 * a combination of Parsing and Text-related utility classes
 * 
 * @author hansonr
 * 
 */

@J2SIgnoreImport(value = { java.lang.reflect.Array.class })
public class PT {

  public static int parseInt(String str) {
    return parseIntNext(str, new int[] {0});
  }

  public static int parseIntNext(String str, int[] next) {
    int cch = str.length();
    if (next[0] < 0 || next[0] >= cch)
      return Integer.MIN_VALUE;
    return parseIntChecked(str, cch, next);
  }

  public static int parseIntChecked(String str, int ichMax, int[] next) {
    boolean digitSeen = false;
    int value = 0;
    int ich = next[0];
    if (ich < 0)
      return Integer.MIN_VALUE;
    int ch;
    while (ich < ichMax && isWhiteSpace(str, ich))
      ++ich;
    boolean negative = false;
    if (ich < ichMax && str.charAt(ich) == 45) { //"-"
      negative = true;
      ++ich;
    }
    while (ich < ichMax && (ch = str.charAt(ich)) >= 48 && ch <= 57) {
      value = value * 10 + (ch - 48);
      digitSeen = true;
      ++ich;
    }
    if (!digitSeen)// || !checkTrailingText(str, ich, ichMax))
      value = Integer.MIN_VALUE;
    else if (negative)
      value = -value;
    next[0] = ich;
    return value;
  }

  public static boolean isWhiteSpace(String str, int ich) {
    char ch;
    return (ich >= 0 && ((ch = str.charAt(ich)) == ' ' || ch == '\t' || ch == '\n'));
  }

  /**
   * A float parser that is 30% faster than Float.parseFloat(x) and also accepts
   * x.yD+-n
   * 
   * @param str
   * @param ichMax
   * @param next
   *        pointer; incremented
   * @param isStrict
   * @return value or Float.NaN
   */
  public static float parseFloatChecked(String str, int ichMax, int[] next,
                                         boolean isStrict) {
    boolean digitSeen = false;
    int ich = next[0];
    if (isStrict && str.indexOf('\n') != str.lastIndexOf('\n'))
      return Float.NaN;
    while (ich < ichMax && isWhiteSpace(str, ich))
      ++ich;
    boolean negative = false;
    if (ich < ichMax && str.charAt(ich) == '-') {
      ++ich;
      negative = true;
    }
    // looks crazy, but if we don't do this, Google Closure Compiler will 
    // write code that Safari will misinterpret in a VERY nasty way -- 
    // getting totally confused as to long integers and double values
    
    // This is Safari figuring out the values of the numbers on the line (x, y, then z):
  
    //  ATOM 1241 CD1 LEU A 64 -2.206 36.532 31.576 1.00 60.60 C
    //  e=1408749273
    //  -e =-1408749273
    //  ATOM 1241 CD1 LEU A 64 -2.206 36.532 31.576 1.00 60.60 C
    //  e=-1821066134
    //  e=36.532
    //  ATOM 1241 CD1 LEU A 64 -2.206 36.532 31.576 1.00 60.60 C
    //  e=-1133871366
    //  e=31.576
    //
    //  "e" values are just before and after the "value = -value" statement.
    
    int ch = 0;
    float ival = 0f;
    float ival2 = 0f;
    while (ich < ichMax && (ch = str.charAt(ich)) >= 48 && ch <= 57) {
      ival = (ival * 10f) + (ch - 48)*1f;
      ++ich;
      digitSeen = true;
    }
    boolean isDecimal = false;
    int iscale = 0;
    int nzero = (ival == 0 ? -1 : 0);
    if (ch == '.') {
      isDecimal = true;
      while (++ich < ichMax && (ch = str.charAt(ich)) >= 48 && ch <= 57) {
        digitSeen = true;
        if (nzero < 0) {
          if (ch == 48) { 
            nzero--;
            continue;
          }
          nzero = -nzero;
        } 
        if (iscale  < decimalScale.length) {
          ival2 = (ival2 * 10f) + (ch - 48)*1f;
          iscale++;
        }
      }
    }
    float value;
    
    // Safari breaks here intermittently converting integers to floats 
    
    if (!digitSeen) {
      value = Float.NaN;
    } else if (ival2 > 0) {
      value = ival2 * decimalScale[iscale - 1];
      if (nzero > 1) {
        if (nzero - 2 < decimalScale.length) {
          value *= decimalScale[nzero - 2];
        } else {
          value *= Math.pow(10, 1 - nzero);
        }
      } else {
        value += ival;
      }
    } else {
      value = ival;
    }
    boolean isExponent = false;
    if (ich < ichMax && (ch == 69 || ch == 101 || ch == 68)) { // E e D
      isExponent = true;
      if (++ich >= ichMax)
        return Float.NaN;
      ch = str.charAt(ich);
      if ((ch == '+') && (++ich >= ichMax))
        return Float.NaN;
      next[0] = ich;
      int exponent = parseIntChecked(str, ichMax, next);
      if (exponent == Integer.MIN_VALUE)
        return Float.NaN;
      if (exponent > 0 && exponent <= tensScale.length)
        value *= tensScale[exponent - 1];
      else if (exponent < 0 && -exponent <= decimalScale.length)
        value *= decimalScale[-exponent - 1];
      else if (exponent != 0)
        value *= Math.pow(10, exponent);
    } else {
      next[0] = ich; // the exponent code finds its own ichNextParse
    }
    // believe it or not, Safari reports the long-equivalent of the 
    // float value here, then later the float value, after no operation!
    if (negative)
      value = -value;
    if (value == Float.POSITIVE_INFINITY)
      value = Float.MAX_VALUE;
    return (!isStrict || (!isExponent || isDecimal)
        && checkTrailingText(str, next[0], ichMax) ? value : Float.NaN);
  }

  public final static float[] tensScale = { 10f, 100f, 1000f, 10000f, 100000f, 1000000f };
  public final static float[] decimalScale = { 
  0.1f, 
  0.01f, 
  0.001f, 
  0.0001f, 
  0.00001f,
  0.000001f, 
  0.0000001f, 
  0.00000001f, 
  0.000000001f, 
  // added for JavaScript to have full double precision if specified
  0.0000000001f,
  0.00000000001f,
  0.000000000001f,
  0.0000000000001f,
  0.00000000000001f,
  0.000000000000001f,
  };
  public static boolean checkTrailingText(String str, int ich, int ichMax) {
    //number must be pure -- no additional characters other than white space or ;
    char ch;
    while (ich < ichMax && (isWhitespace(ch = str.charAt(ich)) || ch == ';'))
      ++ich;
    return (ich == ichMax);
  }

  public static float[] parseFloatArray(String str) {
    return parseFloatArrayNext(str, new int[1], null, null, null);
  }

  public static int parseFloatArrayInfested(String[] tokens, float[] data) {
    int len = data.length;
    int nTokens = tokens.length;
    int n = 0;
    int max = 0;
    for (int i = 0; i >= 0 && i < len && n < nTokens; i++) {
      float f;
      while (Float.isNaN(f = parseFloat(tokens[n++])) 
          && n < nTokens) {
      }
      if (!Float.isNaN(f))
        data[(max = i)] = f;
      if (n == nTokens)
        break;
    }
    return max + 1;
  }

  /**
   * @param str
   * @param next
   * @param f
   * @param strStart or null
   * @param strEnd   or null
   * @return array of float values
   * 
   */
  public static float[] parseFloatArrayNext(String str, int[] next, float[] f,
                                            String strStart, String strEnd) {
    int n = 0;
    int pt = next[0];
    if (pt >= 0) {
      if (strStart != null) {
        int p = str.indexOf(strStart, pt);
        if (p >= 0)
          next[0] = p + strStart.length();
      }
      str = str.substring(next[0]);
      pt = (strEnd == null ? -1 : str.indexOf(strEnd));
      if (pt < 0)
        pt = str.length();
      else
        str = str.substring(0, pt);
      next[0] += pt + 1;
      String[] tokens = getTokens(str);
      if (f == null)
        f = new float[tokens.length];
      n = parseFloatArrayInfested(tokens, f);
    }
    if (f == null)
      return new float[0];
    for (int i = n; i < f.length; i++)
      f[i] = Float.NaN;
    return f;
  }

  public static float parseFloatRange(String str, int ichMax, int[] next) {
    int cch = str.length();
    if (ichMax > cch)
      ichMax = cch;
    if (next[0] < 0 || next[0] >= ichMax)
      return Float.NaN;
    return parseFloatChecked(str, ichMax, next, false);
  }

  public static float parseFloatNext(String str, int[] next) {
    int cch = (str == null ? -1 : str.length());
    return (next[0] < 0 || next[0] >= cch ? Float.NaN : parseFloatChecked(str, cch, next, false));
  }

  public static float parseFloatStrict(String str) {
    // checks trailing characters and does not allow "1E35" to be float
    int cch = str.length();
    if (cch == 0)
      return Float.NaN;
    return parseFloatChecked(str, cch, new int[] {0}, true);
  }

  public static float parseFloat(String str) {
    return parseFloatNext(str, new int[] {0});
  }

  public static int parseIntRadix(String s, int i) throws NumberFormatException {
    /**
     * 
     * JavaScript uses parseIntRadix
     * 
     * @j2sNative
     * 
     *    return Integer.parseIntRadix(s, i);
     *    
     */
    {
      return Integer.parseInt(s, i);
    }
  }

  public static String[] getTokens(String line) {
    return getTokensAt(line, 0);
  }

  public static String parseToken(String str) {
    return parseTokenNext(str, new int[] {0});
  }

  public static String parseTrimmed(String str) {
    return parseTrimmedRange(str, 0, str.length());
  }

  public static String parseTrimmedAt(String str, int ichStart) {
    return parseTrimmedRange(str, ichStart, str.length());
  }

  public static String parseTrimmedRange(String str, int ichStart, int ichMax) {
    int cch = str.length();
    if (ichMax < cch)
      cch = ichMax;
    if (cch < ichStart)
      return "";
    return parseTrimmedChecked(str, ichStart, cch);
  }

  public static String[] getTokensAt(String line, int ich) {
    if (line == null)
      return null;
    int cchLine = line.length();
    if (ich < 0 || ich > cchLine)
      return null;
    int tokenCount = countTokens(line, ich);
    String[] tokens = new String[tokenCount];
    int[] next = new int[1];
    next[0] = ich;
    for (int i = 0; i < tokenCount; ++i)
      tokens[i] = parseTokenChecked(line, cchLine, next);
    return tokens;
  }

  public static int countChar(String line, char c) {
    int n = 0;
    for (int i = line.lastIndexOf(c) + 1; --i >= 0;)
      if (line.charAt(i) == c)
        n++;
    return n;
  }
  
  public static int countTokens(String line, int ich) {
    int tokenCount = 0;
    if (line != null) {
      int ichMax = line.length();
      while (true) {
        while (ich < ichMax && isWhiteSpace(line, ich))
          ++ich;
        if (ich == ichMax)
          break;
        ++tokenCount;
        do {
          ++ich;
        } while (ich < ichMax && !isWhiteSpace(line, ich));
      }
    }
    return tokenCount;
  }

  public static String parseTokenNext(String str, int[] next) {
    int cch = str.length();
    return (next[0] < 0 || next[0] >= cch ? null : parseTokenChecked(str, cch, next));
  }

  public static String parseTokenRange(String str, int ichMax, int[] next) {
    int cch = str.length();
    if (ichMax > cch)
      ichMax = cch;
    return (next[0] < 0 || next[0] >= ichMax ? null : parseTokenChecked(str, ichMax, next));
  }

  public static String parseTokenChecked(String str, int ichMax, int[] next) {
    int ich = next[0];
    while (ich < ichMax && isWhiteSpace(str, ich))
      ++ich;
    int ichNonWhite = ich;
    while (ich < ichMax && !isWhiteSpace(str, ich))
      ++ich;
    next[0] = ich;
    return (ichNonWhite == ich ? null : str.substring(ichNonWhite, ich));
  }

  public static String parseTrimmedChecked(String str, int ich, int ichMax) {
    while (ich < ichMax && isWhiteSpace(str, ich))
      ++ich;
    int ichLast = ichMax - 1;
    while (ichLast >= ich && isWhiteSpace(str, ichLast))
      --ichLast;
    return (ichLast < ich ? "" : str.substring(ich, ichLast + 1));
  }

  public static double dVal(String s) throws NumberFormatException {
    /**
     * @j2sNative
     * 
     * if(s==null)
     *   throw new NumberFormatException("null");
     * var d=parseFloat(s);
     * if(isNaN(d))
     *  throw new NumberFormatException("Not a Number : "+s);
     * return d 
     * 
     */
    {
      return Double.valueOf(s).doubleValue();
    }
  }

  public static float fVal(String s) throws NumberFormatException {
    /**
     * @j2sNative
     * 
     * return this.dVal(s);
     */
    {
      return Float.parseFloat(s);
    }
  }

  public static int parseIntRange(String str, int ichMax, int[] next) {
    int cch = str.length();
    if (ichMax > cch)
      ichMax = cch;
    return (next[0] < 0 || next[0] >= ichMax ? Integer.MIN_VALUE : parseIntChecked(str, ichMax, next));
  }

  /**
   * parses a string array for floats. Returns NaN for nonfloats.
   * 
   *  @param tokens  the strings to parse
   *  @param data    the array to fill
   */
  public static void parseFloatArrayData(String[] tokens, float[] data) {
    parseFloatArrayDataN(tokens, data, data.length);
  }

  /**
   * parses a string array for floats. Returns NaN for nonfloats or missing data.
   * 
   *  @param tokens  the strings to parse
   *  @param data    the array to fill
   *  @param nData   the number of elements
   */
  public static void parseFloatArrayDataN(String[] tokens, float[] data, int nData) {
    for (int i = nData; --i >= 0;)
      data[i] = (i >= tokens.length ? Float.NaN : parseFloat(tokens[i]));
  }

  /**
   * 
   *  proper splitting, even for Java 1.3 -- if the text ends in the run,
   *  no new line is appended.
   * 
   * @param text
   * @param run
   * @return  String array
   */
  public static String[] split(String text, String run) {
    if (text.length() == 0)
      return new String[0];
    int n = 1;
    int i = text.indexOf(run);
    String[] lines;
    int runLen = run.length();
    if (i < 0 || runLen == 0) {
      lines = new String[1];
      lines[0] = text;
      return lines;
    }
    int len = text.length() - runLen;
    for (; i >= 0 && i < len; n++)
      i = text.indexOf(run, i + runLen);
    lines = new String[n];
    i = 0;
    int ipt = 0;
    int pt = 0;
    for (; (ipt = text.indexOf(run, i)) >= 0 && pt + 1 < n;) {
      lines[pt++] = text.substring(i, ipt);
      i = ipt + runLen;
    }
    if (text.indexOf(run, len) != len)
      len += runLen;
    lines[pt] = text.substring(i, len);
    return lines;
  }

  public final static float FLOAT_MIN_SAFE = 2E-45f; 
  // Float.MIN_VALUE (1.45E-45) is not reliable with JavaScript because of the float/double difference there
  
  /// general static string-parsing class ///

  // next[0] tracks the pointer within the string so these can all be static.
  // but the methods parseFloat, parseInt, parseToken, parseTrimmed, and getTokens do not require this.

//  public static String concatTokens(String[] tokens, int iFirst, int iEnd) {
//    String str = "";
//    String sep = "";
//    for (int i = iFirst; i < iEnd; i++) {
//      if (i < tokens.length) {
//        str += sep + tokens[i];
//        sep = " ";
//      }
//    }
//    return str;
//  }
  
  public static String getQuotedStringAt(String line, int ipt0) {
    int[] next = new int[] { ipt0 };
    return getQuotedStringNext(line, next);
  }
  
  /**
   * 
   * @param line
   * @param next passes [current pointer]
   * @return quoted string -- does NOT unescape characters
   */
  public static String getQuotedStringNext(String line, int[] next) {
    int i = next[0];
    if (i < 0 || (i = line.indexOf("\"", i)) < 0)
      return "";
    int pt = i + 1;
    int len = line.length();
    while (++i < len && line.charAt(i) != '"')
      if (line.charAt(i) == '\\')
        i++;
    next[0] = i + 1;
    return line.substring(pt, i);
  }
  
  /**
   * single- or double-quoted string or up to the first space -- like HTML5
   * not case-sensitive
   * 
   * @param line
   * @param key
   * @return attribute
   */
  public static String getQuotedOrUnquotedAttribute(String line, String key) {
    if (line == null || key == null)
      return null;
    int pt = line.toLowerCase().indexOf(key.toLowerCase() + "=");
    if (pt < 0 || (pt = pt + key.length() + 1) >= line.length())
      return "";
    char c = line.charAt(pt);
    switch (c) {
    case '\'':
    case '"':
      pt++;
      break;
    default:
      c = ' ';
      line += " ";
    }
    int pt1 = line.indexOf(c, pt);
    return (pt1 < 0 ? null : line.substring(pt, pt1));
  }
  
  /**
   * CSV format -- escaped quote is "" WITHIN "..."
   *
   * 
   * @param line
   * @param next int[2] filled with [ptrQuote1, ptrAfterQuote2]
   *            next[1] will be -1 if unmatched quotes are found (continuation on next line)
   * @return unescaped string or null
   */
  public static String getCSVString(String line, int[] next) {
    int i = next[1];
    if (i < 0 || (i = line.indexOf("\"", i)) < 0)
      return null;
    int pt = next[0] = i;
    int len = line.length();
    boolean escaped = false;
    boolean haveEscape = false;
    while (++i < len 
        && (line.charAt(i) != '"' || (escaped = (i + 1 < len && line.charAt(i + 1) == '"'))))
      if (escaped) {
        escaped = false;
        haveEscape = true;
        i++;
      }
    if (i >= len) {
      next[1] = -1;
      return null; // unmatched
    }
    next[1] = i + 1;
    String s = line.substring(pt + 1, i);
    return (haveEscape ? rep(rep(s, "\"\"", "\0"), "\0","\"") : s);
  }
  
  public static boolean isOneOf(String key, String semiList) {
    if (semiList.length() == 0)
      return false;
    if (semiList.charAt(0) != ';')
      semiList = ";" + semiList + ";";
    return key.indexOf(";") < 0  && semiList.indexOf(';' + key + ';') >= 0;
  }

  public static String getQuotedAttribute(String info, String name) {
    int i = info.indexOf(name + "=");
    return (i < 0 ? null : getQuotedStringAt(info, i));
  }

  public static float approx(float f, float n) {
    return Math.round (f * n) / n;
  }

  /**
   * Does a clean ITERATIVE replace of strFrom in str with strTo. 
   * Thus, rep("Testttt", "tt","t") becomes "Test".
   * 
   * @param str
   * @param strFrom
   * @param strTo
   * @return replaced string
   */
  public static String rep(String str, String strFrom, String strTo) {
    if (str == null || strFrom.length() == 0 || str.indexOf(strFrom) < 0)
      return str;
    boolean isOnce = (strTo.indexOf(strFrom) >= 0);
    do {
      str = str.replace(strFrom, strTo);
    } while (!isOnce && str.indexOf(strFrom) >= 0);
    return str;
  }

  public static String formatF(float value, int width, int precision,
                              boolean alignLeft, boolean zeroPad) {
    return formatS(DF.formatDecimal(value, precision), width, 0, alignLeft, zeroPad);
  }

  /**
   * 
   * @param value
   * @param width
   * @param precision
   * @param alignLeft
   * @param zeroPad
   * @param allowOverflow IGNORED
   * @return formatted string
   */
  public static String formatD(double value, int width, int precision,
                              boolean alignLeft, boolean zeroPad, boolean allowOverflow) {
    return formatS(DF.formatDecimal((float)value, -1 - precision), width, 0, alignLeft, zeroPad);
  }

  /**
   * 
   * @param value       
   * @param width       number of columns
   * @param precision   precision > 0 ==> precision = number of characters max from left
   *                    precision < 0 ==> -1 - precision = number of char. max from right
   * @param alignLeft
   * @param zeroPad     generally for numbers turned strings
   * @return            formatted string
   */
  public static String formatS(String value, int width, int precision,
                              boolean alignLeft, boolean zeroPad) {
    if (value == null)
      return "";
    int len = value.length();
    if (precision != Integer.MAX_VALUE && precision > 0
        && precision < len)
      value = value.substring(0, precision);
    else if (precision < 0 && len + precision >= 0)
      value = value.substring(len + precision + 1);
  
    int padLength = width - value.length();
    if (padLength <= 0)
      return value;
    boolean isNeg = (zeroPad && !alignLeft && value.charAt(0) == '-');
    char padChar = (zeroPad ? '0' : ' ');
    char padChar0 = (isNeg ? '-' : padChar);
  
    SB sb = new SB();
    if (alignLeft)
      sb.append(value);
    sb.appendC(padChar0);
    for (int i = padLength; --i > 0;)
      // this is correct, not >= 0
      sb.appendC(padChar);
    if (!alignLeft)
      sb.append(isNeg ? padChar + value.substring(1) : value);
    return sb.toString();
  }

  /**
   * Does a clean replace of any of the characters in str with chrTo
   * If strTo contains strFrom, then only a single pass is done.
   * Otherwise, multiple passes are made until no more replacements can be made.
   * 
   * @param str
   * @param strFrom
   * @param chTo
   * @return  replaced string
   */
  public static String replaceWithCharacter(String str, String strFrom,
                                            char chTo) {
    if (str == null)
      return null;
    for (int i = strFrom.length(); --i >= 0;)
      str = str.replace(strFrom.charAt(i), chTo);
    return str;
  }

  /**
   * Does a clean replace of any of the characters in str with strTo
   * If strTo contains strFrom, then only a single pass is done.
   * Otherwise, multiple passes are made until no more replacements can be made.
   * 
   * @param str
   * @param strFrom
   * @param strTo
   * @return  replaced string
   */
  public static String replaceAllCharacters(String str, String strFrom,
                                            String strTo) {
    for (int i = strFrom.length(); --i >= 0;) {
      String chFrom = strFrom.substring(i, i + 1);
      str = rep(str, chFrom, strTo);
    }
    return str;
  }

  public static String trim(String str, String chars) {
    if (str == null || str.length() == 0)
      return str;
    if (chars.length() == 0)
      return str.trim();
    int len = str.length();
    int k = 0;
    while (k < len && chars.indexOf(str.charAt(k)) >= 0)
      k++;
    int m = str.length() - 1;
    while (m > k && chars.indexOf(str.charAt(m)) >= 0)
      m--;
    return str.substring(k, m + 1);
  }

  public static String trimQuotes(String value) {
    return (value != null && value.length() > 1 && value.startsWith("\"")
        && value.endsWith("\"") ? value.substring(1, value.length() - 1)
        : value);
  }

  public static boolean isNonStringPrimitive(Object info) {
    // note that we don't use Double, Float, or Integer here
    // because in JavaScript those would be false for unwrapped primitives
    // coming from equivalent of Array.get()
    // Strings will need their own escaped processing
    /**
     * @j2sNative
     * 
     * if(typeof info == "number" || typeof info == "boolean") {
     * return true;
     * }
     * 
     * 
     */
    {}
     return info instanceof Number || info instanceof Boolean;
  }
  
  @SuppressWarnings({ "null", "unused", "unchecked" })
  public static String toJSON(String infoType, Object info) {
    if (info == null)
      return packageJSON(infoType, null);
    if (isNonStringPrimitive(info))
      return packageJSON(infoType, info.toString());
    String s = null;
    SB sb = null;
    while (true) {
      if (info instanceof String) {
        s = (String) info;
        /**
         * @j2sNative
         * 
         *            if (typeof s == "undefined") s = "null"
         * 
         */
        {
        }

        if (s.indexOf("{\"") != 0) {
          //don't doubly fix JSON strings when retrieving status
          // what about  \1 \2 \3 etc.?
          s = esc(s);
        }
        break;
      }
      if (info instanceof JSONEncodable) {
        // includes javajs.util.BS, org.jmol.script.SV
        if ((s = ((JSONEncodable) info).toJSON()) == null)
          s = "null"; // perhaps a list has a null value (group3List, for example)
        break;
      }
      sb = new SB();
      if (info instanceof Map) {
        sb.append("{ ");
        String sep = "";
        for (String key : ((Map<String, ?>) info).keySet()) {
          if (key == null)
            key = "null";
          sb.append(sep).append(
              packageJSON(key, toJSON(null, ((Map<?, ?>) info).get(key))));
          sep = ",";
        }
        sb.append(" }");
        break;
      }
      if (info instanceof Lst) {
        sb.append("[ ");
        int n = ((Lst<?>) info).size();
        for (int i = 0; i < n; i++) {
          if (i > 0)
            sb.appendC(',');
          sb.append(toJSON(null, ((Lst<?>) info).get(i)));
        }
        sb.append(" ]");
        break;
      }
      if (info instanceof M34) {
        // M4 extends M3
        int len = (info instanceof M4 ? 4 : 3);
        float[] x = new float[len];
        M34 m = (M34) info;
        sb.appendC('[');
        for (int i = 0; i < len; i++) {
          if (i > 0)
            sb.appendC(',');
          m.getRow(i, x);
          sb.append(toJSON(null, x));
        }
        sb.appendC(']');
        break;
      }
      s = nonArrayString(info);
      if (s == null) {
        sb.append("[");
        int n = AU.getLength(info);
        Object o = null;
        /** @j2sNative 
         *  o = info[0];
         *  typeof o != "number" && typeof 0 != "boolean" && (o = null);
         */
        {}
        if (o != null) {
          sb.appendO(info);
        } else {
          for (int i = 0; i < n; i++) {
            if (i > 0)
              sb.appendC(',');
            sb.append(toJSON(null, arrayGet(info, i)));
          }
        }
        sb.append("]");
        break;
      }
      info = info.toString();
    }
    return packageJSON(infoType, (s == null ? sb.toString() : s));
  }

  private static Object arrayGet(Object info, int i) {
    /**
     * 
     * Note that info will be a primitive in JavaScript
     * but a wrapped primitive in Java.
     * 
     * @j2sNative
     * 
     *            return info[i];
     */
    {
      return Array.get(info, i);
    }
  }
  

  /**
   * Checks to see if an object is an array (including typed arrays), and if it is, returns null;
   * otherwise it returns the string equivalent of that object.
   * 
   * @param x
   * @return String or null
   */
  public static String nonArrayString(Object x) {
    /**
     * @j2sNative
     * 
     * return (x.constructor == Array || x.BYTES_PER_ELEMENT ? null : x.toString());
     * 
     */
    {
      try {
        Array.getLength(x);
        return null;
      } catch (Exception e) {
        return x.toString();
      }
    }
  }

  public static String byteArrayToJSON(byte[] data) {
    SB sb = new SB();
    sb.append("[");
    int n = data.length;
    for (int i = 0; i < n; i++) {
      if (i > 0)
        sb.appendC(',');
      sb.appendI(data[i] & 0xFF);
    }
    sb.append("]");
    return sb.toString();
  }
  
  public static String packageJSON(String infoType, String info) {
    return (infoType == null ? info : "\"" + infoType + "\": " + info);
  }

  public static String escapeUrl(String url) {
    url = rep(url, "\n", "");
    url = rep(url, "%", "%25");
    url = rep(url, "#", "%23");
    url = rep(url, "[", "%5B");
    url = rep(url, "\\", "%5C");
    url = rep(url, "]", "%5D");
    url = rep(url, " ", "%20");
    return url;
  }

  private final static String escapable = "\\\\\tt\rr\nn\"\""; 

  public static String esc(String str) {
    if (str == null || str.length() == 0)
      return "\"\"";
    boolean haveEscape = false;
    int i = 0;
    for (; i < escapable.length(); i += 2)
      if (str.indexOf(escapable.charAt(i)) >= 0) {
        haveEscape = true;
        break;
      }
    if (haveEscape)
      while (i < escapable.length()) {
        int pt = -1;
        char ch = escapable.charAt(i++);
        char ch2 = escapable.charAt(i++);
        SB sb = new SB();
        int pt0 = 0;
        while ((pt = str.indexOf(ch, pt + 1)) >= 0) {
          sb.append(str.substring(pt0, pt)).appendC('\\').appendC(ch2);
          pt0 = pt + 1;
        }
        sb.append(str.substring(pt0, str.length()));
        str = sb.toString();
      }    
    return "\"" + escUnicode(str) + "\"";
  }

  public static String escUnicode(String str) {
    for (int i = str.length(); --i >= 0;)
      if (str.charAt(i) > 0x7F) {
        String s = "0000" + Integer.toHexString(str.charAt(i));
        str = str.substring(0, i) + "\\u" + s.substring(s.length() - 4)
            + str.substring(i + 1);
      }
    return str;
  }

  /**
   * ensures that a float turned to string has a decimal point
   * 
   * @param f
   * @return string version of float
   */
  public static String escF(float f) {
    String sf = "" + f;
    // NaN, Infinity
    /**
     * @j2sNative
     * 
     * if (sf.indexOf(".") < 0 && sf.indexOf("e") < 0 && sf.indexOf("N") < 0 && sf.indexOf("n") < 0)
     *   sf += ".0";
     */
    {
    }
    return sf;
  }
  public static String join(String[] s, char c, int i0) {
    if (s.length < i0)
      return null;
    SB sb = new SB();
    sb.append(s[i0++]);
    for (int i = i0; i < s.length; i++)
      sb.appendC(c).append(s[i]);
    return sb.toString();
  }

  /**
   * a LIKE "x"    a is a string and equals x
   * 
   * a LIKE "*x"   a is a string and ends with x
   * 
   * a LIKE "x*"   a is a string and starts with x
   * 
   * a LIKE "*x*"  a is a string and contains x
   *  
   * @param a
   * @param b
   * @return  a LIKE b
   */
  public static boolean isLike(String a, String b) {
    boolean areEqual = a.equals(b);
    if (areEqual)
      return true;
    boolean isStart = b.startsWith("*");
    boolean isEnd = b.endsWith("*");
    return (!isStart && !isEnd) ? areEqual
        : isStart && isEnd ? b.length() == 1 || a.contains(b.substring(1, b.length() - 1))
        : isStart ? a.endsWith(b.substring(1))
        : a.startsWith(b.substring(0, b.length() - 1));
  }

  public static Object getMapValueNoCase(Map<String, ?> h, String key) {
    if ("this".equals(key))
      return h;
    Object val = h.get(key);
    if (val == null)
      for (Entry<String, ?> e : h.entrySet())
        if (e.getKey().equalsIgnoreCase(key))
          return e.getValue();
    return val;
  }

  public static String clean(String s) {
    return rep(replaceAllCharacters(s, " \t\n\r", " "), "  ", " ").trim();
  }

  /**
   * 
   * fdup      duplicates p or q formats for formatCheck
   *           and the format() function.
   * 
   * @param f
   * @param pt
   * @param n
   * @return     %3.5q%3.5q%3.5q%3.5q or %3.5p%3.5p%3.5p
   */
  public static String fdup(String f, int pt, int n) {
    char ch;
    int count = 0;
    for (int i = pt; --i >= 1; ) {
      if (isDigit(ch = f.charAt(i)))
        continue;
      switch (ch) {
      case '.':
        if (count++ != 0)
          return f;
        continue;
      case '-':
        if (i != 1 && f.charAt(i - 1) != '.')
          return f;
        continue;
      default:
        return f;
      }
    }
    String s = f.substring(0, pt + 1);
    SB sb = new SB();
    for (int i = 0; i < n; i++)
      sb.append(s);
    sb.append(f.substring(pt + 1));
    return sb.toString();
  }

  /**
   * generic string formatter  based on formatLabel in Atom
   * 
   * 
   * @param strFormat   .... %width.precisionKEY....
   * @param key      any string to match
   * @param strT     replacement string or null
   * @param floatT   replacement float or Float.NaN
   * @param doubleT  replacement double or Double.NaN -- for exponential
   * @param doOne    mimic sprintf    
   * @return         formatted string
   */
  
  private static String formatString(String strFormat, String key, String strT,
                                    float floatT, double doubleT, boolean doOne) {
    if (strFormat == null)
      return null;
    if ("".equals(strFormat))
      return "";
    int len = key.length();
    if (strFormat.indexOf("%") < 0 || len == 0 || strFormat.indexOf(key) < 0)
      return strFormat;
  
    String strLabel = "";
    int ich, ichPercent, ichKey;
    for (ich = 0; (ichPercent = strFormat.indexOf('%', ich)) >= 0
        && (ichKey = strFormat.indexOf(key, ichPercent + 1)) >= 0;) {
      if (ich != ichPercent)
        strLabel += strFormat.substring(ich, ichPercent);
      ich = ichPercent + 1;
      if (ichKey > ichPercent + 6) {
        strLabel += '%';
        continue;//%12.10x
      }
      try {
        boolean alignLeft = false;
        if (strFormat.charAt(ich) == '-') {
          alignLeft = true;
          ++ich;
        }
        boolean zeroPad = false;
        if (strFormat.charAt(ich) == '0') {
          zeroPad = true;
          ++ich;
        }
        char ch;
        int width = 0;
        while ((ch = strFormat.charAt(ich)) >= '0' && (ch <= '9')) {
          width = (10 * width) + (ch - '0');
          ++ich;
        }
        int precision = Integer.MAX_VALUE;
        boolean isExponential = false;
        if (strFormat.charAt(ich) == '.') {
          ++ich;
          if ((ch = strFormat.charAt(ich)) == '-') {
            isExponential = true;//(strT == null);
            ++ich;
          } 
          if ((ch = strFormat.charAt(ich)) >= '0' && ch <= '9') {
            precision = ch - '0';
            ++ich;
            if ((ch = strFormat.charAt(ich)) >= '0' && ch <= '9') {
              precision = 10*precision + (ch - '0');
              ++ich;
            }
          }
          if (isExponential)
            precision = -precision;
        }
        String st = strFormat.substring(ich, ich + len);
        if (!st.equals(key)) {
          ich = ichPercent + 1;
          strLabel += '%';
          continue;
        }
        ich += len;
        if (!Float.isNaN(floatT)) // 'f'
          strLabel += formatF(floatT, width, precision, alignLeft,
              zeroPad);
        else if (strT != null)  // 'd' 'i' or 's'
          strLabel += formatS(strT, width, precision < 0 ? precision - 1 : precision, alignLeft,
              zeroPad);
        else if (!Double.isNaN(doubleT)) // 'e'
          strLabel += formatD(doubleT, width, precision - 1, alignLeft,
              zeroPad, true);
        if (doOne)
          break;
      } catch (IndexOutOfBoundsException ioobe) {
        ich = ichPercent;
        break;
      }
    }
    strLabel += strFormat.substring(ich);
    //if (strLabel.length() == 0)
      //return null;
    return strLabel;
  }

  public static String formatStringS(String strFormat, String key, String strT) {
    return formatString(strFormat, key, strT, Float.NaN, Double.NaN, false);
  }

  public static String formatStringF(String strFormat, String key, float floatT) {
    return formatString(strFormat, key, null, floatT, Double.NaN, false);
  }

  public static String formatStringI(String strFormat, String key, int intT) {
    return formatString(strFormat, key, "" + intT, Float.NaN, Double.NaN, false);
  }

  /**
   * sprintf emulation uses (almost) c++ standard string formats
   * 
   * 's' string 'i' or 'd' integer, 'e' double, 'f' float, 'p' point3f 'q'
   * quaternion/plane/axisangle with added "i" (equal to the insipid "d" --
   * digits?)
   * 
   * @param strFormat
   * @param list
   *        a listing of what sort of data will be found in Object[] values, in
   *        order: s string, f float, i integer, d double, p point3f, q
   *        quaternion/point4f, S String[], F float[], I int[], and D double[]
   * @param values
   *        Object[] containing above types
   * @return formatted string
   */
  public static String sprintf(String strFormat, String list, Object[] values) {
    if (values == null)
      return strFormat;
    int n = list.length();
    if (n == values.length)
      try {
        for (int o = 0; o < n; o++) {
          if (values[o] == null)
            continue;
          switch (list.charAt(o)) {
          case 's':
            strFormat = formatString(strFormat, "s", (String) values[o],
                Float.NaN, Double.NaN, true);
            break;
          case 'f':
            strFormat = formatString(strFormat, "f", null, ((Float) values[o])
                .floatValue(), Double.NaN, true);
            break;
          case 'i':
            strFormat = formatString(strFormat, "d", "" + values[o], Float.NaN,
                Double.NaN, true);
            strFormat = formatString(strFormat, "i", "" + values[o], Float.NaN,
                Double.NaN, true);
            break;
          case 'd':
            strFormat = formatString(strFormat, "e", null, Float.NaN,
                ((Double) values[o]).doubleValue(), true);
            break;
          case 'p':
            T3 pVal = (T3) values[o];
            strFormat = formatString(strFormat, "p", null, pVal.x, Double.NaN,
                true);
            strFormat = formatString(strFormat, "p", null, pVal.y, Double.NaN,
                true);
            strFormat = formatString(strFormat, "p", null, pVal.z, Double.NaN,
                true);
            break;
          case 'q':
            T4 qVal = (T4) values[o];
            strFormat = formatString(strFormat, "q", null, qVal.x, Double.NaN,
                true);
            strFormat = formatString(strFormat, "q", null, qVal.y, Double.NaN,
                true);
            strFormat = formatString(strFormat, "q", null, qVal.z, Double.NaN,
                true);
            strFormat = formatString(strFormat, "q", null, qVal.w, Double.NaN,
                true);
            break;
          case 'S':
            String[] sVal = (String[]) values[o];
            for (int i = 0; i < sVal.length; i++)
              strFormat = formatString(strFormat, "s", sVal[i], Float.NaN,
                  Double.NaN, true);
            break;
          case 'F':
            float[] fVal = (float[]) values[o];
            for (int i = 0; i < fVal.length; i++)
              strFormat = formatString(strFormat, "f", null, fVal[i],
                  Double.NaN, true);
            break;
          case 'I':
            int[] iVal = (int[]) values[o];
            for (int i = 0; i < iVal.length; i++)
              strFormat = formatString(strFormat, "d", "" + iVal[i], Float.NaN,
                  Double.NaN, true);
            for (int i = 0; i < iVal.length; i++)
              strFormat = formatString(strFormat, "i", "" + iVal[i], Float.NaN,
                  Double.NaN, true);
            break;
          case 'D':
            double[] dVal = (double[]) values[o];
            for (int i = 0; i < dVal.length; i++)
              strFormat = formatString(strFormat, "e", null, Float.NaN,
                  dVal[i], true);
          }
  
        }
        return rep(strFormat, "%%", "%");
      } catch (Exception e) {
        //
      }
    System.out.println("TextFormat.sprintf error " + list + " " + strFormat);
    return rep(strFormat, "%", "?");
  }

  /**
   * 
   * formatCheck   checks p and q formats and duplicates if necessary
   *               "%10.5p xxxx" ==> "%10.5p%10.5p%10.5p xxxx" 
   * 
   * @param strFormat
   * @return    f or dupicated format
   */
  public static String formatCheck(String strFormat) {
    if (strFormat == null || strFormat.indexOf('p') < 0 && strFormat.indexOf('q') < 0)
      return strFormat;
    strFormat = rep(strFormat, "%%", "\1");
    strFormat = rep(strFormat, "%p", "%6.2p");
    strFormat = rep(strFormat, "%q", "%6.2q");
    String[] format = split(strFormat, "%");
    SB sb = new SB();
    sb.append(format[0]);
    for (int i = 1; i < format.length; i++) {
      String f = "%" + format[i];
      int pt;
      if (f.length() >= 3) {
        if ((pt = f.indexOf('p')) >= 0)
          f = fdup(f, pt, 3);
        if ((pt = f.indexOf('q')) >= 0)
          f = fdup(f, pt, 4);
      }
      sb.append(f);
    }
    return sb.toString().replace('\1', '%');
  }

  public static void leftJustify(SB s, String s1, String s2) {
    s.append(s2);
    int n = s1.length() - s2.length();
    if (n > 0)
      s.append(s1.substring(0, n));
  }

  public static void rightJustify(SB s, String s1, String s2) {
    int n = s1.length() - s2.length();
    if (n > 0)
      s.append(s1.substring(0, n));
    s.append(s2);
  }

  public static String safeTruncate(float f, int n) {
    if (f > -0.001 && f < 0.001)
      f = 0;
    return (f + "         ").substring(0,n);
  }

  public static boolean isWild(String s) {
    return s != null && (s.indexOf("*") >= 0 || s.indexOf("?") >= 0);
  }

  /**
   * A general non-regex (for performance) text matcher that utilizes ? and *.
   * 
   * ??? means "at most three" characters if at beginning or end; 
   *   "exactly three" otherwise
   * \1 in search is a stand-in for actual ?
   * 
   * @param search
   *        the string to search
   * @param match
   *        the match string
   * @param checkStar
   * @param allowInitialStar
   * @return true if found
   */
  public static boolean isMatch(String search, String match, boolean checkStar,
                                boolean allowInitialStar) {
    // search == match --> true
    if (search.equals(match))
      return true;
    int mLen = match.length();
    // match == ""  --> false
    if (mLen == 0)
      return false;
    boolean isStar0 = (checkStar && allowInitialStar ? match.charAt(0) == '*'
        : false);
    // match == "*" --> true
    if (mLen == 1 && isStar0)
      return true;
    boolean isStar1 = (checkStar && match.endsWith("*"));
    boolean haveQ = (match.indexOf('?') >= 0);
    // match == "**" --> true
    // match == "*xxx*" --> search contains "xxx"
    // match == "*xxx" --> search ends with "xxx"
    // match == "xxx*" --> search starts with "xxx"
    if (!haveQ) {
      if (isStar0)
        return (isStar1 ? (mLen < 3 || search.indexOf(match.substring(1,
            mLen - 1)) >= 0) : search.endsWith(match.substring(1)));
      else if (isStar1)
        return search.startsWith(match.substring(0, mLen - 1));
    }
    int sLen = search.length();
    // pad match with "?" -- same as *
    String qqqq = "????";
    int nq = 4;
    while (nq < sLen) {
      qqqq += qqqq;
      nq += 4;
    }
    if (checkStar) {
      if (isStar0) {
        match = qqqq + match.substring(1);
        mLen += nq - 1;
      }
      if (isStar1) {
        match = match.substring(0, mLen - 1) + qqqq;
        mLen += nq - 1;
      }
    }
    // length of match < length of search --> false 
    if (mLen < sLen)
      return false;
  
    // -- each ? matches ONE character if not at end
    // -- extra ? at end ignored
  
    // (allowInitialStar == true)
    // -- extra ? at beginning reduced to match length
  
    int ich = 0;
    while (mLen > sLen) {
      if (allowInitialStar && match.charAt(ich) == '?') {
        ++ich;
      } else if (match.charAt(ich + mLen - 1) != '?') {
        return false;
      }
      --mLen;
    }
  
    // both are effectively same length now.
    // \1 is stand-in for "?"
  
    for (int i = sLen; --i >= 0;) {
      char chm = match.charAt(ich + i);
      if (chm == '?')
        continue;
      char chs = search.charAt(i);
      if (chm != chs && (chm != '\1' || chs != '?'))
        return false;
    }
    return true;
  }

  public static String replaceQuotedStrings(String s, Lst<String> list,
                                            Lst<String> newList) {
    int n = list.size();
    for (int i = 0; i < n; i++) {
      String name = list.get(i);
      String newName = newList.get(i);
      if (!newName.equals(name))
        s = rep(s, "\"" + name + "\"", "\"" + newName
            + "\"");
    }
    return s;
  }

  public static String replaceStrings(String s, Lst<String> list,
                                      Lst<String> newList) {
    int n = list.size();
    for (int i = 0; i < n; i++) {
      String name = list.get(i);
      String newName = newList.get(i);
      if (!newName.equals(name))
        s = rep(s, name, newName);
    }
    return s;
  }

  public static boolean isDigit(char ch) {
    // just way simpler code than  Character.isDigit(ch);
    int c = ch;
    return (48 <= c && c <= 57);
  }

  public static boolean isUpperCase(char ch) {
    int c = ch;
    return (65 <= c && c <= 90);
  }

  public static boolean isLowerCase(char ch) {
    int c = ch;
    return (97 <= c && c <= 122);
  }

  public static boolean isLetter(char ch) {
    // just way simpler code than     Character.isLetter(ch);
    int c = ch;
    return (65 <= c && c <= 90 || 97 <= c && c <= 122);
  }

  public static boolean isLetterOrDigit(char ch) {
    // just way simpler code than     Character.isLetterOrDigit(ch);
    int c = ch;
    return (65 <= c && c <= 90 || 97 <= c && c <= 122 || 48 <= c && c <= 57);
  }

  public static boolean isWhitespace(char ch) {
    int c = ch;
    return (c >= 0x1c && c <= 0x20 || c >= 0x9 && c <= 0xd);
  }

  public static final float FRACTIONAL_PRECISION = 100000f;
  public static final float CARTESIAN_PRECISION =  10000f;

  public static void fixPtFloats(T3 pt, float f) {
    //this will equate float and double as long as -256 <= x <= 256
    pt.x = Math.round(pt.x * f) / f;
    pt.y = Math.round(pt.y * f) / f;
    pt.z = Math.round(pt.z * f) / f;
  }
  
  public static double fixDouble(double d, double f) {
    return Math.round(d * f) / f;
  }

  /**
   * parse a float or "float/float"
   * @param s
   * @return a/b
   */
  public static float parseFloatFraction(String s) {
      int pt = s.indexOf("/");
      return (pt < 0 ? parseFloat(s) : parseFloat(s.substring(0, pt))
          / parseFloat(s.substring(pt + 1)));
  }

//static {
//    
//  double d = 790.8999998888;
//  float x  = 790.8999998888f;
//  for (int i = 0; i < 50; i++) {
//  System.out.println(x + " " + d);
//  System.out.println(Math.round(x * 100000) / 100000f);
//  System.out.println(Math.round(d * 100000) / 100000.);
//  System.out.println(Math.round(x * 10000) / 10000f);
//  System.out.println(Math.round(d * 10000) / 10000.);
//  x+=1; 
//  d+=1;
//  }
//  System.out.println(100.123456789f);
//}

//  static {
//    long t;
//    char c = '0';
//    t = System.currentTimeMillis();
//    for (int i = 0; i < 10000000; i++) {
//      boolean b = PT.isUpperCase(c);
//    }
//    System.out.println(System.currentTimeMillis() - t);
//
//    t = System.currentTimeMillis();
//    for (int i = 0; i < 10000000; i++) {
//      boolean b = Character.isUpperCase(c);
//    }
//    System.out.println(System.currentTimeMillis() - t);
//    
//    t = System.currentTimeMillis();
//    for (int i = 0; i < 10000000; i++) {
//      boolean b = PT.isUpperCase(c);
//    }
//    System.out.println(System.currentTimeMillis() - t);
//
//    System.out.println("PT test");
//  }
}
