/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *   Free SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.es;

import java.util.Locale;

import com.caucho.util.CharBuffer;
import com.caucho.util.IntArray;

/**
 * JavaScript object
 */
class NativeString extends Native {
  static final int NEW = 1;
  static final int TO_STRING = NEW + 1;
  static final int FROM_CHAR_CODE = TO_STRING + 1;
  static final int VALUE_OF = FROM_CHAR_CODE + 1;
  static final int CHAR_AT = VALUE_OF + 1;
  static final int CHAR_CODE_AT = CHAR_AT + 1;
  static final int INDEX_OF = CHAR_CODE_AT + 1;
  static final int LAST_INDEX_OF = INDEX_OF + 1;
  static final int SPLIT = LAST_INDEX_OF + 1;
  static final int SUBSTRING = SPLIT + 1;
  static final int TO_UPPER_CASE = SUBSTRING + 1;
  static final int TO_LOWER_CASE = TO_UPPER_CASE + 1;

  // js1.2
  static final int CONCAT = TO_LOWER_CASE + 1;
  static final int MATCH = CONCAT + 1;
  static final int REPLACE = MATCH + 1;
  static final int SEARCH = REPLACE + 1;
  static final int SLICE = SEARCH + 1;
  static final int SUBSTR = SLICE + 1;

  // caucho
  static final int PRINTF = SUBSTR + 1;
  static final int CONTAINS = PRINTF + 1;
  static final int STARTS_WITH = CONTAINS + 1;
  static final int ENDS_WITH = STARTS_WITH + 1;
  static final int GET_BYTES = ENDS_WITH + 1;

  static final int REPLACE_WS = 0;
  static final int REPLACE_DIGIT = REPLACE_WS + 1;
  static final int REPLACE_ID = REPLACE_DIGIT + 1;

  private NativeString(String name, int n, int len)
  {
    super(name, len);

    this.n = n;
  }

  /**
   * Creates the native String object
   */
  static ESObject create(Global resin)
  {
    NativeString nativeString = new NativeString("String", NEW, 1);
    ESObject stringProto = new ESWrapper("String", resin.objProto,
                                         ESString.NULL);
    NativeWrapper string;
    string = new NativeWrapper(resin, nativeString,
                               stringProto, ESThunk.STRING_THUNK);
    resin.stringProto = stringProto;

    stringProto.put("length", ESNumber.create(0), 
                    DONT_ENUM|DONT_DELETE|READ_ONLY);

    put(stringProto, "valueOf", VALUE_OF, 0);
    put(stringProto, "toString", TO_STRING, 0);
    put(stringProto, "charAt", CHAR_AT, 1);
    put(stringProto, "charCodeAt", CHAR_CODE_AT, 1);
    put(stringProto, "indexOf", INDEX_OF, 2);
    put(stringProto, "lastIndexOf", LAST_INDEX_OF, 2);
    put(stringProto, "split", SPLIT, 1);
    put(stringProto, "substring", SUBSTRING, 2);
    put(stringProto, "toUpperCase", TO_UPPER_CASE, 0);
    put(stringProto, "toLowerCase", TO_LOWER_CASE, 0);

    put(string, "fromCharCode", FROM_CHAR_CODE, 0);

    // js1.2
    put(stringProto, "concat", CONCAT, 1);
    put(stringProto, "match", MATCH, 1);
    put(stringProto, "replace", REPLACE, 2);
    put(stringProto, "search", SEARCH, 1);
    put(stringProto, "slice", SLICE, 2);
    put(stringProto, "substr", SUBSTR, 2);

    // caucho extensions
    put(string, "printf", PRINTF, 1);
    put(stringProto, "contains", CONTAINS, 1);
    put(stringProto, "startsWith", STARTS_WITH, 1);
    put(stringProto, "endsWith", ENDS_WITH, 1);
    put(stringProto, "getBytes", GET_BYTES, 1);

    stringProto.setClean();
    string.setClean();

    return string;
  }
  
  private static void put(ESObject obj, String name, int n, int len)
  {
    ESId id = ESId.intern(name);

    obj.put(id, new NativeString(name, n, len), DONT_ENUM);
  }

