/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: set ts=8 sts=2 et sw=2 tw=80:
 * 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 "vm/AsyncIteration.h"

#include "builtin/Array.h"

#include "builtin/Promise.h"  // js::AsyncFromSyncIteratorMethod, js::AsyncGeneratorEnqueue
#include "js/PropertySpec.h"
#include "vm/FunctionFlags.h"  // js::FunctionFlags
#include "vm/GeneratorObject.h"
#include "vm/GlobalObject.h"
#include "vm/Interpreter.h"
#include "vm/PlainObject.h"    // js::PlainObject
#include "vm/PromiseObject.h"  // js::PromiseObject
#include "vm/Realm.h"
#include "vm/SelfHosting.h"

#include "vm/JSContext-inl.h"
#include "vm/JSObject-inl.h"
#include "vm/List-inl.h"

using namespace js;

// ES2019 draft rev 49b781ec80117b60f73327ef3054703a3111e40c
// 6.2.3.1.1 Await Fulfilled Functions
MOZ_MUST_USE bool js::AsyncGeneratorAwaitedFulfilled(
    JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj,
    HandleValue value) {
  return AsyncGeneratorResume(cx, asyncGenObj, CompletionKind::Normal, value);
}

// ES2019 draft rev 49b781ec80117b60f73327ef3054703a3111e40c
// 6.2.3.1.2 Await Rejected Functions
MOZ_MUST_USE bool js::AsyncGeneratorAwaitedRejected(
    JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj,
    HandleValue reason) {
  return AsyncGeneratorResume(cx, asyncGenObj, CompletionKind::Throw, reason);
}

// ES2019 draft rev 49b781ec80117b60f73327ef3054703a3111e40c
// 25.5.3.7 AsyncGeneratorYield, step 8.e.
MOZ_MUST_USE bool js::AsyncGeneratorYieldReturnAwaitedFulfilled(
    JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj,
    HandleValue value) {
  return AsyncGeneratorResume(cx, asyncGenObj, CompletionKind::Return, value);
}

// ES2019 draft rev 49b781ec80117b60f73327ef3054703a3111e40c
// 25.5.3.7 AsyncGeneratorYield, step 8.c.
MOZ_MUST_USE bool js::AsyncGeneratorYieldReturnAwaitedRejected(
    JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj,
    HandleValue reason) {
  return AsyncGeneratorResume(cx, asyncGenObj, CompletionKind::Throw, reason);
}

const JSClass AsyncFromSyncIteratorObject::class_ = {
    "AsyncFromSyncIteratorObject",
    JSCLASS_HAS_RESERVED_SLOTS(AsyncFromSyncIteratorObject::Slots)};

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 25.1.4.1 CreateAsyncFromSyncIterator
JSObject* js::CreateAsyncFromSyncIterator(JSContext* cx, HandleObject iter,
                                          HandleValue nextMethod) {
  // Steps 1-3.
  return AsyncFromSyncIteratorObject::create(cx, iter, nextMethod);
}

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 25.1.4.1 CreateAsyncFromSyncIterator
/* static */
JSObject* AsyncFromSyncIteratorObject::create(JSContext* cx, HandleObject iter,
                                              HandleValue nextMethod) {
  // Step 1.
  RootedObject proto(cx,
                     GlobalObject::getOrCreateAsyncFromSyncIteratorPrototype(
                         cx, cx->global()));
  if (!proto) {
    return nullptr;
  }

  AsyncFromSyncIteratorObject* asyncIter =
      NewObjectWithGivenProto<AsyncFromSyncIteratorObject>(cx, proto);
  if (!asyncIter) {
    return nullptr;
  }

  // Step 2.
  asyncIter->init(iter, nextMethod);

  // Step 3 (Call to 7.4.1 GetIterator).
  // 7.4.1 GetIterator, steps 1-5 are a no-op (*).
  // 7.4.1 GetIterator, steps 6-8 are implemented in bytecode.
  //
  // (*) With <https://github.com/tc39/ecma262/issues/1172> fixed.
  return asyncIter;
}

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 25.1.4.2.1 %AsyncFromSyncIteratorPrototype%.next
static bool AsyncFromSyncIteratorNext(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  return AsyncFromSyncIteratorMethod(cx, args, CompletionKind::Normal);
}

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 25.1.4.2.2 %AsyncFromSyncIteratorPrototype%.return
static bool AsyncFromSyncIteratorReturn(JSContext* cx, unsigned argc,
                                        Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  return AsyncFromSyncIteratorMethod(cx, args, CompletionKind::Return);
}

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 25.1.4.2.3 %AsyncFromSyncIteratorPrototype%.throw
static bool AsyncFromSyncIteratorThrow(JSContext* cx, unsigned argc,
                                       Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);
  return AsyncFromSyncIteratorMethod(cx, args, CompletionKind::Throw);
}

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 25.5.1.2 AsyncGenerator.prototype.next
static bool AsyncGeneratorNext(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  // Steps 1-3.
  return AsyncGeneratorEnqueue(cx, args.thisv(), CompletionKind::Normal,
                               args.get(0), args.rval());
}

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 25.5.1.3 AsyncGenerator.prototype.return
static bool AsyncGeneratorReturn(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  // Steps 1-3.
  return AsyncGeneratorEnqueue(cx, args.thisv(), CompletionKind::Return,
                               args.get(0), args.rval());
}

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 25.5.1.4 AsyncGenerator.prototype.throw
static bool AsyncGeneratorThrow(JSContext* cx, unsigned argc, Value* vp) {
  CallArgs args = CallArgsFromVp(argc, vp);

  // Steps 1-3.
  return AsyncGeneratorEnqueue(cx, args.thisv(), CompletionKind::Throw,
                               args.get(0), args.rval());
}

