// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include 'src/builtins/builtins-promise.h'
#include 'src/builtins/builtins-promise-gen.h'

namespace runtime {
extern transitioning runtime
AllowDynamicFunction(implicit context: Context)(JSAny): JSAny;

extern transitioning runtime
ReportMessageFromMicrotask(implicit context: Context)(JSAny): JSAny;
}

// Unsafe functions that should be used very carefully.
namespace promise_internal {
extern macro PromiseBuiltinsAssembler::ZeroOutEmbedderOffsets(JSPromise): void;

extern macro PromiseBuiltinsAssembler::AllocateJSPromise(Context): HeapObject;
}

extern macro
PromiseBuiltinsAssembler::IsContextPromiseHookEnabled(uint32): bool;

extern macro
PromiseBuiltinsAssembler::PromiseHookFlags(): uint32;

namespace promise {
extern macro IsFunctionWithPrototypeSlotMap(Map): bool;

@export
macro PromiseHasHandler(promise: JSPromise): bool {
  return promise.HasHandler();
}

@export
macro PromiseInit(promise: JSPromise): void {
  promise.reactions_or_result = kZero;
  promise.flags = SmiTag(JSPromiseFlags{
    status: PromiseState::kPending,
    has_handler: false,
    handled_hint: false,
    is_silent: false,
    async_task_id: 0
  });
  promise_internal::ZeroOutEmbedderOffsets(promise);
}

macro InnerNewJSPromise(implicit context: Context)(): JSPromise {
  const promiseFun = *NativeContextSlot(ContextSlot::PROMISE_FUNCTION_INDEX);
  assert(IsFunctionWithPrototypeSlotMap(promiseFun.map));
  const promiseMap = UnsafeCast<Map>(promiseFun.prototype_or_initial_map);
  const promiseHeapObject = promise_internal::AllocateJSPromise(context);
  *UnsafeConstCast(&promiseHeapObject.map) = promiseMap;
  const promise = UnsafeCast<JSPromise>(promiseHeapObject);
  promise.properties_or_hash = kEmptyFixedArray;
  promise.elements = kEmptyFixedArray;
  promise.reactions_or_result = kZero;
  promise.flags = SmiTag(JSPromiseFlags{
    status: PromiseState::kPending,
    has_handler: false,
    handled_hint: false,
    is_silent: false,
    async_task_id: 0
  });
  return promise;
}

macro NewPromiseFulfillReactionJobTask(implicit context: Context)(
    handlerContext: Context, argument: Object, handler: Callable|Undefined,
    promiseOrCapability: JSPromise|PromiseCapability|
    Undefined): PromiseFulfillReactionJobTask {
  const nativeContext = LoadNativeContext(handlerContext);
  return new PromiseFulfillReactionJobTask{
    map: PromiseFulfillReactionJobTaskMapConstant(),
    argument,
    context: handlerContext,
    handler,
    promise_or_capability: promiseOrCapability,
    continuation_preserved_embedder_data:
        *ContextSlot(
        nativeContext, ContextSlot::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX)
  };
}

macro NewPromiseRejectReactionJobTask(implicit context: Context)(
    handlerContext: Context, argument: Object, handler: Callable|Undefined,
    promiseOrCapability: JSPromise|PromiseCapability|
    Undefined): PromiseRejectReactionJobTask {
  const nativeContext = LoadNativeContext(handlerContext);
  return new PromiseRejectReactionJobTask{
    map: PromiseRejectReactionJobTaskMapConstant(),
    argument,
    context: handlerContext,
    handler,
    promise_or_capability: promiseOrCapability,
    continuation_preserved_embedder_data:
        *ContextSlot(
        nativeContext, ContextSlot::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX)
  };
}

@export
transitioning macro RunContextPromiseHookInit(implicit context: Context)(
    promise: JSPromise, parent: Object) {
  const maybeHook = *NativeContextSlot(
      ContextSlot::PROMISE_HOOK_INIT_FUNCTION_INDEX);
  const hook = Cast<Callable>(maybeHook) otherwise return;
  const parentObject = Is<JSPromise>(parent) ? Cast<JSPromise>(parent)
      otherwise unreachable: Undefined;

  try {
    Call(context, hook, Undefined, promise, parentObject);
  } catch (e) {
    runtime::ReportMessageFromMicrotask(e);
  }
}

@export
transitioning macro RunContextPromiseHookResolve(implicit context: Context)(
    promise: JSPromise) {
  RunContextPromiseHook(
      ContextSlot::PROMISE_HOOK_RESOLVE_FUNCTION_INDEX, promise,
      PromiseHookFlags());
}

@export
transitioning macro RunContextPromiseHookResolve(implicit context: Context)(
    promise: JSPromise, flags: uint32) {
  RunContextPromiseHook(
      ContextSlot::PROMISE_HOOK_RESOLVE_FUNCTION_INDEX, promise, flags);
}

@export
transitioning macro RunContextPromiseHookBefore(implicit context: Context)(
    promiseOrCapability: JSPromise|PromiseCapability|Undefined) {
  RunContextPromiseHook(
      ContextSlot::PROMISE_HOOK_BEFORE_FUNCTION_INDEX, promiseOrCapability,
      PromiseHookFlags());
}

@export
transitioning macro RunContextPromiseHookBefore(implicit context: Context)(
    promiseOrCapability: JSPromise|PromiseCapability|Undefined, flags: uint32) {
  RunContextPromiseHook(
      ContextSlot::PROMISE_HOOK_BEFORE_FUNCTION_INDEX, promiseOrCapability,
      flags);
}

@export
transitioning macro RunContextPromiseHookAfter(implicit context: Context)(
    promiseOrCapability: JSPromise|PromiseCapability|Undefined) {
  RunContextPromiseHook(
      ContextSlot::PROMISE_HOOK_AFTER_FUNCTION_INDEX, promiseOrCapability,
      PromiseHookFlags());
}

@export
transitioning macro RunContextPromiseHookAfter(implicit context: Context)(
    promiseOrCapability: JSPromise|PromiseCapability|Undefined, flags: uint32) {
  RunContextPromiseHook(
      ContextSlot::PROMISE_HOOK_AFTER_FUNCTION_INDEX, promiseOrCapability,
      flags);
}

transitioning macro RunContextPromiseHook(implicit context: Context)(
    slot: Slot<NativeContext, Undefined|Callable>,
    promiseOrCapability: JSPromise|PromiseCapability|Undefined, flags: uint32) {
  if (!IsContextPromiseHookEnabled(flags)) return;
  const maybeHook = *NativeContextSlot(slot);
  const hook = Cast<Callable>(maybeHook) otherwise return;

  let promise: JSPromise;
  typeswitch (promiseOrCapability) {
    case (jspromise: JSPromise): {
      promise = jspromise;
    }
    case (capability: PromiseCapability): {
      promise = Cast<JSPromise>(capability.promise) otherwise return;
    }
    case (Undefined): {
      return;
    }
  }

  try {
    Call(context, hook, Undefined, promise);
  } catch (e) {
    runtime::ReportMessageFromMicrotask(e);
  }
}

transitioning macro RunAnyPromiseHookInit(implicit context: Context)(
    promise: JSPromise, parent: Object) {
  const promiseHookFlags = PromiseHookFlags();
  // Fast return if no hooks are set.
  if (promiseHookFlags == 0) return;
  if (IsContextPromiseHookEnabled(promiseHookFlags)) {
    RunContextPromiseHookInit(promise, parent);
  }
  if (IsIsolatePromiseHookEnabledOrHasAsyncEventDelegate(promiseHookFlags)) {
    runtime::PromiseHookInit(promise, parent);
  }
}

// These allocate and initialize a promise with pending state and
// undefined fields.
//
// This uses the given parent as the parent promise for the promise
// init hook.
@export
transitioning macro NewJSPromise(implicit context: Context)(parent: Object):
    JSPromise {
  const instance = InnerNewJSPromise();
  PromiseInit(instance);
  RunAnyPromiseHookInit(instance, parent);
  return instance;
}

// This uses undefined as the parent promise for the promise init
// hook.
@export
transitioning macro NewJSPromise(implicit context: Context)(): JSPromise {
  return NewJSPromise(Undefined);
}

// This allocates and initializes a promise with the given state and
// fields.
@export
transitioning macro NewJSPromise(implicit context: Context)(
    status: constexpr PromiseState, result: JSAny): JSPromise {
  assert(status != PromiseState::kPending);

  const instance = InnerNewJSPromise();
  instance.reactions_or_result = result;
  instance.SetStatus(status);
  promise_internal::ZeroOutEmbedderOffsets(instance);
  RunAnyPromiseHookInit(instance, Undefined);
  return instance;
}

macro NewPromiseReaction(implicit context: Context)(
    handlerContext: Context, next: Zero|PromiseReaction,
    promiseOrCapability: JSPromise|PromiseCapability|Undefined,
    fulfillHandler: Callable|Undefined,
    rejectHandler: Callable|Undefined): PromiseReaction {
  const nativeContext = LoadNativeContext(handlerContext);
  return new PromiseReaction{
    map: PromiseReactionMapConstant(),
    next: next,
    reject_handler: rejectHandler,
    fulfill_handler: fulfillHandler,
    promise_or_capability: promiseOrCapability,
    continuation_preserved_embedder_data:
        *ContextSlot(
        nativeContext, ContextSlot::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX)
  };
}

extern macro PromiseResolveThenableJobTaskMapConstant(): Map;

// https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob
macro NewPromiseResolveThenableJobTask(implicit context: Context)(
    promiseToResolve: JSPromise, thenable: JSReceiver,
    then: Callable): PromiseResolveThenableJobTask {
  // 2. Let getThenRealmResult be GetFunctionRealm(then).
  // 3. If getThenRealmResult is a normal completion, then let thenRealm be
  //    getThenRealmResult.[[Value]].
  // 4. Otherwise, let thenRealm be null.
  //
  // The only cases where |thenRealm| can be null is when |then| is a revoked
  // Proxy object, which would throw when it is called anyway. So instead of
  // setting the context to null as the spec does, we just use the current
  // realm.
  const thenContext: Context = ExtractHandlerContext(then);
  const nativeContext = LoadNativeContext(thenContext);

  // 1. Let job be a new Job abstract closure with no parameters that
  //    captures promiseToResolve, thenable, and then...
  // 5. Return { [[Job]]: job, [[Realm]]: thenRealm }.
  return new PromiseResolveThenableJobTask{
    map: PromiseResolveThenableJobTaskMapConstant(),
    context: nativeContext,
    promise_to_resolve: promiseToResolve,
    thenable,
    then
  };
}

struct InvokeThenOneArgFunctor {
  transitioning
  macro Call(
      nativeContext: NativeContext, then: JSAny, receiver: JSAny, arg1: JSAny,
      _arg2: JSAny): JSAny {
    return Call(nativeContext, then, receiver, arg1);
  }
}

struct InvokeThenTwoArgFunctor {
  transitioning
  macro Call(
      nativeContext: NativeContext, then: JSAny, receiver: JSAny, arg1: JSAny,
      arg2: JSAny): JSAny {
    return Call(nativeContext, then, receiver, arg1, arg2);
  }
}

transitioning
macro InvokeThen<F: type>(implicit context: Context)(
    nativeContext: NativeContext, receiver: JSAny, arg1: JSAny, arg2: JSAny,
    callFunctor: F): JSAny {
  // We can skip the "then" lookup on {receiver} if it's [[Prototype]]
  // is the (initial) Promise.prototype and the Promise#then protector
  // is intact, as that guards the lookup path for the "then" property
  // on JSPromise instances which have the (initial) %PromisePrototype%.
  if (!Is<Smi>(receiver) &&
      IsPromiseThenLookupChainIntact(
          nativeContext, UnsafeCast<HeapObject>(receiver).map)) {
    const then =
        *NativeContextSlot(nativeContext, ContextSlot::PROMISE_THEN_INDEX);
    return callFunctor.Call(nativeContext, then, receiver, arg1, arg2);
  } else
    deferred {
      const then = UnsafeCast<JSAny>(GetProperty(receiver, kThenString));
      return callFunctor.Call(nativeContext, then, receiver, arg1, arg2);
    }
}

transitioning
macro InvokeThen(implicit context: Context)(
    nativeContext: NativeContext, receiver: JSAny, arg: JSAny): JSAny {
  return InvokeThen(
      nativeContext, receiver, arg, Undefined, InvokeThenOneArgFunctor{});
}

transitioning
macro InvokeThen(implicit context: Context)(
    nativeContext: NativeContext, receiver: JSAny, arg1: JSAny,
    arg2: JSAny): JSAny {
  return InvokeThen(
      nativeContext, receiver, arg1, arg2, InvokeThenTwoArgFunctor{});
}

transitioning
macro BranchIfAccessCheckFailed(implicit context: Context)(
    nativeContext: NativeContext, promiseConstructor: JSAny,
    executor: JSAny): void labels IfNoAccess {
  try {
    // If executor is a bound function, load the bound function until we've
    // reached an actual function.
    let foundExecutor = executor;
    while (true) {
      typeswitch (foundExecutor) {
        case (f: JSFunction): {
          // Load the context from the function and compare it to the Promise
          // constructor's context. If they match, everything is fine,
          // otherwise, bail out to the runtime.
          const functionContext = f.context;
          const nativeFunctionContext = LoadNativeContext(functionContext);
          if (TaggedEqual(nativeContext, nativeFunctionContext)) {
            goto HasAccess;
          } else {
            goto CallRuntime;
          }
        }
        case (b: JSBoundFunction): {
          foundExecutor = b.bound_target_function;
        }
        case (Object): {
          goto CallRuntime;
        }
      }
    }
  } label CallRuntime deferred {
    const result = runtime::AllowDynamicFunction(promiseConstructor);
    if (result != True) {
      goto IfNoAccess;
    }
  } label HasAccess {}
}
}