/*
 * Copyright (c) 1998-2006 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.io.*;
import java.net.*;
import java.util.*;

import com.caucho.util.*;

/**
 * JavaScript object
 */
class NativeArray extends Native {
  static ESId LENGTH = ESId.intern("length");
  static final int NEW = 1;
  static final int JOIN = NEW + 1;
  static final int TO_STRING = JOIN + 1;
  static final int REVERSE = TO_STRING + 1;
  static final int SORT = REVERSE + 1;

  // js1.2
  static final int CONCAT = SORT + 1;
  static final int POP = CONCAT + 1;
  static final int PUSH = POP + 1;
  static final int SHIFT = PUSH + 1;
  static final int UNSHIFT = SHIFT + 1;
  static final int SLICE = UNSHIFT + 1;
  static final int SPLICE = SLICE + 1;

  /**
   * Create a new object based on a prototype
   */
  private NativeArray(String name, int n, int len)
  {
    super(name, len);

    this.n = n;
  }

  /**
   * Creates the native Array object
   */
  static ESObject create(Global resin)
  {
    Native nativeArray = new NativeArray("Array", NEW, 1);
    ESArray proto = new ESArray();
    proto.prototype = resin.objProto;
    NativeWrapper array = new NativeWrapper(resin, nativeArray,
					    proto, ESThunk.ARRAY_THUNK);
    resin.arrayProto = proto;

    put(proto, "join", JOIN, 1);
    put(proto, "toString", TO_STRING, 0);
    put(proto, "reverse", REVERSE, 0);
    put(proto, "sort", SORT, 0);

    // js1.2
    put(proto, "concat", CONCAT, 0);
    put(proto, "pop", POP, 0);
    put(proto, "push", PUSH, 0);
    put(proto, "shift", SHIFT, 0);
    put(proto, "unshift", UNSHIFT, 0);
    put(proto, "slice", SLICE, 2);
    put(proto, "splice", SPLICE, 0);

    proto.setClean();
    array.setClean();

    return array;
  }

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