const JSClass AsyncGeneratorObject::class_ = {
    "AsyncGenerator",
    JSCLASS_HAS_RESERVED_SLOTS(AsyncGeneratorObject::Slots),
    &classOps_,
};

const JSClassOps AsyncGeneratorObject::classOps_ = {
    nullptr,                                   // addProperty
    nullptr,                                   // delProperty
    nullptr,                                   // enumerate
    nullptr,                                   // newEnumerate
    nullptr,                                   // resolve
    nullptr,                                   // mayResolve
    nullptr,                                   // finalize
    nullptr,                                   // call
    nullptr,                                   // hasInstance
    nullptr,                                   // construct
    CallTraceMethod<AbstractGeneratorObject>,  // trace
};

// ES 2017 draft 9.1.13.
// OrdinaryCreateFromConstructor specialized for AsyncGeneratorObjects.
static AsyncGeneratorObject* OrdinaryCreateFromConstructorAsynGen(
    JSContext* cx, HandleFunction fun) {
  // Step 1 (skipped).

  // Step 2.
  RootedValue protoVal(cx);
  if (!GetProperty(cx, fun, fun, cx->names().prototype, &protoVal)) {
    return nullptr;
  }

  RootedObject proto(cx, protoVal.isObject() ? &protoVal.toObject() : nullptr);
  if (!proto) {
    proto = GlobalObject::getOrCreateAsyncGeneratorPrototype(cx, cx->global());
    if (!proto) {
      return nullptr;
    }
  }

  // Step 3.
  return NewObjectWithGivenProto<AsyncGeneratorObject>(cx, proto);
}

/* static */
AsyncGeneratorObject* AsyncGeneratorObject::create(JSContext* cx,
                                                   HandleFunction asyncGen) {
  MOZ_ASSERT(asyncGen->isAsync() && asyncGen->isGenerator());

  AsyncGeneratorObject* asyncGenObj =
      OrdinaryCreateFromConstructorAsynGen(cx, asyncGen);
  if (!asyncGenObj) {
    return nullptr;
  }

  // ES2019 draft rev c2aad21fee7f5ddc89fdf7d3d305618ca3a13242
  // 25.5.3.2 AsyncGeneratorStart.

  // Step 7.
  asyncGenObj->setSuspendedStart();

  // Step 8.
  asyncGenObj->clearSingleQueueRequest();

  asyncGenObj->clearCachedRequest();

  return asyncGenObj;
}

/* static */
AsyncGeneratorRequest* AsyncGeneratorObject::createRequest(
    JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj,
    CompletionKind completionKind, HandleValue completionValue,
    Handle<PromiseObject*> promise) {
  if (!asyncGenObj->hasCachedRequest()) {
    return AsyncGeneratorRequest::create(cx, completionKind, completionValue,
                                         promise);
  }

  AsyncGeneratorRequest* request = asyncGenObj->takeCachedRequest();
  request->init(completionKind, completionValue, promise);
  return request;
}

