// 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;
}

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

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

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,
      async_task_id: 0
    });
    promise_internal::ZeroOutEmbedderOffsets(promise);
  }

  macro InnerNewJSPromise(implicit context: Context)(): JSPromise {
    const nativeContext = LoadNativeContext(context);
    const promiseFun = UnsafeCast<JSFunction>(
        nativeContext[NativeContextSlot::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,
      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: nativeContext
          [NativeContextSlot::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: nativeContext
          [NativeContextSlot::CONTINUATION_PRESERVED_EMBEDDER_DATA_INDEX]
    };
  }

  // 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);
    if (IsPromiseHookEnabledOrHasAsyncEventDelegate()) {
      runtime::PromiseHookInit(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);

    if (IsPromiseHookEnabledOrHasAsyncEventDelegate()) {
      runtime::PromiseHookInit(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: nativeContext
          [NativeContextSlot::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 = UnsafeCast<JSAny>(
          nativeContext[NativeContextSlot::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 {}
  }
}