/*
 * 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 com.caucho.util.CharBuffer;
import com.caucho.util.IntMap;

import java.util.HashMap;
import java.util.Iterator;

/**
 * Implementation class for a JavaScript Object.
 */
public class ESObject extends ESBase {
  static ESId TO_STRING = ESId.intern("toString");
  static ESId VALUE_OF = ESId.intern("valueOf");
  static ESId CALL = ESId.intern("call");
  static ESId CONSTRUCT = ESId.intern("construct");
  static int DIRTY = 0;
  static int CLEAN = DIRTY + 1;
  static int COW = CLEAN + 1;

  int copyState = DIRTY;

  ESString []propNames;
  ESBase []propValues;
  ESBase []propWatch;
  int []propFlags;
  int size;
  int fill;
  int mask;

  int mark; // for printing
  protected boolean snapPrototype;

  protected ESObject()
  {
  }

  /**
   * Simple constructor for parentless objects.
   */
  public ESObject(String className, ESBase proto)
  {
    init(className, proto, 16);
  }

  protected ESObject(String className, ESBase proto, int hashSize)
  {
    init(className, proto, hashSize < 16 ? 16 : hashSize);
  }

  private void init(String className, ESBase proto, int hashSize)
  {
    if (proto == null) {
      Global resin = Global.getGlobalProto();
      proto = resin == null ? null : resin.objProto;
    }
    if (className == null && proto != null)
      className = proto.className;
    prototype = proto;

    propNames = new ESString[hashSize];
    propValues = new ESBase[hashSize];
    propFlags = new int[hashSize];
    mask = propNames.length - 1;
    size = 0;
    fill = 0;
    copyState = DIRTY;

    this.className = className == null ? "Object" : className;
  }

  void init(String className, ESBase proto)
  {
    init(className, proto, 16);
  }

  void setClean()
  {
    copyState = CLEAN;
  }

  /**
   * Expands the property table
   */
  private void resize(int newSize)
  {
    ESString []newNames = new ESString[newSize];
    ESBase []newValues = new ESBase[newSize];
    int []newFlags = new int[newSize];
    ESBase []newWatch = null;
    if (propWatch != null)
      newWatch = new ESBase[newSize];

    mask = newNames.length - 1;

    for (int i = 0; i < propNames.length; i++) {
      if (propValues[i] == null && (propFlags[i] & WATCH) == 0)
        continue;

      int hash = propNames[i].hashCode() & mask;

      while (true) {
        if (newNames[hash] == null) {
          newNames[hash] = propNames[i];
          newValues[hash] = propValues[i];
          newFlags[hash] = propFlags[i];
          if (newWatch != null)
            newWatch[hash] = propWatch[i];
          break;
        }
        hash = (hash + 1) & mask;
      }
    }

    propNames = newNames;
    propValues = newValues;
    propFlags = newFlags;
    propWatch = newWatch;
    fill = size;
  }

  private void refill()
  {
    for (int i = 0; i < propNames.length; i++) {
      if (propValues[i] == null && (propFlags[i] & WATCH) == 0) {
        propNames[i] = null;
        continue;
      }

      int hash = propNames[i].hashCode() & mask;

      while (true) {
        if (propValues[hash] == null && (propFlags[hash] & WATCH) == 0) {
          propNames[hash] = propNames[i];
          propValues[hash] = propValues[i];
          propFlags[hash] = propFlags[i];
          propNames[i] = null;
          propValues[i] = null;
          propFlags[i] = 0;
          break;
        }
        hash = (hash + 1) & mask;
      }
    }

    fill = size;
  }

  /**
   * Gets a property value.
   */
  public ESBase getProperty(ESString name) throws Throwable
  {
    int hash = name.hashCode() & mask;

    while (true) {
      ESString propName = propNames[hash];

      if (propName == name || name.equals(propName)) {
        ESBase value = propValues[hash];
        return value == null ? prototype.getProperty(name) : value;
      }
      else if (propName == null) {
        ESBase value = prototype.getProperty(name);
        if (snapPrototype)
          setProperty(name, value);
        return value;
      }

      hash = (hash + 1) & mask;
    }
  }

