/* 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/. */

/*
 * JS object implementation.
 */

#include "vm/PlainObject-inl.h"

#include "mozilla/Assertions.h"  // MOZ_ASSERT

#include "jspubtd.h"  // JSProto_Object

#include "ds/IdValuePair.h"  // js::IdValuePair
#include "gc/AllocKind.h"    // js::gc::AllocKind
#include "vm/JSContext.h"    // JSContext
#include "vm/JSFunction.h"   // JSFunction
#include "vm/JSObject.h"     // JSObject, js::GetPrototypeFromConstructor
#include "vm/TaggedProto.h"  // js::TaggedProto

#include "vm/JSFunction-inl.h"
#include "vm/JSObject-inl.h"  // js::NewObjectWithGroup, js::NewObjectGCKind

using namespace js;

using JS::Handle;
using JS::Rooted;

static MOZ_ALWAYS_INLINE SharedShape* GetPlainObjectShapeWithProto(
    JSContext* cx, JSObject* proto, gc::AllocKind kind) {
  MOZ_ASSERT(JSCLASS_RESERVED_SLOTS(&PlainObject::class_) == 0,
             "all slots can be used for properties");
  uint32_t nfixed = GetGCKindSlots(kind);
  return SharedShape::getInitialShape(cx, &PlainObject::class_, cx->realm(),
                                      TaggedProto(proto), nfixed);
}

SharedShape* js::ThisShapeForFunction(JSContext* cx, Handle<JSFunction*> callee,
                                      Handle<JSObject*> newTarget) {
  MOZ_ASSERT(cx->realm() == callee->realm());
  MOZ_ASSERT(!callee->constructorNeedsUninitializedThis());

  Rooted<JSObject*> proto(cx);
  if (!GetPrototypeFromConstructor(cx, newTarget, JSProto_Object, &proto)) {
    return nullptr;
  }

  js::gc::AllocKind allocKind = NewObjectGCKind();
  if (!JSFunction::getAllocKindForThis(cx, callee, allocKind)) {
    return nullptr;
  }

  SharedShape* res;
  if (proto && proto != cx->global()->maybeGetPrototype(JSProto_Object)) {
    res = GetPlainObjectShapeWithProto(cx, proto, allocKind);
  } else {
    res = GlobalObject::getPlainObjectShapeWithDefaultProto(cx, allocKind);
  }

  MOZ_ASSERT_IF(res, res->realm() == callee->realm());

  return res;
}

// static
PlainObject* PlainObject::createWithTemplateFromDifferentRealm(
    JSContext* cx, Handle<PlainObject*> templateObject) {
  MOZ_ASSERT(cx->realm() != templateObject->realm(),
             "Use createWithTemplate() for same-realm objects");

  // Currently only implemented for null-proto.
  MOZ_ASSERT(templateObject->staticPrototype() == nullptr);

  // The object mustn't be in dictionary mode.
  MOZ_ASSERT(!templateObject->shape()->isDictionary());

  TaggedProto proto = TaggedProto(nullptr);
  SharedShape* templateShape = templateObject->sharedShape();
  Rooted<SharedPropMap*> map(cx, templateShape->propMap());

  Rooted<SharedShape*> shape(
      cx, SharedShape::getInitialOrPropMapShape(
              cx, &PlainObject::class_, cx->realm(), proto,
              templateShape->numFixedSlots(), map,
              templateShape->propMapLength(), templateShape->objectFlags()));
  if (!shape) {
    return nullptr;
  }
  return createWithShape(cx, shape);
}

// static
SharedShape* GlobalObject::createPlainObjectShapeWithDefaultProto(
    JSContext* cx, gc::AllocKind kind) {
  PlainObjectSlotsKind slotsKind = PlainObjectSlotsKindFromAllocKind(kind);
  GCPtr<SharedShape*>& shapeRef =
      cx->global()->data().plainObjectShapesWithDefaultProto[slotsKind];
  MOZ_ASSERT(!shapeRef);

  JSObject* proto = &cx->global()->getObjectPrototype();
  SharedShape* shape = GetPlainObjectShapeWithProto(cx, proto, kind);
  if (!shape) {
    return nullptr;
  }

  shapeRef.init(shape);
  return shape;
}

PlainObject* js::NewPlainObject(JSContext* cx,
                                const NewObjectOptions& options) {
  gc::AllocKind allocKind = options.allocKind;
  if (allocKind == gc::AllocKind::INVALID) {
    allocKind = gc::AllocKind::OBJECT0;
    MOZ_ASSERT(gc::GetGCObjectKind(&PlainObject::class_) == allocKind);
  }

  Rooted<SharedShape*> shape(
      cx, GlobalObject::getPlainObjectShapeWithDefaultProto(cx, allocKind));
  if (!shape) {
    return nullptr;
  }

  return PlainObject::createWithShape(cx, shape, allocKind, options.newKind);
}

