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

// TODO(joshualitt): The below ContextSlots are only available on synthetic
// contexts created by the promise pipeline for use in the promise pipeline.
// However, with Torque we should type the context and its slots to prevent
// accidentially using these slots on contexts which don't support them.
const kPromiseBuiltinsValueSlot: constexpr ContextSlot
    generates 'PromiseBuiltins::kValueSlot';
const kPromiseBuiltinsOnFinallySlot: constexpr ContextSlot
    generates 'PromiseBuiltins::kOnFinallySlot';
const kPromiseBuiltinsConstructorSlot: constexpr ContextSlot
    generates 'PromiseBuiltins::kConstructorSlot';
const kPromiseBuiltinsPromiseValueThunkOrReasonContextLength: constexpr int31
    generates 'PromiseBuiltins::kPromiseValueThunkOrReasonContextLength';
const kPromiseBuiltinsPromiseFinallyContextLength: constexpr int31
    generates 'PromiseBuiltins::kPromiseFinallyContextLength';

transitioning javascript builtin
PromiseValueThunkFinally(
    js-implicit context: Context, receiver: JSAny)(): JSAny {
  return UnsafeCast<JSAny>(context.elements[kPromiseBuiltinsValueSlot]);
}

transitioning javascript builtin
PromiseThrowerFinally(js-implicit context: Context, receiver: JSAny)(): never {
  const reason = UnsafeCast<JSAny>(context.elements[kPromiseBuiltinsValueSlot]);
  Throw(reason);
}

macro CreateThrowerFunction(implicit context: Context)(
    nativeContext: NativeContext, reason: JSAny): JSFunction {
  const throwerContext = AllocateSyntheticFunctionContext(
      nativeContext, kPromiseBuiltinsPromiseValueThunkOrReasonContextLength);
  throwerContext.elements[kPromiseBuiltinsValueSlot] = reason;
  const map = UnsafeCast<Map>(
      nativeContext.elements
          [NativeContextSlot::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 {
  // 1. Let onFinally be F.[[OnFinally]].
  // 2. Assert: IsCallable(onFinally) is true.
  const onFinally =
      UnsafeCast<Callable>(context.elements[kPromiseBuiltinsOnFinallySlot]);

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

  // 4. Let C be F.[[Constructor]].
  const constructor =
      UnsafeCast<JSFunction>(context.elements[kPromiseBuiltinsConstructorSlot]);

  // 5. Assert: IsConstructor(C) is true.
  assert(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 = AllocateSyntheticFunctionContext(
      nativeContext, kPromiseBuiltinsPromiseValueThunkOrReasonContextLength);
  valueThunkContext.elements[kPromiseBuiltinsValueSlot] = value;
  const map = UnsafeCast<Map>(
      nativeContext.elements
          [NativeContextSlot::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 {
  // 1. Let onFinally be F.[[OnFinally]].
  // 2.  Assert: IsCallable(onFinally) is true.
  const onFinally =
      UnsafeCast<Callable>(context.elements[kPromiseBuiltinsOnFinallySlot]);

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

  // 4. Let C be F.[[Constructor]].
  const constructor =
      UnsafeCast<JSFunction>(context.elements[kPromiseBuiltinsConstructorSlot]);

  // 5. Assert: IsConstructor(C) is true.
  assert(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: JSReceiver): PromiseFinallyFunctions {
  const promiseContext = AllocateSyntheticFunctionContext(
      nativeContext, kPromiseBuiltinsPromiseFinallyContextLength);
  promiseContext.elements[kPromiseBuiltinsOnFinallySlot] = onFinally;
  promiseContext.elements[kPromiseBuiltinsConstructorSlot] = constructor;
  const map = UnsafeCast<Map>(
      nativeContext.elements
          [NativeContextSlot::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
  };
}

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%).
  const nativeContext = LoadNativeContext(context);
  const promiseFun = UnsafeCast<Callable>(
      nativeContext.elements[NativeContextSlot::PROMISE_FUNCTION_INDEX]);

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

  // 4. Assert: IsConstructor(C) is true.
  assert(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;
}