  public ESBase call(Call eval, int length) throws Throwable
  {
    switch (n) {
    case NEW:
      if (length == 0)
        return ESString.create("");
      else
        return eval.getArg(0).toStr();

    case FROM_CHAR_CODE:
      return fromCharCode(eval, length);

    case VALUE_OF:
    case TO_STRING:
      try {
        return (ESBase) ((ESWrapper) eval.getArg(-1)).value;
      } catch (ClassCastException e) {
        if (eval.getArg(-1) instanceof ESString)
          return eval.getArg(-1);

        if (eval.getArg(-1) instanceof ESThunk)
          return (ESBase) ((ESWrapper) ((ESThunk) eval.getArg(-1)).getObject()).value;

        throw new ESException("toString expects string object");
      }

    case CHAR_AT:
      return charAt(eval, length);

    case CHAR_CODE_AT:
      return charCodeAt(eval, length);

    case INDEX_OF:
      return indexOf(eval, length);

    case LAST_INDEX_OF:
      return lastIndexOf(eval, length);

    case SPLIT:
      return split(eval, length);

    case SUBSTRING:
      return substring(eval, length);

    case TO_UPPER_CASE:
      return eval.getArg(-1).toStr().toUpperCase();

    case TO_LOWER_CASE:
      return eval.getArg(-1).toStr().toLowerCase();

    case CONCAT:
      return concat(eval, length);

    case MATCH:
      return match(eval, length);

    case REPLACE:
      return replace(eval, length);

    case SEARCH:
      return search(eval, length);

    case SLICE:
      return slice(eval, length);

    case SUBSTR:
      return substr(eval, length);

    case PRINTF:
      return printf(eval, length);

    case CONTAINS:
      if (length < 1)
        return ESBoolean.FALSE;
      else
        return eval.getArg(-1).toStr().contains(eval.getArg(0));

    case STARTS_WITH:
      if (length < 1)
        return ESBoolean.FALSE;
      else
        return eval.getArg(-1).toStr().startsWith(eval.getArg(0));

    case ENDS_WITH:
      if (length < 1)
        return ESBoolean.FALSE;
      else
        return eval.getArg(-1).toStr().endsWith(eval.getArg(0));

    case GET_BYTES:
      if (length < 1)
        return eval.wrap(eval.getArgString(-1, length).getBytes());
      else
        return eval.wrap(eval.getArgString(-1, length).getBytes(eval.getArgString(0, length)));

    default:
      throw new ESException("Unknown object function");
    }
  }

  public ESBase construct(Call eval, int length) throws Throwable
  {
    if (n != NEW)
      throw new ESException("Unknown object function");
 
    return (ESObject) create(eval, length);
  }

  private ESBase create(Call eval, int length) throws Throwable
  {
    ESBase value;
    if (length == 0)
      value = ESString.create("");
    else
      value = eval.getArg(0).toStr();

    return value.toObject();
  }

  private ESBase fromCharCode(Call eval, int length) throws Throwable
  {
    StringBuffer sbuf = new StringBuffer();

    for (int i = 0; i < length; i++) {
      int value = eval.getArg(i).toInt32() & 0xffff;

      sbuf.append((char) value);
    }

    return ESString.create(sbuf.toString());
  }

  private ESBase charAt(Call eval, int length) throws Throwable
  {
    ESString string = eval.getArg(-1).toStr();

    if (length == 0)
      return ESString.create("");

    int value = (int) eval.getArg(0).toNum();

    if (value < 0 || value >= string.length())
      return ESString.create("");
    else
      return ESString.create("" + (char) string.charAt(value));
  }

  private ESBase charCodeAt(Call eval, int length) throws Throwable
  {
    ESString string = eval.getArg(-1).toStr();

    if (length == 0)
      return ESNumber.NaN;

    int value = (int) eval.getArg(0).toNum();

    if (value < 0 || value >= string.length())
      return ESNumber.NaN;
    else
      return ESNumber.create(string.charAt(value));
  }

  private ESBase indexOf(Call eval, int length) throws Throwable
  {
    ESString string = eval.getArg(-1).toStr();

    if (length == 0)
      return ESNumber.create(-1);

    int pos = 0;
    if (length > 1)
      pos = (int) eval.getArg(1).toNum();

    ESString test = eval.getArg(0).toStr();

    return ESNumber.create(string.indexOf(test, pos));
  }

  private ESBase lastIndexOf(Call eval, int length) throws Throwable
  {
    ESString string = eval.getArg(-1).toStr();

    if (length == 0)
      return ESNumber.create(-1);

    int pos = string.length() + 1;
    if (length > 1)
      pos = (int) eval.getArg(1).toNum();

    ESString test = eval.getArg(0).toStr();

    return ESNumber.create(string.lastIndexOf(test, pos));
  }

  private String escapeRegexp(String arg)
  {
    CharBuffer cb = new CharBuffer();

    for (int i = 0; i < arg.length(); i++) {
      int ch;
      switch ((ch = arg.charAt(i))) {
      case '\\': case '-': case '[': case ']': case '(': case ')': 
      case '$': case '^': case '|': case '?': case '*': case '{':
      case '}': case '.':
        cb.append('\\');
        cb.append((char) ch);
        break;

      default:
        cb.append((char) ch);
      }
    }

    return cb.toString();
  }