  protected boolean canPut(ESString name)
  {
    int hash = name.hashCode() & mask;

    while (true) {
      ESString propName = propNames[hash];

      if (name.equals(propName) && propValues[hash] != null)
        return (propFlags[hash] & READ_ONLY) == 0;
      else if (propName == null) {
        if (prototype instanceof ESObject)
          return ((ESObject) prototype).canPut(name);
        else
          return true;
      }

      hash = (hash + 1) & mask;
    }
  }

  /*
  public ESBase callWatch(ESString name, int hash, ESBase value)
  throws Exception
  {
    Global resin = Global.getGlobalProto();
    Call call = resin.getCall();
    call.top = 1;

    if (propWatch[hash] instanceof ESClosure)
      call.setArg(-1, ((ESClosure) propWatch[hash]).scope[0]);
    else
      call.setArg(-1, null);
    call.setArg(0, name);
    call.setArg(1, propValues[hash]);
    call.setArg(2, value);
    value = propWatch[hash].call(call, 3);
    resin.freeCall(call);

    return value;
  }
  */

  /**
   * Puts a new value in the property table with the appropriate flags
   */
  public void setProperty(ESString name, ESBase value) throws Throwable
  {
    if (copyState != DIRTY) {
      if (copyState == COW)
        copyAll();
      copyState = DIRTY;
    }

    if (value == esEmpty)
      value = esUndefined;

    int hash = name.hashCode() & mask;

    while (true) {
      ESString propName = propNames[hash];

      if (propValues[hash] == null) {
        if (! prototype.canPut(name))
          return;

        if (propName == null)
          fill++;

        propNames[hash] = name;
        /*
        if ((propFlags[hash] & WATCH) != 0)
          value = callWatch(name, hash, value);
        */
        propValues[hash] = value;
        propFlags[hash] = 0;

        size++;

        if (propNames.length <= 4 * size) {
          resize(4 * propNames.length);
        }
        else if (propNames.length <= 2 * fill)
          refill();

        return;
      }
      else if (propName != name && ! propName.equals(name)) {
        hash = (hash + 1) & mask;
        continue;
      }
      else if ((propFlags[hash] & READ_ONLY) != 0)
        return;
      else {
        /*
        if ((propFlags[hash] & WATCH) != 0)
          value = callWatch(name, hash, value);
        */
        propValues[hash] = value;
        return;
      }
    }
  }

  public void put(ESString name, ESBase value, int flags)
  {
    int hash = name.hashCode() & mask;

    while (true) {
      ESString propName = propNames[hash];

      if (propName == null ||
          propValues[hash] == null || propName.equals(name)) {
        if (propName == null)
          fill++;
        if (propValues[hash] == null)
          size++;

        propNames[hash] = name;
        propValues[hash] = value;
        propFlags[hash] = flags;

        if (propNames.length <= 4 * size) {
          resize(4 * propNames.length);
        }
        else if (propNames.length <= 2 * fill)
          refill();

        return;
      }

      hash = (hash + 1) & mask;
    }
  }

  public void put(String name, ESBase value, int flags)
  {
    ESId id = ESId.intern(name);

    put(id, value, flags);
  }

  /**
   * Deletes the entry.  Returns true if successful.
   */
  public ESBase delete(ESString name) throws Throwable
  {
    if (copyState != DIRTY) {
      if (copyState == COW)
        copyAll();
      copyState = DIRTY;
    }

    int hash = name.hashCode() & mask;

    while (true) {
      ESString hashName = propNames[hash];

      if (hashName == null)
        return ESBoolean.FALSE;
      else if (propValues[hash] != null && hashName.equals(name)) {
        if ((propFlags[hash] & DONT_DELETE) != 0)
          return ESBoolean.FALSE;
        else {
          propValues[hash] = null;
          size--;
          return ESBoolean.TRUE;
        }
      }

      hash = (hash + 1) & mask;
    }
  }