/* static */ MOZ_MUST_USE bool AsyncGeneratorObject::enqueueRequest(
    JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj,
    Handle<AsyncGeneratorRequest*> request) {
  if (asyncGenObj->isSingleQueue()) {
    if (asyncGenObj->isSingleQueueEmpty()) {
      asyncGenObj->setSingleQueueRequest(request);
      return true;
    }

    Rooted<ListObject*> queue(cx, ListObject::create(cx));
    if (!queue) {
      return false;
    }

    RootedValue requestVal(cx, ObjectValue(*asyncGenObj->singleQueueRequest()));
    if (!queue->append(cx, requestVal)) {
      return false;
    }
    requestVal = ObjectValue(*request);
    if (!queue->append(cx, requestVal)) {
      return false;
    }

    asyncGenObj->setQueue(queue);
    return true;
  }

  Rooted<ListObject*> queue(cx, asyncGenObj->queue());
  RootedValue requestVal(cx, ObjectValue(*request));
  return queue->append(cx, requestVal);
}

/* static */
AsyncGeneratorRequest* AsyncGeneratorObject::dequeueRequest(
    JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj) {
  if (asyncGenObj->isSingleQueue()) {
    AsyncGeneratorRequest* request = asyncGenObj->singleQueueRequest();
    asyncGenObj->clearSingleQueueRequest();
    return request;
  }

  Rooted<ListObject*> queue(cx, asyncGenObj->queue());
  return &queue->popFirstAs<AsyncGeneratorRequest>(cx);
}

/* static */
AsyncGeneratorRequest* AsyncGeneratorObject::peekRequest(
    Handle<AsyncGeneratorObject*> asyncGenObj) {
  if (asyncGenObj->isSingleQueue()) {
    return asyncGenObj->singleQueueRequest();
  }

  return &asyncGenObj->queue()->getAs<AsyncGeneratorRequest>(0);
}

const JSClass AsyncGeneratorRequest::class_ = {
    "AsyncGeneratorRequest",
    JSCLASS_HAS_RESERVED_SLOTS(AsyncGeneratorRequest::Slots)};

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 25.5.3.1 AsyncGeneratorRequest Records
/* static */
AsyncGeneratorRequest* AsyncGeneratorRequest::create(
    JSContext* cx, CompletionKind completionKind, HandleValue completionValue,
    Handle<PromiseObject*> promise) {
  AsyncGeneratorRequest* request =
      NewObjectWithGivenProto<AsyncGeneratorRequest>(cx, nullptr);
  if (!request) {
    return nullptr;
  }

  request->init(completionKind, completionValue, promise);
  return request;
}

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 25.5.3.2 AsyncGeneratorStart
static MOZ_MUST_USE bool AsyncGeneratorReturned(
    JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj,
    HandleValue value) {
  // Step 5.d.
  asyncGenObj->setCompleted();

  // Step 5.e (done in bytecode).
  // Step 5.f.i (implicit).

  // Step 5.g.
  return AsyncGeneratorResolve(cx, asyncGenObj, value, true);
}

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 25.5.3.2 AsyncGeneratorStart
static MOZ_MUST_USE bool AsyncGeneratorThrown(
    JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj) {
  // Step 5.d.
  asyncGenObj->setCompleted();

  // Not much we can do about uncatchable exceptions, so just bail.
  if (!cx->isExceptionPending()) {
    return false;
  }

  // Step 5.f.i.
  RootedValue value(cx);
  if (!GetAndClearException(cx, &value)) {
    return false;
  }

  // Step 5.f.ii.
  return AsyncGeneratorReject(cx, asyncGenObj, value);
}

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 25.5.3.7 AsyncGeneratorYield (partially)
// Most steps are done in generator.
static MOZ_MUST_USE bool AsyncGeneratorYield(
    JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj,
    HandleValue value) {
  // Step 5 is done in bytecode.

  // Step 6.
  asyncGenObj->setSuspendedYield();

  // Step 9.
  return AsyncGeneratorResolve(cx, asyncGenObj, value, false);
}