  private ESBase split(Call eval, int length) throws Throwable
  {
    Global resin = Global.getGlobalProto();
    ESString string = eval.getArg(-1).toStr();

    ESArray array = resin.createArray();

    if (length == 0) {
      array.setProperty(0, string);
      return array;
    }
    else if (eval.getArg(0) instanceof ESRegexp) {
      throw new UnsupportedOperationException();
      
      // splitter = (ESRegexp) eval.getArg(0);
    }
    else {
      String arg = eval.getArg(0).toString();

      String []values = string.toString().split(arg);

      for (int i = 0; i < values.length; i++) {
        array.setProperty(i, ESString.create(values[i]));
      }
      
      /*
      if (arg.length() == 1 && arg.charAt(0) == ' ') {
        splitter = new ESRegexp("\\s", "g");
      } else
        splitter = new ESRegexp(escapeRegexp(arg), "g");
      */
    }

    return array;
  }

  private ESBase substring(Call eval, int length) throws Throwable
  {
    ESString string = eval.getArg(-1).toStr();

    if (length == 0)
      return string;

    int start = (int) eval.getArg(0).toNum();
    int end = string.length();

    if (length > 1)
      end = (int) eval.getArg(1).toNum();

    if (start < 0)
      start = 0;
    if (end > string.length())
      end = string.length();
    if (start > end)
      return string.substring(end, start);
    else
      return string.substring(start, end);
  }

  private ESBase concat(Call eval, int length) throws Throwable
  {
    ESString string = eval.getArg(-1).toStr();

    if (length == 0)
      return string;

    CharBuffer cb = new CharBuffer();
    cb.append(string.toString());

    for (int i = 0; i < length; i++) {
      ESString next = eval.getArg(i).toStr();

      cb.append(next.toString());
    }

    return ESString.create(cb);
  }

  private ESBase match(Call eval, int length) throws Throwable
  {
    /*
    if (length == 0)
      return esNull;
    
    Global resin = Global.getGlobalProto();
    ESString string = eval.getArg(-1).toStr();

    ESBase arg = eval.getArg(0);
    ESRegexp regexp;

    if (arg instanceof ESRegexp)
      regexp = (ESRegexp) arg;
    else if (length > 1)
      regexp = new ESRegexp(arg.toStr(), eval.getArg(1).toStr());
    else
      regexp = new ESRegexp(arg.toStr(), ESString.NULL);

    IntArray results = new IntArray();

    resin.getRegexp().setRegexp(regexp);
    regexp.setLastIndex(0);
    if (! regexp.exec(string))
      return esNull;

    ESArray array = resin.createArray();

    if (! regexp.regexp.isGlobal()) {
      for (int i = 0; i < regexp.regexp.length(); i++) {
        array.setProperty(i, string.substring(regexp.regexp.getBegin(i),
                                              regexp.regexp.getEnd(i)));
      }

      return array;
    }

    int i = 0;
    do {
      array.setProperty(i, string.substring(regexp.regexp.getBegin(0), 
                                            regexp.regexp.getEnd(0)));
      i++;
    } while (regexp.exec(string));

    return array;
    */
    return esNull;
  }

  private void replaceFun(CharBuffer result, String pattern, 
                          ESRegexp regexp, ESBase fun)
    throws Throwable
  {
    /*
    Call call = Global.getGlobalProto().getCall();

    call.top = 1;
    call.setThis(regexp);
    for (int i = 0; i < regexp.regexp.length(); i++) {
      int begin = regexp.regexp.getBegin(i);
      int end = regexp.regexp.getEnd(i);
      call.setArg(i, ESString.create(pattern.substring(begin, end)));
    }

    ESBase value = fun.call(call, regexp.regexp.length());

    Global.getGlobalProto().freeCall(call);

    String string = value.toStr().toString();

    result.append(string);
    */
  }

