// 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'
#include 'src/objects/property-array.h'

namespace promise {

struct PromiseAllWrapResultAsFulfilledFunctor {
  macro Call(_nativeContext: NativeContext, value: JSAny): JSAny {
    return value;
  }
}

struct PromiseAllSettledWrapResultAsFulfilledFunctor {
  transitioning
  macro Call(implicit context: Context)(
      nativeContext: NativeContext, value: JSAny): JSAny {
    // TODO(gsathya): Optimize the creation using a cached map to
    // prevent transitions here.
    // 9. Let obj be ! ObjectCreate(%ObjectPrototype%).
    const objectFunction =
        *NativeContextSlot(nativeContext, ContextSlot::OBJECT_FUNCTION_INDEX);
    const objectFunctionMap =
        UnsafeCast<Map>(objectFunction.prototype_or_initial_map);
    const obj = AllocateJSObjectFromMap(objectFunctionMap);

    // 10. Perform ! CreateDataProperty(obj, "status", "fulfilled").
    FastCreateDataProperty(
        obj, StringConstant('status'), StringConstant('fulfilled'));

    // 11. Perform ! CreateDataProperty(obj, "value", x).
    FastCreateDataProperty(obj, StringConstant('value'), value);
    return obj;
  }
}

struct PromiseAllSettledWrapResultAsRejectedFunctor {
  transitioning
  macro Call(implicit context: Context)(
      nativeContext: NativeContext, value: JSAny): JSAny {
    // TODO(gsathya): Optimize the creation using a cached map to
    // prevent transitions here.
    // 9. Let obj be ! ObjectCreate(%ObjectPrototype%).
    const objectFunction =
        *NativeContextSlot(nativeContext, ContextSlot::OBJECT_FUNCTION_INDEX);
    const objectFunctionMap =
        UnsafeCast<Map>(objectFunction.prototype_or_initial_map);
    const obj = AllocateJSObjectFromMap(objectFunctionMap);

    // 10. Perform ! CreateDataProperty(obj, "status", "rejected").
    FastCreateDataProperty(
        obj, StringConstant('status'), StringConstant('rejected'));

    // 11. Perform ! CreateDataProperty(obj, "reason", x).
    FastCreateDataProperty(obj, StringConstant('reason'), value);
    return obj;
  }
}

extern macro LoadJSReceiverIdentityHash(Object): intptr labels IfNoHash;

type PromiseAllResolveElementContext extends FunctionContext;
extern enum PromiseAllResolveElementContextSlots extends intptr
constexpr 'PromiseBuiltins::PromiseAllResolveElementContextSlots' {
  kPromiseAllResolveElementRemainingSlot:
      Slot<PromiseAllResolveElementContext, Smi>,
  kPromiseAllResolveElementCapabilitySlot:
      Slot<PromiseAllResolveElementContext, PromiseCapability>,
  kPromiseAllResolveElementValuesSlot:
      Slot<PromiseAllResolveElementContext, FixedArray>,
  kPromiseAllResolveElementLength
}
extern operator '[]=' macro StoreContextElement(
    Context, constexpr PromiseAllResolveElementContextSlots, Object): void;
extern operator '[]' macro LoadContextElement(
    Context, constexpr PromiseAllResolveElementContextSlots): Object;

const kPropertyArrayNoHashSentinel: constexpr int31
    generates 'PropertyArray::kNoHashSentinel';

const kPropertyArrayHashFieldMax: constexpr int31
    generates 'PropertyArray::HashField::kMax';

transitioning macro PromiseAllResolveElementClosure<F: type>(
    implicit context: PromiseAllResolveElementContext|NativeContext)(
    value: JSAny, function: JSFunction, wrapResultFunctor: F,
    hasResolveAndRejectClosures: constexpr bool): JSAny {
  // We use the {function}s context as the marker to remember whether this
  // resolve element closure was already called. It points to the resolve
  // element context (which is a FunctionContext) until it was called the
  // first time, in which case we make it point to the native context here
  // to mark this resolve element closure as done.
  let promiseContext: PromiseAllResolveElementContext;
  typeswitch (context) {
    case (NativeContext): deferred {
      return Undefined;
    }
    case (context: PromiseAllResolveElementContext): {
      promiseContext = context;
    }
  }

  assert(
      promiseContext.length ==
      SmiTag(PromiseAllResolveElementContextSlots::
                 kPromiseAllResolveElementLength));
  const nativeContext = LoadNativeContext(promiseContext);
  function.context = nativeContext;

  // Determine the index from the {function}.
  assert(kPropertyArrayNoHashSentinel == 0);
  const identityHash =
      LoadJSReceiverIdentityHash(function) otherwise unreachable;
  assert(identityHash > 0);
  const index = identityHash - 1;

  let remainingElementsCount = *ContextSlot(
      promiseContext,
      PromiseAllResolveElementContextSlots::
          kPromiseAllResolveElementRemainingSlot);

  let values = *ContextSlot(
      promiseContext,
      PromiseAllResolveElementContextSlots::
          kPromiseAllResolveElementValuesSlot);
  const newCapacity = index + 1;
  if (newCapacity > values.length_intptr) deferred {
      // This happens only when the promises are resolved during iteration.
      values = ExtractFixedArray(values, 0, values.length_intptr, newCapacity);
      *ContextSlot(
          promiseContext,
          PromiseAllResolveElementContextSlots::
              kPromiseAllResolveElementValuesSlot) = values;
    }

  // Promise.allSettled, for each input element, has both a resolve and a reject
  // closure that share an [[AlreadyCalled]] boolean. That is, the input element
  // can only be settled once: after resolve is called, reject returns early,
  // and vice versa. Using {function}'s context as the marker only tracks
  // per-closure instead of per-element. When the second resolve/reject closure
  // is called on the same index, values.object[index] will already exist and
  // will not be the hole value. In that case, return early. Everything up to
  // this point is not yet observable to user code. This is not a problem for
  // Promise.all since Promise.all has a single resolve closure (no reject) per
  // element.
  if (hasResolveAndRejectClosures) {
    if (values.objects[index] != TheHole) deferred {
        return Undefined;
      }
  }

  // Update the value depending on whether Promise.all or
  // Promise.allSettled is called.
  const updatedValue = wrapResultFunctor.Call(nativeContext, value);

  values.objects[index] = updatedValue;

  remainingElementsCount = remainingElementsCount - 1;
  check(remainingElementsCount >= 0);

  *ContextSlot(
      promiseContext,
      PromiseAllResolveElementContextSlots::
          kPromiseAllResolveElementRemainingSlot) = remainingElementsCount;
  if (remainingElementsCount == 0) {
    const capability = *ContextSlot(
        promiseContext,
        PromiseAllResolveElementContextSlots::
            kPromiseAllResolveElementCapabilitySlot);
    const resolve = UnsafeCast<JSAny>(capability.resolve);
    const arrayMap =
        *NativeContextSlot(
        nativeContext, ContextSlot::JS_ARRAY_PACKED_ELEMENTS_MAP_INDEX);
    const valuesArray = NewJSArray(arrayMap, values);
    Call(promiseContext, resolve, Undefined, valuesArray);
  }
  return Undefined;
}

transitioning javascript builtin
PromiseAllResolveElementClosure(
    js-implicit context: Context, receiver: JSAny,
    target: JSFunction)(value: JSAny): JSAny {
  const context =
      %RawDownCast<PromiseAllResolveElementContext|NativeContext>(context);
  return PromiseAllResolveElementClosure(
      value, target, PromiseAllWrapResultAsFulfilledFunctor{}, false);
}

transitioning javascript builtin
PromiseAllSettledResolveElementClosure(
    js-implicit context: Context, receiver: JSAny,
    target: JSFunction)(value: JSAny): JSAny {
  const context =
      %RawDownCast<PromiseAllResolveElementContext|NativeContext>(context);
  return PromiseAllResolveElementClosure(
      value, target, PromiseAllSettledWrapResultAsFulfilledFunctor{}, true);
}

transitioning javascript builtin
PromiseAllSettledRejectElementClosure(
    js-implicit context: Context, receiver: JSAny,
    target: JSFunction)(value: JSAny): JSAny {
  const context =
      %RawDownCast<PromiseAllResolveElementContext|NativeContext>(context);
  return PromiseAllResolveElementClosure(
      value, target, PromiseAllSettledWrapResultAsRejectedFunctor{}, true);
}
}