/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.javascript;

import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

/**
 * This class implements the Object native object.
 * See ECMA 15.2.
 * @author Norris Boyd
 */
public class NativeObject extends IdScriptableObject implements Map
{
    static final long serialVersionUID = -6345305608474346996L;

    private static final Object OBJECT_TAG = "Object";

    static void init(Scriptable scope, boolean sealed)
    {
        NativeObject obj = new NativeObject();
        obj.exportAsJSClass(MAX_PROTOTYPE_ID, scope, sealed);
    }

    @Override
    public String getClassName()
    {
        return "Object";
    }

    @Override
    public String toString()
    {
        return ScriptRuntime.defaultObjectToString(this);
    }

    @Override
    protected void fillConstructorProperties(IdFunctionObject ctor)
    {
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_getPrototypeOf,
                "getPrototypeOf", 1);
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_keys,
                "keys", 1);
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_getOwnPropertyNames,
                "getOwnPropertyNames", 1);
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_getOwnPropertyDescriptor,
                "getOwnPropertyDescriptor", 2);
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_defineProperty,
                "defineProperty", 3);
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_isExtensible,
                "isExtensible", 1);
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_preventExtensions,
                "preventExtensions", 1);
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_defineProperties,
                "defineProperties", 2);
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_create,
                "create", 2);
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_isSealed,
                "isSealed", 1);
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_isFrozen,
                "isFrozen", 1);
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_seal,
                "seal", 1);
        addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_freeze,
                "freeze", 1);
        super.fillConstructorProperties(ctor);
    }

    @Override
    protected void initPrototypeId(int id)
    {
        String s;
        int arity;
        switch (id) {
          case Id_constructor:    arity=1; s="constructor";    break;
          case Id_toString:       arity=0; s="toString";       break;
          case Id_toLocaleString: arity=0; s="toLocaleString"; break;
          case Id_valueOf:        arity=0; s="valueOf";        break;
          case Id_hasOwnProperty: arity=1; s="hasOwnProperty"; break;
          case Id_propertyIsEnumerable:
            arity=1; s="propertyIsEnumerable"; break;
          case Id_isPrototypeOf:  arity=1; s="isPrototypeOf";  break;
          case Id_toSource:       arity=0; s="toSource";       break;
          case Id___defineGetter__:
            arity=2; s="__defineGetter__";     break;
          case Id___defineSetter__:
            arity=2; s="__defineSetter__";     break;
          case Id___lookupGetter__:
            arity=1; s="__lookupGetter__";     break;
          case Id___lookupSetter__:
            arity=1; s="__lookupSetter__";     break;
          default: throw new IllegalArgumentException(String.valueOf(id));
        }
        initPrototypeMethod(OBJECT_TAG, id, s, arity);
    }

    @Override
    public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope,
                             Scriptable thisObj, Object[] args)
    {
        if (!f.hasTag(OBJECT_TAG)) {
            return super.execIdCall(f, cx, scope, thisObj, args);
        }
        int id = f.methodId();
        switch (id) {
          case Id_constructor: {
            if (thisObj != null) {
                // BaseFunction.construct will set up parent, proto
                return f.construct(cx, scope, args);
            }
            if (args.length == 0 || args[0] == null
                || args[0] == Undefined.instance)
            {
                return new NativeObject();
            }
            return ScriptRuntime.toObject(cx, scope, args[0]);
          }

          case Id_toLocaleString: {
            Object toString = ScriptableObject.getProperty(thisObj, "toString");
            if(!(toString instanceof Callable)) {
                throw ScriptRuntime.notFunctionError(toString);
            }
            Callable fun = (Callable)toString;
            return fun.call(cx, scope, thisObj, ScriptRuntime.emptyArgs);
          }

          case Id_toString: {
            if (cx.hasFeature(Context.FEATURE_TO_STRING_AS_SOURCE)) {
                String s = ScriptRuntime.defaultObjectToSource(cx, scope,
                                                               thisObj, args);
                int L = s.length();
                if (L != 0 && s.charAt(0) == '(' && s.charAt(L - 1) == ')') {
                    // Strip () that surrounds toSource
                    s = s.substring(1, L - 1);
                }
                return s;
            }
            return ScriptRuntime.defaultObjectToString(thisObj);
          }

          case Id_valueOf:
            return thisObj;

          case Id_hasOwnProperty: {
            boolean result;
            Object arg = args.length < 1 ? Undefined.instance : args[0];
            String s = ScriptRuntime.toStringIdOrIndex(cx, arg);
            if (s == null) {
                int index = ScriptRuntime.lastIndexResult(cx);
                result = thisObj.has(index, thisObj);
            } else {
                result = thisObj.has(s, thisObj);
            }
            return ScriptRuntime.wrapBoolean(result);
          }

          case Id_propertyIsEnumerable: {
            boolean result;
            Object arg = args.length < 1 ? Undefined.instance : args[0];
            String s = ScriptRuntime.toStringIdOrIndex(cx, arg);
            if (s == null) {
                int index = ScriptRuntime.lastIndexResult(cx);
                result = thisObj.has(index, thisObj);
                if (result && thisObj instanceof ScriptableObject) {
                    ScriptableObject so = (ScriptableObject)thisObj;
                    int attrs = so.getAttributes(index);
                    result = ((attrs & ScriptableObject.DONTENUM) == 0);
                }
            } else {
                result = thisObj.has(s, thisObj);
                if (result && thisObj instanceof ScriptableObject) {
                    ScriptableObject so = (ScriptableObject)thisObj;
                    int attrs = so.getAttributes(s);
                    result = ((attrs & ScriptableObject.DONTENUM) == 0);
                }
            }
            return ScriptRuntime.wrapBoolean(result);
          }

          case Id_isPrototypeOf: {
            boolean result = false;
            if (args.length != 0 && args[0] instanceof Scriptable) {
                Scriptable v = (Scriptable) args[0];
                do {
                    v = v.getPrototype();
                    if (v == thisObj) {
                        result = true;
                        break;
                    }
                } while (v != null);
            }
            return ScriptRuntime.wrapBoolean(result);
          }

          case Id_toSource:
            return ScriptRuntime.defaultObjectToSource(cx, scope, thisObj,
                                                       args);
          case Id___defineGetter__:
          case Id___defineSetter__:
            {
                if (args.length < 2 || !(args[1] instanceof Callable)) {
                    Object badArg = (args.length >= 2 ? args[1]
                                     : Undefined.instance);
                    throw ScriptRuntime.notFunctionError(badArg);
                }
                if (!(thisObj instanceof ScriptableObject)) {
                    throw Context.reportRuntimeError2(
                        "msg.extend.scriptable",
                        thisObj.getClass().getName(),
                        String.valueOf(args[0]));
                }
                ScriptableObject so = (ScriptableObject)thisObj;
                String name = ScriptRuntime.toStringIdOrIndex(cx, args[0]);
                int index = (name != null ? 0
                             : ScriptRuntime.lastIndexResult(cx));
                Callable getterOrSetter = (Callable)args[1];
                boolean isSetter = (id == Id___defineSetter__);
                so.setGetterOrSetter(name, index, getterOrSetter, isSetter);
                if (so instanceof NativeArray)
                    ((NativeArray)so).setDenseOnly(false);
            }
            return Undefined.instance;

            case Id___lookupGetter__:
            case Id___lookupSetter__:
              {
                  if (args.length < 1 ||
                      !(thisObj instanceof ScriptableObject))
                      return Undefined.instance;

                  ScriptableObject so = (ScriptableObject)thisObj;
                  String name = ScriptRuntime.toStringIdOrIndex(cx, args[0]);
                  int index = (name != null ? 0
                               : ScriptRuntime.lastIndexResult(cx));
                  boolean isSetter = (id == Id___lookupSetter__);
                  Object gs;
                  for (;;) {
                      gs = so.getGetterOrSetter(name, index, isSetter);
                      if (gs != null)
                          break;
                      // If there is no getter or setter for the object itself,
                      // how about the prototype?
                      Scriptable v = so.getPrototype();
                      if (v == null)
                          break;
                      if (v instanceof ScriptableObject)
                          so = (ScriptableObject)v;
                      else
                          break;
                  }
                  if (gs != null)
                      return gs;
              }
              return Undefined.instance;

          case ConstructorId_getPrototypeOf:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                Scriptable obj = ensureScriptable(arg);
                return obj.getPrototype();
              }
          case ConstructorId_keys:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                Scriptable obj = ensureScriptable(arg);
                Object[] ids = obj.getIds();
                for (int i = 0; i < ids.length; i++) {
                  ids[i] = ScriptRuntime.toString(ids[i]);
                }
                return cx.newArray(scope, ids);
              }
          case ConstructorId_getOwnPropertyNames:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                ScriptableObject obj = ensureScriptableObject(arg);
                Object[] ids = obj.getAllIds();
                for (int i = 0; i < ids.length; i++) {
                  ids[i] = ScriptRuntime.toString(ids[i]);
                }
                return cx.newArray(scope, ids);
              }
          case ConstructorId_getOwnPropertyDescriptor:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                // TODO(norris): There's a deeper issue here if
                // arg instanceof Scriptable. Should we create a new
                // interface to admit the new ECMAScript 5 operations?
                ScriptableObject obj = ensureScriptableObject(arg);
                Object nameArg = args.length < 2 ? Undefined.instance : args[1];
                String name = ScriptRuntime.toString(nameArg);
                Scriptable desc = obj.getOwnPropertyDescriptor(cx, name);
                return desc == null ? Undefined.instance : desc;
              }
          case ConstructorId_defineProperty:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                ScriptableObject obj = ensureScriptableObject(arg);
                Object name = args.length < 2 ? Undefined.instance : args[1];
                Object descArg = args.length < 3 ? Undefined.instance : args[2];
                ScriptableObject desc = ensureScriptableObject(descArg);
                obj.defineOwnProperty(cx, name, desc);
                return obj;
              }
          case ConstructorId_isExtensible:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                ScriptableObject obj = ensureScriptableObject(arg);
                return Boolean.valueOf(obj.isExtensible());
              }
          case ConstructorId_preventExtensions:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                ScriptableObject obj = ensureScriptableObject(arg);
                obj.preventExtensions();
                return obj;
              }
          case ConstructorId_defineProperties:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                ScriptableObject obj = ensureScriptableObject(arg);
                Object propsObj = args.length < 2 ? Undefined.instance : args[1];
                Scriptable props = Context.toObject(propsObj, getParentScope());
                obj.defineOwnProperties(cx, ensureScriptableObject(props));
                return obj;
              }
          case ConstructorId_create:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                Scriptable obj = (arg == null) ? null : ensureScriptable(arg);

                ScriptableObject newObject = new NativeObject();
                newObject.setParentScope(getParentScope());
                newObject.setPrototype(obj);

                if (args.length > 1 && args[1] != Undefined.instance) {
                  Scriptable props = Context.toObject(args[1], getParentScope());
                  newObject.defineOwnProperties(cx, ensureScriptableObject(props));
                }

                return newObject;
              }
          case ConstructorId_isSealed:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                ScriptableObject obj = ensureScriptableObject(arg);

                if (obj.isExtensible()) return Boolean.FALSE;

                for (Object name: obj.getAllIds()) {
                  Object configurable = obj.getOwnPropertyDescriptor(cx, name).get("configurable");
                  if (Boolean.TRUE.equals(configurable))
                    return Boolean.FALSE;
                }

                return Boolean.TRUE;
              }
          case ConstructorId_isFrozen:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                ScriptableObject obj = ensureScriptableObject(arg);

                if (obj.isExtensible()) return Boolean.FALSE;

                for (Object name: obj.getAllIds()) {
                  ScriptableObject desc = obj.getOwnPropertyDescriptor(cx, name);
                  if (Boolean.TRUE.equals(desc.get("configurable")))
                    return Boolean.FALSE;
                  if (isDataDescriptor(desc) && Boolean.TRUE.equals(desc.get("writable")))
                    return Boolean.FALSE;
                }

                return Boolean.TRUE;
              }
          case ConstructorId_seal:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                ScriptableObject obj = ensureScriptableObject(arg);

                for (Object name: obj.getAllIds()) {
                  ScriptableObject desc = obj.getOwnPropertyDescriptor(cx, name);
                  if (Boolean.TRUE.equals(desc.get("configurable"))) {
                    desc.put("configurable", desc, Boolean.FALSE);
                    obj.defineOwnProperty(cx, name, desc, false);
                  }
                }
                obj.preventExtensions();

                return obj;
              }
          case ConstructorId_freeze:
              {
                Object arg = args.length < 1 ? Undefined.instance : args[0];
                ScriptableObject obj = ensureScriptableObject(arg);

                for (Object name: obj.getAllIds()) {
                  ScriptableObject desc = obj.getOwnPropertyDescriptor(cx, name);
                  if (isDataDescriptor(desc) && Boolean.TRUE.equals(desc.get("writable")))
                    desc.put("writable", desc, Boolean.FALSE);
                  if (Boolean.TRUE.equals(desc.get("configurable")))
                    desc.put("configurable", desc, Boolean.FALSE);
                  obj.defineOwnProperty(cx, name, desc, false);
                }
                obj.preventExtensions();

                return obj;
              }


          default:
            throw new IllegalArgumentException(String.valueOf(id));
        }
    }

    // methods implementing java.util.Map

    public boolean containsKey(Object key) {
        if (key instanceof String) {
            return has((String) key, this);
        } else if (key instanceof Number) {
            return has(((Number) key).intValue(), this);
        }
        return false;
    }

    public boolean containsValue(Object value) {
        for (Object obj : values()) {
            if (value == obj ||
                    value != null && value.equals(obj)) {
                return true;
            }
        }
        return false;
    }

    public Object remove(Object key) {
        Object value = get(key);
        if (key instanceof String) {
            delete((String) key);
        } else if (key instanceof Number) {
            delete(((Number) key).intValue());
        }
        return value;
    }


    public Set<Object> keySet() {
        return new KeySet();
    }

    public Collection<Object> values() {
        return new ValueCollection();
    }

    public Set<Map.Entry<Object, Object>> entrySet() {
        return new EntrySet();
    }

    public Object put(Object key, Object value) {
        throw new UnsupportedOperationException();
    }

    public void putAll(Map m) {
        throw new UnsupportedOperationException();
    }

    public void clear() {
        throw new UnsupportedOperationException();
    }


    class EntrySet extends AbstractSet<Entry<Object, Object>> {
        @Override
        public Iterator<Entry<Object, Object>> iterator() {
            return new Iterator<Map.Entry<Object, Object>>() {
                Object[] ids = getIds();
                Object key = null;
                int index = 0;

                public boolean hasNext() {
                    return index < ids.length;
                }

                public Map.Entry<Object, Object> next() {
                    final Object ekey = key = ids[index++];
                    final Object value = get(key);
                    return new Map.Entry<Object, Object>() {
                        public Object getKey() {
                            return ekey;
                        }

                        public Object getValue() {
                            return value;
                        }

                        public Object setValue(Object value) {
                            throw new UnsupportedOperationException();
                        }

                        @Override
                        public boolean equals(Object other) {
                            if (!(other instanceof Map.Entry)) {
                                return false;
                            }
                            Map.Entry<?, ?> e = (Map.Entry<?, ?>) other;
                            return (ekey == null ? e.getKey() == null : ekey.equals(e.getKey()))
                                && (value == null ? e.getValue() == null : value.equals(e.getValue()));
                        }

                        @Override
                        public int hashCode() {
                            return (ekey == null ? 0 : ekey.hashCode()) ^
                                   (value == null ? 0 : value.hashCode());
                        }

                        @Override
                        public String toString() {
                            return ekey + "=" + value;
                        }
                    };
                }

                public void remove() {
                    if (key == null) {
                        throw new IllegalStateException();
                    }
                    NativeObject.this.remove(key);
                    key = null;
                }
            };
        }

        @Override
        public int size() {
            return NativeObject.this.size();
        }
    }

    class KeySet extends AbstractSet<Object> {

        @Override
        public boolean contains(Object key) {
            return containsKey(key);
        }

        @Override
        public Iterator<Object> iterator() {
            return new Iterator<Object>() {
                Object[] ids = getIds();
                Object key;
                int index = 0;

                public boolean hasNext() {
                    return index < ids.length;
                }

                public Object next() {
                    try {
                        return (key = ids[index++]);
                    } catch(ArrayIndexOutOfBoundsException e) {
                        key = null;
                        throw new NoSuchElementException();
                    }
                }

                public void remove() {
                    if (key == null) {
                        throw new IllegalStateException();
                    }
                    NativeObject.this.remove(key);
                    key = null;
                }
           };
        }

        @Override
        public int size() {
            return NativeObject.this.size();
        }
    }

    class ValueCollection extends AbstractCollection<Object> {

        @Override
        public Iterator<Object> iterator() {
            return new Iterator<Object>() {
                Object[] ids = getIds();
                Object key;
                int index = 0;

                public boolean hasNext() {
                    return index < ids.length;
                }

                public Object next() {
                    return get((key = ids[index++]));
                }

                public void remove() {
                    if (key == null) {
                        throw new IllegalStateException();
                    }
                    NativeObject.this.remove(key);
                    key = null;
                }
            };
        }

        @Override
        public int size() {
            return NativeObject.this.size();
        }
    }