// ES2019 draft rev c012f9c70847559a1d9dc0d35d35b27fec42911e
// 6.2.3.1 Await, steps 2-9.
// 14.4.13 RS: Evaluation, yield*, steps 7.a.vi, 7.b.ii.7, 7.c.ix.
// 25.5.3.2 AsyncGeneratorStart, steps 5.d-g.
// 25.5.3.5 AsyncGeneratorResumeNext, steps 12-20.
// 25.5.3.7 AsyncGeneratorYield, steps 5-6, 9.
//
// Note: Execution context switching is handled in generator.
MOZ_MUST_USE bool js::AsyncGeneratorResume(
    JSContext* cx, Handle<AsyncGeneratorObject*> asyncGenObj,
    CompletionKind completionKind, HandleValue argument) {
  MOZ_ASSERT(!asyncGenObj->isClosed(),
             "closed generator when resuming async generator");
  MOZ_ASSERT(asyncGenObj->isSuspended(),
             "non-suspended generator when resuming async generator");
  MOZ_ASSERT(asyncGenObj->isExecuting(),
             "async generator not set into 'executing' state");

  // 25.5.3.5, steps 12-14, 16-20.
  HandlePropertyName funName = completionKind == CompletionKind::Normal
                                   ? cx->names().AsyncGeneratorNext
                                   : completionKind == CompletionKind::Throw
                                         ? cx->names().AsyncGeneratorThrow
                                         : cx->names().AsyncGeneratorReturn;
  FixedInvokeArgs<1> args(cx);
  args[0].set(argument);
  RootedValue thisOrRval(cx, ObjectValue(*asyncGenObj));
  if (!CallSelfHostedFunction(cx, funName, thisOrRval, args, &thisOrRval)) {
    // 25.5.3.2, steps 5.f, 5.g.
    if (!asyncGenObj->isClosed()) {
      asyncGenObj->setClosed();
    }
    return AsyncGeneratorThrown(cx, asyncGenObj);
  }

  // 6.2.3.1, steps 2-9.
  if (asyncGenObj->isAfterAwait()) {
    return AsyncGeneratorAwait(cx, asyncGenObj, thisOrRval);
  }

  // 25.5.3.7, steps 5-6, 9.
  if (asyncGenObj->isAfterYield()) {
    return AsyncGeneratorYield(cx, asyncGenObj, thisOrRval);
  }

  // 25.5.3.2, steps 5.d-g.
  return AsyncGeneratorReturned(cx, asyncGenObj, thisOrRval);
}

static const JSFunctionSpec async_iterator_proto_methods[] = {
    JS_SELF_HOSTED_SYM_FN(asyncIterator, "AsyncIteratorIdentity", 0, 0),
    JS_FS_END};

static const JSFunctionSpec async_from_sync_iter_methods[] = {
    JS_FN("next", AsyncFromSyncIteratorNext, 1, 0),
    JS_FN("throw", AsyncFromSyncIteratorThrow, 1, 0),
    JS_FN("return", AsyncFromSyncIteratorReturn, 1, 0), JS_FS_END};

static const JSFunctionSpec async_generator_methods[] = {
    JS_FN("next", AsyncGeneratorNext, 1, 0),
    JS_FN("throw", AsyncGeneratorThrow, 1, 0),
    JS_FN("return", AsyncGeneratorReturn, 1, 0), JS_FS_END};

bool GlobalObject::initAsyncIteratorProto(JSContext* cx,
                                          Handle<GlobalObject*> global) {
  if (global->getReservedSlot(ASYNC_ITERATOR_PROTO).isObject()) {
    return true;
  }

  // 25.1.3 The %AsyncIteratorPrototype% Object
  RootedObject asyncIterProto(
      cx, GlobalObject::createBlankPrototype<PlainObject>(cx, global));
  if (!asyncIterProto) {
    return false;
  }
  if (!DefinePropertiesAndFunctions(cx, asyncIterProto, nullptr,
                                    async_iterator_proto_methods)) {
    return false;
  }

  global->setReservedSlot(ASYNC_ITERATOR_PROTO, ObjectValue(*asyncIterProto));
  return true;
}

