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

#include "builtin/WeakRefObject.h"

#include "jsapi.h"

#include "gc/FinalizationObservers.h"
#include "js/friend/ErrorMessages.h"  // js::GetErrorMessage, JSMSG_*
#include "vm/GlobalObject.h"
#include "vm/JSContext.h"

#include "gc/PrivateIterators-inl.h"
#include "gc/WeakMap-inl.h"
#include "vm/JSObject-inl.h"
#include "vm/NativeObject-inl.h"

using namespace js::gc;

namespace js {

/* static */
bool WeakRefObject::construct(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  // https://tc39.es/proposal-weakrefs/#sec-weak-ref-constructor
  // The WeakRef constructor is not intended to be called as a function and will
  // throw an exception when called in that manner.
  if (!ThrowIfNotConstructing(cx, args, "WeakRef")) {
    return false;
  }

  // https://tc39.es/proposal-weakrefs/#sec-weak-ref-target
  // 1. If NewTarget is undefined, throw a TypeError exception.
  // 2. If Type(target) is not Object, throw a TypeError exception.
  if (!CanBeHeldWeakly(args.get(0))) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_BAD_WEAKREF_TARGET);
    return false;
  }

  // 3. Let weakRef be ? OrdinaryCreateFromConstructor(NewTarget,
  //    "%WeakRefPrototype%", « [[Target]] »).
  RootedObject proto(cx);
  if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_WeakRef, &proto)) {
    return false;
  }

  Rooted<WeakRefObject*> weakRef(
      cx, NewObjectWithClassProto<WeakRefObject>(cx, proto));
  if (!weakRef) {
    return false;
  }

  RootedValue target(cx, args[0]);
  bool isPermanent = false;
  if (target.isObject()) {
    // Fully unwrap the target to register it with the GC.
    RootedObject object(cx, CheckedUnwrapDynamic(&target.toObject(), cx));
    if (!object) {
      ReportAccessDenied(cx);
      return false;
    }

    target = ObjectValue(*object);

    // If the target is a DOM wrapper, preserve it.
    MaybePreserveDOMWrapper(cx, object);
  } else {
    JS::Symbol* symbol = target.toSymbol();
    isPermanent = symbol->isPermanentAndMayBeShared();
  }

  // Skip the following steps for permanent targets.
  // (See the note following https://tc39.es/ecma262/#sec-canbeheldweakly)
  if (!isPermanent) {
    // 4. Perform AddToKeptObjects(target).
    if (!target.toGCThing()->zone()->addToKeptObjects(target)) {
      ReportOutOfMemory(cx);
      return false;
    };

    // Add an entry to the per-zone maps from target JS object to a list of weak
    // ref objects.
    gc::GCRuntime* gc = &cx->runtime()->gc;
    if (!gc->registerWeakRef(cx, target, weakRef)) {
      ReportOutOfMemory(cx);
      return false;
    }
  }

  // 5. Set weakRef.[[Target]] to target.
  weakRef->setReservedSlotGCThingAsPrivate(TargetSlot, target.toGCThing());

  // 6. Return weakRef.
  args.rval().setObject(*weakRef);

  return true;
}

/* static */
void WeakRefObject::trace(JSTracer* trc, JSObject* obj) {
  WeakRefObject* weakRef = &obj->as<WeakRefObject>();

  // The next and prev slots added by the ObserverListObject base class are
  // internal weak pointers and are not traced, even if requested by the tracer.

  if (trc->traceWeakEdges()) {
    Value target = weakRef->target();
    Value prior = target;
    TraceManuallyBarrieredEdge(trc, &target, "WeakRefObject::target");
    if (target != prior) {
      weakRef->setTargetUnbarriered(target);
    }
  }
}

/* static */
void WeakRefObject::finalize(JS::GCContext* gcx, JSObject* obj) {
  auto* weakRef = &obj->as<WeakRefObject>();
  weakRef->clearTargetAndUnlink();
}

const JSClassOps WeakRefObject::classOps_ = {
    .finalize = finalize,
    .trace = trace,
};

const ClassSpec WeakRefObject::classSpec_ = {
    GenericCreateConstructor<WeakRefObject::construct, 1,
                             gc::AllocKind::FUNCTION>,
    GenericCreatePrototype<WeakRefObject>,
    nullptr,
    nullptr,
    WeakRefObject::methods,
    WeakRefObject::properties,
};

const JSClass WeakRefObject::class_ = {
    "WeakRef",
    JSCLASS_HAS_RESERVED_SLOTS(SlotCount) |
        JSCLASS_HAS_CACHED_PROTO(JSProto_WeakRef) | JSCLASS_FOREGROUND_FINALIZE,
    &classOps_, &classSpec_, &classExtension_};

