// 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 promise {

type PromiseValueThunkOrReasonContext extends FunctionContext;
extern enum PromiseValueThunkOrReasonContextSlot extends intptr
constexpr 'PromiseBuiltins::PromiseValueThunkOrReasonContextSlot' {
  kValueSlot: Slot<PromiseValueThunkOrReasonContext, JSAny>,
  kPromiseValueThunkOrReasonContextLength
}

type PromiseFinallyContext extends FunctionContext;
extern enum PromiseFinallyContextSlot extends intptr
constexpr 'PromiseBuiltins::PromiseFinallyContextSlot' {
  kOnFinallySlot: Slot<PromiseFinallyContext, Callable>,
  kConstructorSlot: Slot<PromiseFinallyContext, Constructor>,
  kPromiseFinallyContextLength
}

transitioning javascript builtin
PromiseValueThunkFinally(
    js-implicit context: Context, receiver: JSAny)(): JSAny {
  const context = %RawDownCast<PromiseValueThunkOrReasonContext>(context);
  return *ContextSlot(
      context, PromiseValueThunkOrReasonContextSlot::kValueSlot);
}

transitioning javascript builtin
PromiseThrowerFinally(js-implicit context: Context, receiver: JSAny)(): never {
  const context = %RawDownCast<PromiseValueThunkOrReasonContext>(context);
  const reason =
      *ContextSlot(context, PromiseValueThunkOrReasonContextSlot::kValueSlot);
  Throw(reason);
}

macro CreateThrowerFunction(implicit context: Context)(
    nativeContext: NativeContext, reason: JSAny): JSFunction {
  const throwerContext = %RawDownCast<PromiseValueThunkOrReasonContext>(
      AllocateSyntheticFunctionContext(
          nativeContext,
          PromiseValueThunkOrReasonContextSlot::
              kPromiseValueThunkOrReasonContextLength));
  InitContextSlot(
      throwerContext, PromiseValueThunkOrReasonContextSlot::kValueSlot, reason);
  const map = *ContextSlot(
      nativeContext, ContextSlot::STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX);
  const throwerInfo = PromiseThrowerFinallySharedFunConstant();
  return AllocateFunctionWithMapAndContext(map, throwerInfo, throwerContext);
}

transitioning javascript builtin
PromiseCatchFinally(
    js-implicit context: Context, receiver: JSAny)(reason: JSAny): JSAny {
  const context = %RawDownCast<PromiseFinallyContext>(context);
  // 1. Let onFinally be F.[[OnFinally]].
  // 2. Assert: IsCallable(onFinally) is true.
  const onFinally: Callable =
      *ContextSlot(context, PromiseFinallyContextSlot::kOnFinallySlot);

  // 3. Let result be ? Call(onFinally).
  const result = Call(context, onFinally, Undefined);

  // 4. Let C be F.[[Constructor]].
  const constructor: Constructor =
      *ContextSlot(context, PromiseFinallyContextSlot::kConstructorSlot);

  // 5. Assert: IsConstructor(C) is true.
  dcheck(IsConstructor(constructor));

  // 6. Let promise be ? PromiseResolve(C, result).
  const promise = PromiseResolve(constructor, result);

  // 7. Let thrower be equivalent to a function that throws reason.
  const nativeContext = LoadNativeContext(context);
  const thrower = CreateThrowerFunction(nativeContext, reason);

  // 8. Return ? Invoke(promise, "then", « thrower »).
  return UnsafeCast<JSAny>(InvokeThen(nativeContext, promise, thrower));
}

macro CreateValueThunkFunction(implicit context: Context)(
    nativeContext: NativeContext, value: JSAny): JSFunction {
  const valueThunkContext = %RawDownCast<PromiseValueThunkOrReasonContext>(
      AllocateSyntheticFunctionContext(
          nativeContext,
          PromiseValueThunkOrReasonContextSlot::
              kPromiseValueThunkOrReasonContextLength));
  InitContextSlot(
      valueThunkContext, PromiseValueThunkOrReasonContextSlot::kValueSlot,
      value);
  const map = *ContextSlot(
      nativeContext, ContextSlot::STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX);
  const valueThunkInfo = PromiseValueThunkFinallySharedFunConstant();
  return AllocateFunctionWithMapAndContext(
      map, valueThunkInfo, valueThunkContext);
}

transitioning javascript builtin
PromiseThenFinally(
    js-implicit context: Context, receiver: JSAny)(value: JSAny): JSAny {
  const context = %RawDownCast<PromiseFinallyContext>(context);
  // 1. Let onFinally be F.[[OnFinally]].
  // 2.  Assert: IsCallable(onFinally) is true.
  const onFinally =
      *ContextSlot(context, PromiseFinallyContextSlot::kOnFinallySlot);

  // 3. Let result be ?  Call(onFinally).
  const result = Call(context, onFinally, Undefined);

  // 4. Let C be F.[[Constructor]].
  const constructor =
      *ContextSlot(context, PromiseFinallyContextSlot::kConstructorSlot);

  // 5. Assert: IsConstructor(C) is true.
  dcheck(IsConstructor(constructor));

  // 6. Let promise be ? PromiseResolve(C, result).
  const promise = PromiseResolve(constructor, result);

  // 7. Let valueThunk be equivalent to a function that returns value.
  const nativeContext = LoadNativeContext(context);
  const valueThunk = CreateValueThunkFunction(nativeContext, value);

  // 8. Return ? Invoke(promise, "then", « valueThunk »).
  return UnsafeCast<JSAny>(InvokeThen(nativeContext, promise, valueThunk));
}