  public void watch(ESString name, ESBase fun)
  {
    if (copyState != DIRTY) {
      if (copyState == COW)
        copyAll();
      copyState = DIRTY;
    }

    int hash = name.hashCode() & mask;

    while (true) {
      ESString propName = propNames[hash];

      if (propValues[hash] == null) {
        if (! prototype.canPut(name))
          return;

        if (propName == null)
          fill++;

        propNames[hash] = name;
        propValues[hash] = esEmpty;
        propFlags[hash] = WATCH;
        if (propWatch == null)
          propWatch = new ESBase[propFlags.length];
        propWatch[hash] = fun;

        size++;

        if (propNames.length <= 4 * size)
          resize(4 * propNames.length);
        else if (propNames.length <= 2 * fill)
          refill();

        return;
      }
      else if (propName != name && ! propName.equals(name)) {
        hash = (hash + 1) & mask;
        continue;
      }
      else if ((propFlags[hash] & READ_ONLY) != 0)
        return;
      else {
        propFlags[hash] |= WATCH;
        if (propWatch == null)
          propWatch = new ESBase[propFlags.length];

        propWatch[hash] = fun;
        return;
      }
    }
  }

  public void unwatch(ESString name)
  {
    if (copyState != DIRTY) {
      if (copyState == COW)
        copyAll();
      copyState = DIRTY;
    }

    int hash = name.hashCode() & mask;

    while (true) {
      ESString propName = propNames[hash];

      if (propName == null)
        return;
      else if (propName.equals(name)) {
        propFlags[hash] &= ~WATCH;

        return;
      }
    }
  }

  /**
   * Sets the named property
   */
  public void put(int i, ESBase value, int flags)
  { 
    put(ESString.create(i), value, flags);
  }

  public Iterator keys() throws ESException
  {
    return new PropertyEnumeration(this);
  }

  public ESBase typeof() throws ESException
  {
    return ESString.create("object");
  }

  /**
   * XXX: not right
   */
  public ESBase toPrimitive(int hint) throws Throwable
  {
    Global resin = Global.getGlobalProto();
    Call eval = resin.getCall();
    eval.global = resin.getGlobal();

    try {
      ESBase fun = hasProperty(hint == STRING ? TO_STRING : VALUE_OF);

      if (fun instanceof ESClosure || fun instanceof Native) {
        eval.stack[0] = this;
        eval.top = 1;
        ESBase value = fun.call(eval, 0);

        if (value instanceof ESBase && ! (value instanceof ESObject))
          return value;
      }

      fun = hasProperty(hint == STRING ? VALUE_OF : TO_STRING);

      if (fun instanceof ESClosure || fun instanceof Native) {
        eval.stack[0] = this;
        eval.top = 1;
        ESBase value = fun.call(eval, 0);

        if (value instanceof ESBase && ! (value instanceof ESObject))
          return value;
      }

      throw new ESException("cannot convert object to primitive type");
    } finally {
      resin.freeCall(eval);
    }
  }

  public ESObject toObject() { return this; }

  public Object toJavaObject() throws ESException
  {
    return this;
  }

  /**
   * Returns a string rep of the object
   */
  public double toNum() throws Throwable
  {
    ESBase value = toPrimitive(NUMBER);

    if (value instanceof ESObject)
      throw new ESException("toPrimitive must return primitive");

    return value.toNum();
  }

  /**
   * Returns a string rep of the object
   */
  public ESString toStr() throws Throwable
  {
    ESBase prim = toPrimitive(STRING);

    if (prim instanceof ESObject)
      throw new ESException("toPrimitive must return primitive");

    return prim.toStr();
  }

  public ESString toSource(IntMap map, boolean isLoopPass) throws Throwable
  {
    CharBuffer cb = new CharBuffer();
    Global resin = Global.getGlobalProto();

    int mark = map.get(this);
    
    if (mark > 0 && isLoopPass)
      return null;
    else if (mark > 0) {
      cb.append("#" + mark + "=");
      map.put(this, -mark);
    } else if (mark == 0 && isLoopPass) {
      map.put(this, resin.addMark());
      return null;
    } else if (mark < 0 && ! isLoopPass) {
      return ESString.create("#" + -mark + "#");
    }

    cb.append("{");

    if (isLoopPass)
      map.put(this, 0);

    Iterator e = keys();

    boolean isFirst = true;
    while (e.hasNext()) {
      if (! isFirst)
        cb.append(", ");
      isFirst = false;

      ESString key = (ESString) e.next();

      cb.append(key);
      cb.append(":");
      ESBase value = getProperty(key);
      if (isLoopPass)
        value.toSource(map, isLoopPass);
      else
        cb.append(value.toSource(map, isLoopPass));
    }

    cb.append("}");

    return new ESString(cb.toString());
  }

  public boolean toBoolean() { return true; }
  