  public ESBase call(Call eval, int length) throws Throwable
  {
    switch (n) {
    case NEW:
      return create(eval, length);

    case JOIN:
      if (length == 0)
	return toString(eval, length);
      else
	return join(eval, length);

    case TO_STRING:
      return toString(eval, length);

    case REVERSE:
      return reverse(eval, length);

    case SORT:
      return sort(eval, length);

    case CONCAT:
      return concat(eval, length);

    case POP:
      return pop(eval, length);

    case PUSH:
      return push(eval, length);

    case SHIFT:
      return shift(eval, length);

    case UNSHIFT:
      return unshift(eval, length);

    case SLICE:
      return slice(eval, length);

    case SPLICE:
      return splice(eval, length);

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

  ESBase create(Call eval, int length) throws Throwable
  {
    ESObject obj = Global.getGlobalProto().createArray();

    if (length == 0)
      return obj;
    if (length == 1) {
      ESBase arg = eval.getArg(0);

      if (arg instanceof ESNumber)
	obj.setProperty(LENGTH, ESNumber.create(arg.toInt32()));
      else
	obj.setProperty(0, arg);

      return obj;
    }

    for (int i = 0; i < length; i++)
      obj.setProperty(i, eval.getArg(i));

    return obj;
  }

  static ESBase join(ESObject array, String separator) throws Throwable
  {
    if (array.mark != 0) {
      return ESString.create("...");
    }
    array.mark = -1;

    try {
      int len = array.getProperty(LENGTH).toInt32();
      StringBuffer sbuf = new StringBuffer();

      for (int i = 0; i < len; i++) {
	if (i != 0)
	  sbuf.append(separator);

	ESBase value = array.hasProperty(i);

	if (value != null && value != esNull && value != esUndefined)
	  sbuf.append(value.toString());
      }

      return ESString.create(sbuf.toString());
    } finally {
      array.mark = 0;
    }
  }

  ESBase join(Call eval, int length) throws Throwable
  {
    String separator = length == 0 ? "," : eval.getArg(0).toString();

    ESObject array = eval.getArg(-1).toObject();

    return join(array, separator);
  }

  static ESBase toString(ESObject array) throws Throwable
  {
    return join(array, ",");
  }

  // XXX: different for non-js1.2
  ESBase toString(Call eval, int length) throws Throwable
  {
    ESObject array = eval.getArg(-1).toObject();

    return toString(array);
  }

  ESBase reverse(Call eval, int length) throws Throwable
  {
    ESObject array = eval.getArg(-1).toObject();

    int len = (int) array.getProperty(LENGTH).toInt32();
    for (int k = 0; k < len / 2; k++) {
      int firstIndex = k;
      int secondIndex = len - k - 1;

      ESBase first = array.hasProperty(firstIndex);
      ESBase second = array.hasProperty(secondIndex);

      if (first == null)
	array.delete(secondIndex);
      else
	array.setProperty(secondIndex, first);

      if (second == null)
	array.delete(firstIndex);
      else
	array.setProperty(firstIndex, second);
    }

    return array;
  }

  ESBase sort(Call eval, int length) throws Throwable
  {
    ESObject array = eval.getArg(-1).toObject();

    ESBase cmp = length == 0 ? null : eval.getArg(0);

    int len = (int) array.getProperty(LENGTH).toInt32();
    ESBase []values = new ESBase[len];

    for (int i = 0; i < len; i++)
      values[i] = array.getProperty("" + i);

    qsort(values, 0, len, cmp);

    for (int i = 0; i < len; i++) {
      if (values[i] == esUndefined)
	array.delete("" + i);
      else
	array.setProperty("" + i, values[i]);
    }

    return array;
  }

  private void qsort(ESBase []array, int offset, int length, ESBase cmp)
    throws Throwable
  {
    if (length == 2) {
      if (compare(cmp, array[offset], array[offset + 1]) > 0) {
	ESBase temp = array[offset];
	array[offset] = array[offset + 1];
	array[offset + 1] = temp;
      }
    } else if (length > 2) {
      int keyIndex = offset + length / 2;
      ESBase key = array[keyIndex];
      int keys = 0;
      int tail = 0;
      int val;

      if ((val = compare(cmp, array[offset], key)) > 0) {
	key = array[offset];
	array[offset] = array[keyIndex];
	array[keyIndex] = key;
      } else if (val == 0)
	keys++;

      if ((val = compare(cmp, key, array[offset + length - 1])) > 0) {
	key = array[offset + length - 1];
	array[offset + length - 1] = array[keyIndex];
	array[keyIndex] = key;
	keys = 0;
	tail = 1;

	if ((val = compare(cmp, array[offset], key)) > 0) {
	  key = array[offset];
	  array[offset] = array[keyIndex];
	  array[keyIndex] = key;
	} else if (val == 0)
	  keys++;
      } else if (val < 0)
	tail = 1;

      int i;
      if (keyIndex == offset + 1) {
	i = 2 + tail;
	keys++;
      }
      else
	i = 1 + tail;

      for (; i < length; i++) {
	int index = offset + i - tail;

	if (array[index] == key) {
	  keys++;
	  continue;
	}

	int cmpResult = compare(cmp, key, array[index]);
	if (cmpResult > 0 && keys != 0) {
	  ESBase temp = array[index];
	  array[index] = array[index - keys];
	  array[index - keys] = temp; 
	} else if (cmpResult < 0) {
	  ESBase temp = array[offset + length - tail - 1];
	  array[offset + length - tail - 1] = array[index];
	  array[index] = temp;
	  tail += 1;
	} else if (cmpResult == 0)
	  keys++;
      }

      if (length - tail - keys > 1)
	qsort(array, offset, length - tail - keys, cmp);
      if (tail > 1)
	qsort(array, offset + length - tail, tail, cmp);
    }
  }

  private int compare(ESBase cmp, ESBase a, ESBase b)
    throws Throwable
  {
    if (a == b)
      return 0;
    else if (a == esUndefined)
      return 1;
    else if (b == esUndefined)
      return -1;
    else if (a == esNull)
      return 1;
    else if (b == esNull)
      return -1;
    else if (cmp != null) {
      // Call eval = new Call(ESGlobal.getGlobalProto(), false);
      Global resin = Global.getGlobalProto();
      Call eval = resin.getCall();

      eval.stack[0] = esNull;
      eval.stack[1] = a;
      eval.stack[2] = b;
      eval.top = 1;

      int result = cmp.call(eval, 2).toInt32();

      resin.freeCall(eval);

      return result;
    }
    else {
      String sa = a.toString();
      String sb = b.toString();

      return sa.compareTo(sb);
    }
  }

  ESBase concat(Call eval, int length) throws Throwable
  {
    ESArray array = Global.getGlobalProto().createArray();

    int k = 0;
    for (int i = -1; i < length; i++) {
      ESBase arg = eval.getArg(i);

      if (arg == esNull || arg == esUndefined || arg == esEmpty)
	continue;

      ESBase arglen = arg.hasProperty(LENGTH);

      if (arglen == null) {
	array.setProperty(k++, arg);
	continue;
      }

      int len = (int) arglen.toInt32();

      if (len < 0) {
	array.setProperty(k++, arg);
	continue;
      }

      for (int j = 0; j < len; j++) {
	ESBase obj = arg.hasProperty(j);

	if (obj != null)
	  array.setProperty(k, obj);
	k++;
      }
    }
    array.setProperty(LENGTH, ESNumber.create(k));

    return array;
  }

  ESBase pop(Call eval, int length) throws Throwable
  {
    ESObject obj = eval.getArg(-1).toObject();

    ESBase lenObj = obj.hasProperty(LENGTH);
    int len;
    if (lenObj == null || (len = lenObj.toInt32()) <= 0)
      return esUndefined;

    ESBase value = obj.getProperty(len - 1);

    obj.setProperty(LENGTH, ESNumber.create(len - 1));

    return value;
  }

  ESBase push(Call eval, int length) throws Throwable
  {
    ESObject obj = eval.getArg(-1).toObject();

    ESBase lenObj = obj.getProperty(LENGTH);
    int len = lenObj.toInt32();
    if (len < 0)
      len = 0;

    for (int i = 0; i < length; i++)
      obj.setProperty(len + i, eval.getArg(i));

    ESNumber newLen = ESNumber.create(len + length);
    obj.setProperty(LENGTH, newLen);

    return newLen;
  }

  ESBase shift(Call eval, int length) throws Throwable
  {
    ESObject obj = eval.getArg(-1).toObject();

    ESBase lenObj = obj.hasProperty(LENGTH);
    int len;
    if (lenObj == null || (len = (int) lenObj.toInt32()) <= 0)
      return esUndefined;

    ESBase value = obj.getProperty(0);

    for (int i = 1; i < len; i++) {
      ESBase temp = obj.hasProperty(i);
      if (temp == null)
	obj.delete(ESString.create(i - 1));
      else
	obj.setProperty(i - 1, temp);
    }

    obj.setProperty(LENGTH, ESNumber.create(len - 1));

    return value;
  }

  ESBase unshift(Call eval, int length) throws Throwable
  {
    ESObject obj = eval.getArg(-1).toObject();

    ESBase lenObj = obj.getProperty(LENGTH);
    int len = lenObj.toInt32();
    if (len < 0)
      len = 0;

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

    for (int i = len - 1; i >= 0; i--) {
      ESBase value = obj.getProperty(i);

      if (value == null)
	obj.delete(ESString.create(length + i));
      else
	obj.setProperty(length + i, value);
    }

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

      if (value == null)
	obj.delete(ESString.create(i));
      else
	obj.setProperty(i, value);
    }

    ESNumber numLen = ESNumber.create(len + length);
    obj.setProperty(LENGTH, numLen);

    return numLen;
  }

  ESBase slice(Call eval, int length) throws Throwable
  {
    ESObject obj = eval.getArg(-1).toObject();

    ESBase lenObj = obj.getProperty(LENGTH);
    int len = lenObj.toInt32();

    ESArray array = Global.getGlobalProto().createArray();
    if (len <= 0)
      return array;

    int start = 0;
    if (length > 0)
      start = eval.getArg(0).toInt32();
    if (start < 0)
      start += len;
    if (start < 0)
      start = 0;
    if (start > len)
      return array;

    int end = len;
    if (length > 1)
      end = eval.getArg(1).toInt32();
    if (end < 0)
      end += len;
    if (end < 0)
      return array;
    if (end > len)
      end = len;

    if (start >= end)
      return array;

    for (int i = 0; i < end - start; i++) {
      ESBase value = obj.hasProperty(start + i);

      if (value != null)
	array.setProperty(i, value);
    }

    array.setProperty(LENGTH, ESNumber.create(end - start));

    return array;
  }

  ESBase splice(Call eval, int length) throws Throwable
  {
    if (length < 2)
      return esUndefined;

    ESObject obj = eval.getArg(-1).toObject();

    int index = eval.getArg(0).toInt32();
    int count = eval.getArg(1).toInt32();
    boolean single = count == 1;

    ESBase lenObj = obj.getProperty(LENGTH);
    int len = lenObj.toInt32();

    if (index < 0)
      index += len;
    if (index < 0)
      index = 0;

    if (count < 0)
      count = 0;
    if (index + count > len)
      count = len - index;

    ESBase value;

    if (count < 1)
      value = esUndefined;
    else {
      value = Global.getGlobalProto().createArray();

      for (int i = 0; i < count; i++)
	value.setProperty(i, obj.getProperty(index + i));
    }

    int delta = length - 2 - count;
    if (delta < 0) {
      for (int i = 0; i < len - count; i++) {
	ESBase temp = obj.getProperty(i + index + count);
	if (temp == null)
	  obj.delete(ESString.create(i + index + count + delta));
	else
	  obj.setProperty(i + index + count + delta, temp);
      }
    } else if (delta > 0) {
      for (int i = len - count - 1; i >= 0; i--) {
	ESBase temp = obj.getProperty(i + index + count);
	if (temp == null)
	  obj.delete(ESString.create(i + index + count + delta));
	else
	  obj.setProperty(i + index + count + delta, temp);
      }
    }

    for (int i = 0; i < length - 2; i++)
      obj.setProperty(i + index, eval.getArg(i + 2)); 

    obj.setProperty(LENGTH, ESNumber.create(len - count + length - 2));

    return value;
  }

}