struct PromiseFinallyFunctions {
  then_finally: JSFunction;
  catch_finally: JSFunction;
}

macro CreatePromiseFinallyFunctions(implicit context: Context)(
    nativeContext: NativeContext, onFinally: Callable,
    constructor: Constructor): PromiseFinallyFunctions {
  const promiseContext =
      %RawDownCast<PromiseFinallyContext>(AllocateSyntheticFunctionContext(
          nativeContext,
          PromiseFinallyContextSlot::kPromiseFinallyContextLength));
  InitContextSlot(
      promiseContext, PromiseFinallyContextSlot::kOnFinallySlot, onFinally);
  InitContextSlot(
      promiseContext, PromiseFinallyContextSlot::kConstructorSlot, constructor);
  const map = *ContextSlot(
      nativeContext, ContextSlot::STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX);
  const thenFinallyInfo = PromiseThenFinallySharedFunConstant();
  const thenFinally =
      AllocateFunctionWithMapAndContext(map, thenFinallyInfo, promiseContext);
  const catchFinallyInfo = PromiseCatchFinallySharedFunConstant();
  const catchFinally =
      AllocateFunctionWithMapAndContext(map, catchFinallyInfo, promiseContext);
  return PromiseFinallyFunctions{
    then_finally: thenFinally,
    catch_finally: catchFinally
  };
}

// https://tc39.es/ecma262/#sec-promise.prototype.finally
transitioning javascript builtin
PromisePrototypeFinally(
    js-implicit context: Context, receiver: JSAny)(onFinally: JSAny): JSAny {
  // 1. Let promise be the this value.
  // 2. If Type(promise) is not Object, throw a TypeError exception.
  const jsReceiver = Cast<JSReceiver>(receiver) otherwise ThrowTypeError(
      MessageTemplate::kCalledOnNonObject, 'Promise.prototype.finally');

  // 3. Let C be ? SpeciesConstructor(promise, %Promise%).
  // This builtin is attached to JSFunction created by the bootstrapper so
  // `context` is the native context.
  check(Is<NativeContext>(context));
  const nativeContext = UnsafeCast<NativeContext>(context);
  const promiseFun = *NativeContextSlot(ContextSlot::PROMISE_FUNCTION_INDEX);

  let constructor: Constructor = UnsafeCast<Constructor>(promiseFun);
  const receiverMap = jsReceiver.map;
  if (!IsJSPromiseMap(receiverMap) ||
      !IsPromiseSpeciesLookupChainIntact(nativeContext, receiverMap))
    deferred {
      constructor =
          UnsafeCast<Constructor>(SpeciesConstructor(jsReceiver, promiseFun));
    }

  // 4. Assert: IsConstructor(C) is true.
  dcheck(IsConstructor(constructor));

  // 5. If IsCallable(onFinally) is not true,
  //    a. Let thenFinally be onFinally.
  //    b. Let catchFinally be onFinally.
  // 6. Else,
  //   a. Let thenFinally be a new built-in function object as defined
  //   in ThenFinally Function.
  //   b. Let catchFinally be a new built-in function object as
  //   defined in CatchFinally Function.
  //   c. Set thenFinally and catchFinally's [[Constructor]] internal
  //   slots to C.
  //   d. Set thenFinally and catchFinally's [[OnFinally]] internal
  //   slots to onFinally.
  let thenFinally: JSAny;
  let catchFinally: JSAny;
  typeswitch (onFinally) {
    case (onFinally: Callable): {
      const pair =
          CreatePromiseFinallyFunctions(nativeContext, onFinally, constructor);
      thenFinally = pair.then_finally;
      catchFinally = pair.catch_finally;
    }
    case (JSAny): deferred {
      thenFinally = onFinally;
      catchFinally = onFinally;
    }
  }

  // 7. Return ? Invoke(promise, "then", « thenFinally, catchFinally »).
  return UnsafeCast<JSAny>(
      InvokeThen(nativeContext, receiver, thenFinally, catchFinally));
}

extern macro PromiseCatchFinallySharedFunConstant(): SharedFunctionInfo;
extern macro PromiseThenFinallySharedFunConstant(): SharedFunctionInfo;
extern macro PromiseThrowerFinallySharedFunConstant(): SharedFunctionInfo;
extern macro PromiseValueThunkFinallySharedFunConstant(): SharedFunctionInfo;
}