  ESObject dup() { return new ESObject(); }
  
  public Object copy(HashMap refs)
  {
    Object ref = refs.get(this);
    if (ref != null)
      return ref;

    ESObject copy = dup();
    refs.put(this, copy);

    copy(refs, copy);

    return copy;
  }

  private void copyAll()
  {
    copyState = DIRTY;

    int len = propValues.length;

    ESString []newPropNames = new ESString[len];
    int []newPropFlags = new int[len];
    ESBase []newPropValues = new ESBase[len];

    System.arraycopy(propNames, 0, newPropNames, 0, len);
    System.arraycopy(propFlags, 0, newPropFlags, 0, len);
    System.arraycopy(propValues, 0, newPropValues, 0, len);

    propNames = newPropNames;
    propFlags = newPropFlags;
    propValues = newPropValues;

    if (propWatch != null) {
      ESBase []newPropWatch = new ESBase[len];
      System.arraycopy(propWatch, 0, newPropWatch, 0, len);
      propWatch = newPropWatch;
    }
  }

  ESObject resinCopy()
  {
    ESObject obj = dup();

    copy(obj);

    return obj;
  }

  protected void copy(Object newObj)
  {
    ESObject obj = (ESObject) newObj;

    obj.prototype = prototype;
    obj.className = className;

    obj.propNames = propNames;
    obj.propValues = propValues;
    obj.propFlags = propFlags;
    obj.propWatch = propWatch;
    obj.size = size;
    obj.fill = fill;
    obj.mask = mask;
    obj.copyState = copyState;

    if (obj.copyState == DIRTY) {
      throw new RuntimeException();
    }
    else if (copyState == CLEAN) {
      copyState = COW;
      obj.copyState = COW;
    }
  }

  protected void copy(HashMap refs, Object newObj)
  {
    ESObject obj = (ESObject) newObj;

    obj.prototype = (ESBase) prototype.copy(refs);
    obj.className = className;

    obj.propNames = propNames;
    obj.propValues = propValues;
    obj.propFlags = propFlags;
    obj.propWatch = propWatch;
    obj.size = size;
    obj.fill = fill;
    obj.mask = mask;
    obj.copyState = copyState;

    if (obj.copyState == DIRTY) {
      obj.copyAll();
    }
    else if (copyState == CLEAN) {
      copyState = COW;
      obj.copyState = COW;
    }
  }

  ESObject shallowCopy()
  {
    ESObject obj = dup();

    shallowCopy(obj);

    return obj;
  }

  protected void shallowCopy(Object newObj)
  {
    ESObject obj = (ESObject) newObj;

    obj.prototype = prototype;
    obj.className = className;

    int len = propValues.length;

    if (propWatch != null) {
      obj.propWatch = new ESBase[len];
      System.arraycopy(propWatch, 0, obj.propWatch, 0, len);
    }

    obj.propNames = new ESString[len];
    obj.propFlags = new int[len];
    obj.propValues = new ESBase[len];

    ESString []newNames = obj.propNames;
    ESString []oldNames = propNames;
    ESBase []newValues = obj.propValues;
    ESBase []oldValues = propValues;
    int []newFlags = obj.propFlags;
    int []oldFlags = propFlags;
    for (int i = 0; i < len; i++) {
      newNames[i] = oldNames[i];
      newValues[i] = oldValues[i];
      newFlags[i] = oldFlags[i];
    }

    obj.size = size;
    obj.mask = mask;
    obj.fill = fill;
    obj.copyState = DIRTY;
  }

  public boolean ecmaEquals(ESBase b) throws Throwable
  {
    if (b instanceof ESObject || b instanceof ESThunk)
      return this == b;
    else
      return toPrimitive(NONE).ecmaEquals(b);
  }

  public ESBase call(Call call, int length) throws Throwable
  {
    ESBase callFun = hasProperty(CALL);

    if (callFun != null) {
      call.setThis(this);
      return callFun.call(call, length);
    }

    throw new ESNullException(toStr() + " is not a function");
  }

  public ESBase construct(Call call, int length) throws Throwable
  {
    ESBase callFun = hasProperty(CONSTRUCT);

    if (callFun != null) {
      call.setThis(this);
      return callFun.construct(call, length);
    }

    throw new ESNullException(toStr() + " is not a constructor");
  }
}