PlainObject* js::NewPlainObjectWithProto(JSContext* cx, HandleObject proto,
                                         const NewObjectOptions& options) {
  // Use a faster path if |proto| is %Object.prototype% (the common case).
  if (proto && proto == cx->global()->maybeGetPrototype(JSProto_Object)) {
    return NewPlainObject(cx, options);
  }

  gc::AllocKind allocKind = options.allocKind;
  if (allocKind == gc::AllocKind::INVALID) {
    allocKind = gc::AllocKind::OBJECT0;
    MOZ_ASSERT(gc::GetGCObjectKind(&PlainObject::class_) == allocKind);
  }

  Rooted<SharedShape*> shape(
      cx, GetPlainObjectShapeWithProto(cx, proto, allocKind));
  if (!shape) {
    return nullptr;
  }

  return PlainObject::createWithShape(cx, shape, allocKind, options.newKind);
}

void js::NewPlainObjectWithPropsCache::add(SharedShape* shape) {
  MOZ_ASSERT(shape);
  MOZ_ASSERT(shape->slotSpan() > 0);
  for (size_t i = NumEntries - 1; i > 0; i--) {
    entries_[i] = entries_[i - 1];
  }
  entries_[0] = shape;
}

static bool ShapeMatches(Handle<IdValueVector> properties, SharedShape* shape) {
  if (shape->slotSpan() != properties.length()) {
    return false;
  }
  SharedShapePropertyIter<NoGC> iter(shape);
  for (size_t i = properties.length(); i > 0; i--) {
    MOZ_ASSERT(iter->isDataProperty());
    MOZ_ASSERT(iter->flags() == PropertyFlags::defaultDataPropFlags);
    if (properties[i - 1].get().id != iter->key()) {
      return false;
    }
    iter++;
  }
  MOZ_ASSERT(iter.done());
  return true;
}

SharedShape* js::NewPlainObjectWithPropsCache::lookup(
    Handle<IdValueVector> properties) const {
  for (size_t i = 0; i < NumEntries; i++) {
    SharedShape* shape = entries_[i];
    if (shape && ShapeMatches(properties, shape)) {
      return shape;
    }
  }
  return nullptr;
}

enum class KeysKind { UniqueNames, Unknown };

template <KeysKind Kind>
static PlainObject* NewPlainObjectWithProperties(
    JSContext* cx, Handle<IdValueVector> properties,
    const NewObjectOptions& options) {
  auto& cache = cx->realm()->newPlainObjectWithPropsCache;

  // If we recently created an object with these properties, we can use that
  // Shape directly.
  if (SharedShape* shape = cache.lookup(properties)) {
    Rooted<SharedShape*> shapeRoot(cx, shape);
    PlainObject* obj =
        PlainObject::createWithShape(cx, shapeRoot, options.newKind);
    if (!obj) {
      return nullptr;
    }
    MOZ_ASSERT(obj->slotSpan() == properties.length());
    for (size_t i = 0; i < properties.length(); i++) {
      obj->initSlot(i, properties[i].get().value);
    }
    return obj;
  }

  gc::AllocKind allocKind = gc::GetGCObjectKind(properties.length());
  Rooted<PlainObject*> obj(cx, NewPlainObject(cx, {.newKind = options.newKind,
                                                   .allocKind = allocKind}));
  if (!obj) {
    return nullptr;
  }

  if (properties.empty()) {
    return obj;
  }

  Rooted<PropertyKey> key(cx);
  Rooted<Value> value(cx);
  bool canCache = true;

  for (const auto& prop : properties) {
    key = prop.id;
    value = prop.value;

    // Integer keys may need to be stored in dense elements. This is uncommon so
    // just fall back to NativeDefineDataProperty.
    if constexpr (Kind == KeysKind::Unknown) {
      if (MOZ_UNLIKELY(key.isInt())) {
        canCache = false;
        if (!NativeDefineDataProperty(cx, obj, key, value, JSPROP_ENUMERATE)) {
          return nullptr;
        }
        continue;
      }
    }

    MOZ_ASSERT(key.isAtom() || key.isSymbol());

    // Check for duplicate keys. In this case we must overwrite the earlier
    // property value.
    if constexpr (Kind == KeysKind::UniqueNames) {
      MOZ_ASSERT(!obj->containsPure(key));
    } else {
      mozilla::Maybe<PropertyInfo> prop = obj->lookup(cx, key);
      if (MOZ_UNLIKELY(prop)) {
        canCache = false;
        MOZ_ASSERT(prop->isDataProperty());
        obj->setSlot(prop->slot(), value);
        continue;
      }
    }

    if (!AddDataPropertyToNativeObjectNoHooks(cx, obj, key, value)) {
      return nullptr;
    }
  }

  if (canCache && !obj->inDictionaryMode()) {
    MOZ_ASSERT(obj->getDenseInitializedLength() == 0);
    MOZ_ASSERT(obj->slotSpan() == properties.length());
    cache.add(obj->sharedShape());
  }

  return obj;
}

PlainObject* js::NewPlainObjectWithUniqueNames(
    JSContext* cx, Handle<IdValueVector> properties,
    const NewObjectOptions& options) {
  return NewPlainObjectWithProperties<KeysKind::UniqueNames>(cx, properties,
                                                             options);
}

PlainObject* js::NewPlainObjectWithMaybeDuplicateKeys(
    JSContext* cx, Handle<IdValueVector> properties,
    const NewObjectOptions& options) {
  return NewPlainObjectWithProperties<KeysKind::Unknown>(cx, properties,
                                                         options);
}