  /*
  private void replaceString(CharBuffer result, String pattern, 
                             Pattern regexp, String replacement)
    throws Throwable
  {
    int len = replacement.length();

    for (int i = 0; i < len; i++) {
      char ch = replacement.charAt(i);
      
      if (ch == '$' && i + 1 < len) {
        i++;
        ch = replacement.charAt(i);

        if (ch >= '0' && ch <= '9') {
          int index = ch - '0';

          if (index < regexp.length()) {
            int begin = regexp.getBegin(index);
            int end = regexp.getEnd(index);
            result.append(pattern.substring(begin, end));
          }
        } else if (ch == '$')
          result.append('$');
        else if (ch == '&') {
          int begin = regexp.getBegin(0);
          int end = regexp.getEnd(0);
          result.append(pattern.substring(begin, end));
        } else if (ch == '+') {
          int begin = regexp.getBegin(regexp.length() - 1);
          int end = regexp.getEnd(regexp.length() - 1);
          result.append(pattern.substring(begin, end));
        } else if (ch == '`') {
          int begin = 0;
          int end = regexp.getBegin(0);
          result.append(pattern.substring(begin, end));
        } else if (ch == '\'') {
          int begin = regexp.getEnd(0);
          int end = pattern.length();
          result.append(pattern.substring(begin, end));
        } else {
          result.append('$');
          result.append(ch);
        }
      } else {
        result.append(ch);
      }
    }
  }
  */

  private ESBase replace(Call eval, int length) throws Throwable
  {
    ESString string = eval.getArg(-1).toStr();

    if (length < 1)
      return string;

    Global resin = Global.getGlobalProto();
    ESBase arg = eval.getArg(0);
    ESRegexp regexp;

    if (arg instanceof ESRegexp)
      regexp = (ESRegexp) arg;
    else
      regexp = new ESRegexp(arg.toStr(), ESString.NULL);

    IntArray results = new IntArray();
    String pattern = string.toString();

    ESBase replace = null;
    String stringPattern = null;
    if (length > 1)
      replace = eval.getArg(1);

    /* XXX: convert to java.util.regex
    int last = 0;
    CharBuffer result = new CharBuffer();
    resin.getRegexp().setRegexp(regexp);
    if (regexp.regexp.isGlobal())
      regexp.setLastIndex(0);
    while (regexp.exec(string)) {
      result.append(pattern.substring(last, regexp.regexp.getBegin(0)));
      last = regexp.regexp.getEnd(0);

      if (replace instanceof ESClosure) {
        replaceFun(result, pattern, regexp, replace);
      } else {
        if (stringPattern == null)
          stringPattern = replace == null ? "" : replace.toString();

        replaceString(result, pattern, regexp.regexp, stringPattern);
      }

      if (! regexp.regexp.isGlobal())
        break;

    }
        result.append(pattern.substring(last));

    return ESString.create(result);
    */

    return null;
  }

  private ESBase search(Call eval, int length) throws Throwable
  {
    if (length == 0)
      return ESNumber.create(-1);
    
    return ESNumber.create(-1);

    /* XXX: convert to java.util.regex
    ESString string = eval.getArg(-1).toStr();

    ESBase arg = eval.getArg(0);
    ESRegexp regexp;

    if (arg instanceof ESRegexp)
      regexp = (ESRegexp) arg;
    else if (length > 1)
      regexp = new ESRegexp(arg.toStr(), eval.getArg(1).toStr());
    else
      regexp = new ESRegexp(arg.toStr(), ESString.NULL);

    Global.getGlobalProto().getRegexp().setRegexp(regexp);
    if (! regexp.exec(string, false))
      return ESNumber.create(-1);
    else
      return ESNumber.create(regexp.regexp.getBegin(0));
    */
  }

  private ESBase slice(Call eval, int length) throws Throwable
  {
    ESString string = eval.getArg(-1).toStr();

    if (length == 0)
      return string;

    int start = (int) eval.getArg(0).toNum();
    int end = string.length();

    if (length > 1)
      end = (int) eval.getArg(1).toNum();

    if (start < 0)
      start += string.length();
    if (end < 0)
      end += string.length();

    if (start < 0)
      start = 0;
    if (start > string.length())
      start = string.length();

    if (end < 0)
      end = 0;
    if (end > string.length())
      end = string.length();

    if (start <= end)
      return string.substring(start, end);
    else
      return ESString.NULL;
  }

  private ESBase substr(Call eval, int length) throws Throwable
  {
    ESString string = eval.getArg(-1).toStr();

    if (length == 0)
      return string;

    int start = (int) eval.getArg(0).toNum();
    int len = string.length();

    if (length > 1)
      len = (int) eval.getArg(1).toNum();

    if (start < 0)
      start += string.length();

    if (start < 0)
      start = 0;
    if (start > string.length())
      start = string.length();

    if (len <= 0)
      return ESString.NULL;

    int end = start + len;

    if (end > string.length())
      end = string.length();

    return string.substring(start, end);
  }

  private ESBase printf(Call eval, int length) throws Throwable
  {
    if (length == 0)
      return ESString.NULL;

    ESString format = eval.getArg(0).toStr();
    CharBuffer cb = new CharBuffer();

    Printf.printf(cb, format, eval, length);
    
    return ESString.create(cb.toString());
  }
}