bool GlobalObject::initAsyncFromSyncIteratorProto(
    JSContext* cx, Handle<GlobalObject*> global) {
  if (global->getReservedSlot(ASYNC_FROM_SYNC_ITERATOR_PROTO).isObject()) {
    return true;
  }

  RootedObject asyncIterProto(
      cx, GlobalObject::getOrCreateAsyncIteratorPrototype(cx, global));
  if (!asyncIterProto) {
    return false;
  }

  // 25.1.4.2 The %AsyncFromSyncIteratorPrototype% Object
  RootedObject asyncFromSyncIterProto(
      cx, GlobalObject::createBlankPrototypeInheriting(cx, &PlainObject::class_,
                                                       asyncIterProto));
  if (!asyncFromSyncIterProto) {
    return false;
  }
  if (!DefinePropertiesAndFunctions(cx, asyncFromSyncIterProto, nullptr,
                                    async_from_sync_iter_methods) ||
      !DefineToStringTag(cx, asyncFromSyncIterProto,
                         cx->names().AsyncFromSyncIterator)) {
    return false;
  }

  global->setReservedSlot(ASYNC_FROM_SYNC_ITERATOR_PROTO,
                          ObjectValue(*asyncFromSyncIterProto));
  return true;
}

static JSObject* CreateAsyncGeneratorFunction(JSContext* cx, JSProtoKey key) {
  RootedObject proto(
      cx, GlobalObject::getOrCreateFunctionConstructor(cx, cx->global()));
  if (!proto) {
    return nullptr;
  }
  HandlePropertyName name = cx->names().AsyncGeneratorFunction;

  // 25.3.1 The AsyncGeneratorFunction Constructor
  return NewFunctionWithProto(cx, AsyncGeneratorConstructor, 1,
                              FunctionFlags::NATIVE_CTOR, nullptr, name, proto,
                              gc::AllocKind::FUNCTION, SingletonObject);
}

static JSObject* CreateAsyncGeneratorFunctionPrototype(JSContext* cx,
                                                       JSProtoKey key) {
  return NewSingletonObjectWithFunctionPrototype(cx, cx->global());
}

static bool AsyncGeneratorFunctionClassFinish(JSContext* cx,
                                              HandleObject asyncGenFunction,
                                              HandleObject asyncGenerator) {
  Handle<GlobalObject*> global = cx->global();

  // Change the "constructor" property to non-writable before adding any other
  // properties, so it's still the last property and can be modified without a
  // dictionary-mode transition.
  MOZ_ASSERT(StringEqualsAscii(
      JSID_TO_LINEAR_STRING(
          asyncGenerator->as<NativeObject>().lastProperty()->propid()),
      "constructor"));
  MOZ_ASSERT(!asyncGenerator->as<NativeObject>().inDictionaryMode());

  RootedValue asyncGenFunctionVal(cx, ObjectValue(*asyncGenFunction));
  if (!DefineDataProperty(cx, asyncGenerator, cx->names().constructor,
                          asyncGenFunctionVal, JSPROP_READONLY)) {
    return false;
  }
  MOZ_ASSERT(!asyncGenerator->as<NativeObject>().inDictionaryMode());

  RootedObject asyncIterProto(
      cx, GlobalObject::getOrCreateAsyncIteratorPrototype(cx, global));
  if (!asyncIterProto) {
    return false;
  }

  // 25.5 AsyncGenerator Objects
  RootedObject asyncGenProto(cx, GlobalObject::createBlankPrototypeInheriting(
                                     cx, &PlainObject::class_, asyncIterProto));
  if (!asyncGenProto) {
    return false;
  }
  if (!DefinePropertiesAndFunctions(cx, asyncGenProto, nullptr,
                                    async_generator_methods) ||
      !DefineToStringTag(cx, asyncGenProto, cx->names().AsyncGenerator)) {
    return false;
  }

  // 25.3.3 Properties of the AsyncGeneratorFunction Prototype Object
  if (!LinkConstructorAndPrototype(cx, asyncGenerator, asyncGenProto,
                                   JSPROP_READONLY, JSPROP_READONLY) ||
      !DefineToStringTag(cx, asyncGenerator,
                         cx->names().AsyncGeneratorFunction)) {
    return false;
  }

  global->setAsyncGeneratorPrototype(asyncGenProto);

  return true;
}

static const ClassSpec AsyncGeneratorFunctionClassSpec = {
    CreateAsyncGeneratorFunction,
    CreateAsyncGeneratorFunctionPrototype,
    nullptr,
    nullptr,
    nullptr,
    nullptr,
    AsyncGeneratorFunctionClassFinish,
    ClassSpec::DontDefineConstructor};

const JSClass js::AsyncGeneratorFunctionClass = {
    "AsyncGeneratorFunction", 0, JS_NULL_CLASS_OPS,
    &AsyncGeneratorFunctionClassSpec};