// #string_id_map#

    @Override
    protected int findPrototypeId(String s)
    {
        int id;
// #generated# Last update: 2007-05-09 08:15:55 EDT
        L0: { id = 0; String X = null; int c;
            L: switch (s.length()) {
            case 7: X="valueOf";id=Id_valueOf; break L;
            case 8: c=s.charAt(3);
                if (c=='o') { X="toSource";id=Id_toSource; }
                else if (c=='t') { X="toString";id=Id_toString; }
                break L;
            case 11: X="constructor";id=Id_constructor; break L;
            case 13: X="isPrototypeOf";id=Id_isPrototypeOf; break L;
            case 14: c=s.charAt(0);
                if (c=='h') { X="hasOwnProperty";id=Id_hasOwnProperty; }
                else if (c=='t') { X="toLocaleString";id=Id_toLocaleString; }
                break L;
            case 16: c=s.charAt(2);
                if (c=='d') {
                    c=s.charAt(8);
                    if (c=='G') { X="__defineGetter__";id=Id___defineGetter__; }
                    else if (c=='S') { X="__defineSetter__";id=Id___defineSetter__; }
                }
                else if (c=='l') {
                    c=s.charAt(8);
                    if (c=='G') { X="__lookupGetter__";id=Id___lookupGetter__; }
                    else if (c=='S') { X="__lookupSetter__";id=Id___lookupSetter__; }
                }
                break L;
            case 20: X="propertyIsEnumerable";id=Id_propertyIsEnumerable; break L;
            }
            if (X!=null && X!=s && !X.equals(s)) id = 0;
            break L0;
        }
// #/generated#
        return id;
    }

    private static final int
        ConstructorId_getPrototypeOf = -1,
        ConstructorId_keys = -2,
        ConstructorId_getOwnPropertyNames = -3,
        ConstructorId_getOwnPropertyDescriptor = -4,
        ConstructorId_defineProperty = -5,
        ConstructorId_isExtensible = -6,
        ConstructorId_preventExtensions = -7,
        ConstructorId_defineProperties= -8,
        ConstructorId_create = -9,
        ConstructorId_isSealed = -10,
        ConstructorId_isFrozen = -11,
        ConstructorId_seal = -12,
        ConstructorId_freeze = -13,

        Id_constructor           = 1,
        Id_toString              = 2,
        Id_toLocaleString        = 3,
        Id_valueOf               = 4,
        Id_hasOwnProperty        = 5,
        Id_propertyIsEnumerable  = 6,
        Id_isPrototypeOf         = 7,
        Id_toSource              = 8,
        Id___defineGetter__      = 9,
        Id___defineSetter__      = 10,
        Id___lookupGetter__      = 11,
        Id___lookupSetter__      = 12,
        MAX_PROTOTYPE_ID         = 12;

// #/string_id_map#
}