const JSClass WeakRefObject::protoClass_ = {
    // https://tc39.es/proposal-weakrefs/#sec-weak-ref.prototype
    // https://tc39.es/proposal-weakrefs/#sec-properties-of-the-weak-ref-prototype-object
    "WeakRef.prototype",
    JSCLASS_HAS_CACHED_PROTO(JSProto_WeakRef),
    JS_NULL_CLASS_OPS,
    &classSpec_,
};

const JSPropertySpec WeakRefObject::properties[] = {
    JS_STRING_SYM_PS(toStringTag, "WeakRef", JSPROP_READONLY),
    JS_PS_END,
};

const JSFunctionSpec WeakRefObject::methods[] = {
    JS_FN("deref", deref, 0, 0),
    JS_FS_END,
};

Value WeakRefObject::target() {
  Value value = getReservedSlot(TargetSlot);
  if (value.isUndefined()) {
    return UndefinedValue();
  }

  auto* cell = static_cast<Cell*>(value.toPrivate());
  if (cell->is<JSObject>()) {
    return ObjectValue(*cell->as<JSObject>());
  }

  return SymbolValue(cell->as<JS::Symbol>());
}

/* static */
bool WeakRefObject::deref(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  // https://tc39.es/proposal-weakrefs/#sec-weak-ref.prototype.deref
  // 1. Let weakRef be the this value.
  // 2. If Type(weakRef) is not Object, throw a TypeError exception.
  // 3. If weakRef does not have a [[Target]] internal slot, throw a TypeError
  //    exception.
  if (!args.thisv().isObject() ||
      !args.thisv().toObject().is<WeakRefObject>()) {
    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
                              JSMSG_NOT_A_WEAK_REF,
                              "Receiver of WeakRef.deref call");
    return false;
  }

  Rooted<WeakRefObject*> weakRef(cx,
                                 &args.thisv().toObject().as<WeakRefObject>());

  // We need to perform a read barrier, which may clear the target.
  readBarrier(cx, weakRef);

  // 4. Let target be the value of weakRef.[[Target]].
  // 5. If target is not empty,
  //    a. Perform AddToKeptObjects(target).
  //    b. Return target.
  // 6. Return undefined.
  RootedValue target(cx, weakRef->target());
  if (target.isUndefined()) {
    args.rval().setUndefined();
    return true;
  }

  bool isPermanent =
      target.isSymbol() && target.toSymbol()->isPermanentAndMayBeShared();
  if (!isPermanent && !target.toGCThing()->zone()->addToKeptObjects(target)) {
    ReportOutOfMemory(cx);
    return false;
  }

  // Target should be wrapped into the current realm before returning it.
  if (!JS_WrapValue(cx, &target)) {
    return false;
  }

  args.rval().set(target);
  return true;
}

void WeakRefObject::setTarget(Value target) {
  setReservedSlotGCThingAsPrivate(TargetSlot, target.toGCThing());
}
void WeakRefObject::setTargetUnbarriered(Value target) {
  setReservedSlotGCThingAsPrivateUnbarriered(TargetSlot, target.toGCThing());
}

void WeakRefObject::clearTargetAndUnlink() {
  unlink();
  clearReservedSlotGCThingAsPrivate(TargetSlot);
}

/* static */
void WeakRefObject::readBarrier(JSContext* cx, Handle<WeakRefObject*> self) {
  RootedValue target(cx, self->target());
  if (target.isUndefined()) {
    return;
  }

  if (target.isObject() && target.toObject().getClass()->isDOMClass()) {
    // We preserved the target when the WeakRef was created. If it has since
    // been released then the DOM object it wraps has been collected, so clear
    // the target.
    RootedObject obj(cx, &target.toObject());

    // Ensure buffered wrapper preservations are committed because the DOM's
    // hasReleasedWrapperCallback checks the preserving-wrapper flag.
    cx->runtime()->commitPendingWrapperPreservations(obj->zone());

    MOZ_ASSERT(cx->runtime()->hasReleasedWrapperCallback);
    bool wasReleased = cx->runtime()->hasReleasedWrapperCallback(obj);
    if (wasReleased) {
      obj->zone()->finalizationObservers()->removeWeakRefTarget(target, self);
      return;
    }
  }

  gc::ValueReadBarrier(target);
}

namespace gc {

void GCRuntime::traceKeptObjects(JSTracer* trc) {
  for (GCZonesIter zone(this); !zone.done(); zone.next()) {
    zone->traceKeptObjects(trc);
  }
}

}  // namespace gc

}  // namespace js
