// 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-constructor-gen.h'

namespace typed_array {
extern builtin IterableToListConvertHoles(Context, Object, Callable): JSArray;

extern macro TypedArrayBuiltinsAssembler::AllocateEmptyOnHeapBuffer(
    implicit context: Context)(): JSArrayBuffer;
extern macro TypedArrayBuiltinsAssembler::GetDefaultConstructor(
    implicit context: Context)(JSTypedArray): JSFunction;
extern macro TypedArrayBuiltinsAssembler::SetupTypedArrayEmbedderFields(
    JSTypedArray): void;

extern runtime ThrowInvalidTypedArrayAlignment(
    implicit context: Context)(Map, String): never;

extern runtime GrowableSharedArrayBufferByteLength(
    implicit context: Context)(Object): JSAny;

const kFloat16ArrayFeature: constexpr UseCounterFeature
    generates 'v8::Isolate::kFloat16Array';

transitioning macro AllocateTypedArray(
    implicit context: Context)(isOnHeap: constexpr bool, map: Map,
    buffer: JSArrayBuffer, byteOffset: uintptr, byteLength: uintptr,
    isLengthTracking: bool): JSTypedArray {
  let elements: ByteArray;
  if constexpr (isOnHeap) {
    dcheck(!IsResizableArrayBuffer(buffer));
    dcheck(!isLengthTracking);
    elements = AllocateByteArray(byteLength);
  } else {
    elements = kEmptyByteArray;

    // The max byteOffset is 8 * MaxSmi on the particular platform. 32 bit
    // platforms are self-limiting, because we can't allocate an array bigger
    // than our 32-bit arithmetic range anyway. 64 bit platforms could
    // theoretically have an offset up to 2^35 - 1.
    const backingStore: uintptr = Convert<uintptr>(buffer.backing_store_ptr);

    // Assert no overflow has occurred. Only assert if the mock array buffer
    // allocator is NOT used. When the mock array buffer is used, impossibly
    // large allocations are allowed that would erroneously cause an overflow
    // and this assertion to fail.
    dcheck(
        IsMockArrayBufferAllocatorFlag() ||
        (backingStore + byteOffset) >= backingStore);
  }

  // We can't just build the new object with "new JSTypedArray" here because
  // Torque doesn't know its full size including embedder fields, so use CSA
  // for the allocation step.
  const typedArray =
      UnsafeCast<JSTypedArray>(AllocateFastOrSlowJSObjectFromMap(map));
  typedArray.elements = elements;
  typedArray.buffer = buffer;
  typedArray.byte_offset = byteOffset;
  if (isLengthTracking) {
    dcheck(IsResizableArrayBuffer(buffer));
    // Set the byte_length and length fields of length-tracking TAs to zero, so
    // that we won't accidentally use them and access invalid data.
    typedArray.byte_length = 0;
  } else {
    typedArray.byte_length = byteLength;
  }
  typedArray.bit_field.is_length_tracking = isLengthTracking;
  typedArray.bit_field.is_backed_by_rab =
      IsResizableArrayBuffer(buffer) && !IsSharedArrayBuffer(buffer);
  if constexpr (isOnHeap) {
    typed_array::SetJSTypedArrayOnHeapDataPtr(typedArray, elements, byteOffset);
  } else {
    typed_array::SetJSTypedArrayOffHeapDataPtr(
        typedArray, buffer.backing_store_ptr, byteOffset);
    dcheck(
        typedArray.data_ptr ==
        (buffer.backing_store_ptr + Convert<intptr>(byteOffset)));
  }
  SetupTypedArrayEmbedderFields(typedArray);
  if (typedArray.elements_kind == ElementsKind::FLOAT16_ELEMENTS ||
      typedArray.elements_kind == ElementsKind::RAB_GSAB_FLOAT16_ELEMENTS) {
    IncrementUseCounter(context, SmiConstant(kFloat16ArrayFeature));
  }
  return typedArray;
}

transitioning macro TypedArrayInitialize(
    implicit context: Context)(initialize: constexpr bool, map: Map,
    length: uintptr, elementsInfo: typed_array::TypedArrayElementsInfo):
    JSTypedArray labels IfRangeError {
  const byteLength = elementsInfo.CalculateByteLength(length)
      otherwise IfRangeError;
  const byteLengthNum = Convert<Number>(byteLength);
  const defaultConstructor = GetArrayBufferFunction();
  const byteOffset: uintptr = 0;

  try {
    if (byteLength > kMaxTypedArrayInHeap || kMaxTypedArrayInHeap == 0)
      goto AllocateOffHeap;

    const buffer = AllocateEmptyOnHeapBuffer();

    const isOnHeap: constexpr bool = true;
    const isLengthTracking: constexpr bool = false;
    const typedArray = AllocateTypedArray(
        isOnHeap, map, buffer, byteOffset, byteLength, isLengthTracking);

    if constexpr (initialize) {
      const backingStore = typedArray.data_ptr;
      typed_array::CallCMemset(backingStore, 0, byteLength);
    }

    return typedArray;
  } label AllocateOffHeap {
    if constexpr (initialize) {
      goto AttachOffHeapBuffer(Construct(defaultConstructor, byteLengthNum));
    } else {
      goto AttachOffHeapBuffer(Call(
          context, GetArrayBufferNoInitFunction(), Undefined, byteLengthNum));
    }
  } label AttachOffHeapBuffer(bufferObj: Object) {
    const buffer = Cast<JSArrayBuffer>(bufferObj) otherwise unreachable;
    const isOnHeap: constexpr bool = false;
    const isLengthTracking: constexpr bool = false;
    return AllocateTypedArray(
        isOnHeap, map, buffer, byteOffset, byteLength, isLengthTracking);
  }
}

// 23.2.5.1 TypedArray ( ...args )
// /#sec-typedarray
// ...
// b. If firstArgument is an Object, then
// ...
// c. Else,
transitioning macro ConstructByLength(
    implicit context: Context)(target: JSFunction, newTarget: JSReceiver,
    lengthObj: JSAny): JSTypedArray {
  try {
    // i. Assert: firstArgument is not an Object.
    // ii. Let elementLength be ? ToIndex(firstArgument).
    const length: uintptr = ToIndex(lengthObj) otherwise RangeError;
    const map = GetDerivedMap(target, newTarget);
    const elementsInfo = GetTypedArrayElementsInfo(map);
    const initialize: constexpr bool = true;
    // iii. Return ? AllocateTypedArray(constructorName, NewTarget, proto,
    // elementLength).
    return TypedArrayInitialize(initialize, map, length, elementsInfo)
        otherwise RangeError;
  } label RangeError deferred {
    ThrowRangeError(MessageTemplate::kInvalidTypedArrayLength, lengthObj);
  }
}

// 22.2.4.4 TypedArray ( object )
// ES #sec-typedarray-object
transitioning macro ConstructByArrayLike(
    implicit context: Context)(map: Map, arrayLike: HeapObject,
    length: uintptr,
    elementsInfo: typed_array::TypedArrayElementsInfo): JSTypedArray {
  try {
    const initialize: constexpr bool = false;
    const typedArray =
        TypedArrayInitialize(initialize, map, length, elementsInfo)
        otherwise RangeError;

    try {
      const src: JSTypedArray = Cast<JSTypedArray>(arrayLike) otherwise IfSlow;
      let byteLength: uintptr;
      try {
        byteLength = LoadJSArrayBufferViewByteLength(src, src.buffer)
            otherwise DetachedOrOutOfBounds;
      } label DetachedOrOutOfBounds deferred {
        ThrowTypeError(MessageTemplate::kDetachedOperation, 'Construct');
      }
      if (src.elements_kind != elementsInfo.kind) {
        goto IfElementsKindMismatch(src.elements_kind);

      } else if (length > 0) {
        dcheck(byteLength <= kArrayBufferMaxByteLength);
        if (IsSharedArrayBuffer(src.buffer)) {
          typed_array::CallCRelaxedMemcpy(
              typedArray.data_ptr, src.data_ptr, byteLength);
        } else {
          typed_array::CallCMemcpy(
              typedArray.data_ptr, src.data_ptr, byteLength);
        }
      }
    } label IfElementsKindMismatch(srcKind: ElementsKind) deferred {
      if (IsBigInt64ElementsKind(srcKind) !=
          IsBigInt64ElementsKind(elementsInfo.kind)) {
        ThrowTypeError(MessageTemplate::kBigIntMixedTypes);
      }
      goto IfSlow;
    } label IfSlow deferred {
      if (length > 0) {
        TypedArrayCopyElements(
            context, typedArray, arrayLike, Convert<Number>(length));
      }
    }
    return typedArray;
  } label RangeError deferred {
    ThrowRangeError(
        MessageTemplate::kInvalidTypedArrayLength, Convert<Number>(length));
  }
}

// 22.2.4.4 TypedArray ( object )
// ES #sec-typedarray-object
transitioning macro ConstructByIterable(
    implicit context: Context)(iterable: JSReceiver,
    iteratorFn: Callable): never
    labels IfConstructByArrayLike(JSArray, uintptr) {
  let array: JSArray;
  try {
    array = Cast<JSArray>(iterable) otherwise UseUserProvidedIterator;

    const elementsKind = array.map.elements_kind;
    // Can only use the fast path for numeric fast elements arrays, since
    // objects in the array can have side-effects in their ToNumber conversion.
    if (!IsFastSmiElementsKind(elementsKind) &&
        !IsDoubleElementsKind(elementsKind)) {
      goto UseUserProvidedIterator;
    }

    // Check that the ArrayIterator prototype's "next" method hasn't been
    // overridden.
    if (IsArrayIteratorProtectorCellInvalid()) goto UseUserProvidedIterator;

    // Check that the iterator function is exactly
    // Builtin::kArrayPrototypeValues.
    const iteratorJSFn =
        Cast<JSFunction>(iteratorFn) otherwise UseUserProvidedIterator;
    if (!TaggedEqual(
            iteratorJSFn.shared_function_info.untrusted_function_data,
            SmiConstant(kArrayPrototypeValues))) {
      goto UseUserProvidedIterator;
    }
  } label UseUserProvidedIterator deferred {
    array = IterableToListConvertHoles(context, iterable, iteratorFn);
  }
  // Max JSArray length is a valid JSTypedArray length so we just use it.
  goto IfConstructByArrayLike(array, array.length_uintptr);
}

// 22.2.4.3 TypedArray ( typedArray )
// ES #sec-typedarray-typedarray
transitioning macro ConstructByTypedArray(
    implicit context: Context)(srcTypedArray: JSTypedArray): never
    labels IfConstructByArrayLike(JSTypedArray, uintptr) {
  let length: uintptr;
  try {
    // TODO(petermarshall): Throw on detached typedArray.
    length = LoadJSTypedArrayLengthAndCheckDetached(srcTypedArray)
        otherwise DetachedOrOutOfBounds;
  } label DetachedOrOutOfBounds {
    length = 0;
  }

  goto IfConstructByArrayLike(srcTypedArray, length);
}

// 22.2.4.5 TypedArray ( buffer, byteOffset, length )
// ES #sec-initializetypedarrayfromarraybuffer
transitioning macro ConstructByArrayBuffer(
    implicit context: Context)(target: JSFunction, newTarget: JSReceiver,
    buffer: JSArrayBuffer, byteOffset: JSAny, length: JSAny): JSTypedArray {
  let map: Map;
  const isLengthTracking: bool =
      IsResizableArrayBuffer(buffer) && (length == Undefined);
  // Pick the RAB / GSAB map (containing the corresponding RAB / GSAB
  // ElementsKind). GSAB-backed non-length-tracking TypedArrays behave just like
  // normal TypedArrays, so exclude them.
  const rabGsab: bool = IsResizableArrayBuffer(buffer) &&
      (!IsSharedArrayBuffer(buffer) || isLengthTracking);
  if (rabGsab) {
    map = GetDerivedRabGsabTypedArrayMap(target, newTarget);
  } else {
    map = GetDerivedMap(target, newTarget);
  }

  // 1. Let elementSize be TypedArrayElementSize(O).
  const elementsInfo = GetTypedArrayElementsInfo(map);

  try {
    // 2. Let offset be ? ToIndex(byteOffset).
    const offset: uintptr = ToIndex(byteOffset) otherwise IfInvalidOffset;

    // 3. If offset modulo elementSize ≠ 0, throw a RangeError exception.
    if (elementsInfo.IsUnaligned(offset)) {
      goto IfInvalidAlignment('start offset');
    }

    // 4. Let bufferIsResizable be IsResizableArrayBuffer(buffer).

    // 5. If length is not undefined, then
    // a. Let newLength be ? ToIndex(length).
    let newLength: uintptr = ToIndex(length) otherwise IfInvalidLength;
    let newByteLength: uintptr;

    // 6. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
    if (IsDetachedBuffer(buffer)) {
      ThrowTypeError(MessageTemplate::kDetachedOperation, 'Construct');
    }

    // 7. Let bufferByteLength be ArrayBufferByteLength(buffer, SeqCst).
    let bufferByteLength: uintptr;
    if (IsResizableArrayBuffer(buffer) && IsSharedArrayBuffer(buffer)) {
      bufferByteLength = ToIndex(GrowableSharedArrayBufferByteLength(buffer))
          otherwise unreachable;
    } else {
      bufferByteLength = buffer.byte_length;
    }

    // 8. If length is undefined and bufferIsResizable is true, then
    //   a. If offset > bufferByteLength, throw a RangeError exception.
    //   b. Set O.[[ByteLength]] to auto.
    //   c. Set O.[[ArrayLength]] to auto.
    if (isLengthTracking) {
      if (bufferByteLength < offset) goto IfInvalidOffset;
      newLength = 0;
      newByteLength = 0;
    } else {
      // 9. Else
      //   a. If length is undefined, then
      if (length == Undefined) {
        //   i. If bufferByteLength modulo elementSize ≠ 0, throw a RangeError
        //   exception.
        if (elementsInfo.IsUnaligned(bufferByteLength)) {
          goto IfInvalidAlignment('byte length');
        }

        //   ii. Let newByteLength be bufferByteLength - offset.
        //   iii. If newByteLength < 0, throw a RangeError exception.
        if (bufferByteLength < offset) goto IfInvalidOffset;

        newByteLength = bufferByteLength - offset;
        newLength = elementsInfo.CalculateLength(newByteLength);
      } else {
        // b. Else,
        //   i. Let newByteLength be newLength × elementSize.
        // Since the ArrayBuffer already exists, the typed array view on top
        // of it can't be too big to allocate, but it could still exceed the
        // limit.
        newByteLength = elementsInfo.CalculateByteLength(newLength)
            otherwise IfInvalidLength;

        //   ii. If offset + newByteLength > bufferByteLength, throw a
        //   RangeError
        // exception.
        if ((bufferByteLength < newByteLength) ||
            (offset > bufferByteLength - newByteLength))
          goto IfInvalidLength;
      }
    }

    const isOnHeap: constexpr bool = false;
    return AllocateTypedArray(
        isOnHeap, map, buffer, offset, newByteLength, isLengthTracking);
  } label IfInvalidAlignment(problemString: String) deferred {
    ThrowInvalidTypedArrayAlignment(map, problemString);
  } label IfInvalidLength deferred {
    ThrowRangeError(MessageTemplate::kInvalidTypedArrayLength, length);
  } label IfInvalidOffset deferred {
    ThrowRangeError(MessageTemplate::kInvalidOffset, byteOffset);
  }
}

// 22.2.4.6 TypedArrayCreate ( constructor, argumentList )
// ES #typedarray-create
@export
transitioning macro TypedArrayCreateByLength(
    implicit context: Context)(constructor: Constructor, length: Number,
    methodName: constexpr string): JSTypedArray {
  dcheck(IsSafeInteger(length));

  // 1. Let newTypedArray be ? Construct(constructor, argumentList).
  const newTypedArrayObj = Construct(constructor, length);

  // 2. Perform ? ValidateTypedArray(newTypedArray).
  //    ValidateTypedArray currently returns the array, not the ViewBuffer.
  const newTypedArrayLength =
      ValidateTypedArrayAndGetLength(context, newTypedArrayObj, methodName);
  const newTypedArray: JSTypedArray =
      UnsafeCast<JSTypedArray>(newTypedArrayObj);

  dcheck(
      newTypedArray.bit_field.is_backed_by_rab ==
      (IsResizableArrayBuffer(newTypedArray.buffer) &&
       !IsSharedArrayBuffer(newTypedArray.buffer)));
  dcheck(
      !newTypedArray.bit_field.is_length_tracking ||
      IsResizableArrayBuffer(newTypedArray.buffer));

  if (IsDetachedBuffer(newTypedArray.buffer)) deferred {
      ThrowTypeError(MessageTemplate::kDetachedOperation, methodName);
    }

  // 3. If argumentList is a List of a single Number, then
  //   a. If newTypedArray.[[ArrayLength]] < argumentList[0], throw a
  //      TypeError exception.
  if (newTypedArrayLength < Convert<uintptr>(length)) deferred {
      ThrowTypeError(MessageTemplate::kTypedArrayTooShort);
    }

  // 4. Return newTypedArray.
  return newTypedArray;
}

transitioning macro ConstructByJSReceiver(
    implicit context: Context)(obj: JSReceiver): never
    labels IfConstructByArrayLike(JSReceiver, uintptr),
    IfIteratorNotCallable(JSAny) {
  try {
    // TODO(v8:8906): Use iterator::GetIteratorMethod() once it supports
    // labels.
    const iteratorMethod = GetMethod(obj, IteratorSymbolConstant())
        otherwise IfIteratorUndefined, IfIteratorNotCallable;
    ConstructByIterable(obj, iteratorMethod)
        otherwise IfConstructByArrayLike;
  } label IfIteratorUndefined {
    const lengthObj: JSAny = GetProperty(obj, kLengthString);
    const lengthNumber: Number = ToLength_Inline(lengthObj);
    // Throw RangeError here if the length does not fit in uintptr because
    // such a length will not pass bounds checks in ConstructByArrayLike()
    // anyway.
    const length: uintptr = ChangeSafeIntegerNumberToUintPtr(lengthNumber)
        otherwise goto IfInvalidLength(lengthNumber);
    goto IfConstructByArrayLike(obj, length);
  } label IfInvalidLength(length: Number) {
    ThrowRangeError(MessageTemplate::kInvalidTypedArrayLength, length);
  }
}

// 22.2.4 The TypedArray Constructors
// ES #sec-typedarray-constructors
transitioning builtin CreateTypedArray(
    context: Context, target: JSFunction, newTarget: JSReceiver, arg1: JSAny,
    arg2: JSAny, arg3: JSAny): JSTypedArray {
  dcheck(IsConstructor(target));
  // 4. Let O be ? AllocateTypedArray(constructorName, NewTarget,
  // "%TypedArrayPrototype%").
  try {
    typeswitch (arg1) {
      case (length: Smi): {
        goto IfConstructByLength(length);
      }
      case (buffer: JSArrayBuffer): {
        return ConstructByArrayBuffer(target, newTarget, buffer, arg2, arg3);
      }
      case (typedArray: JSTypedArray): {
        ConstructByTypedArray(typedArray) otherwise IfConstructByArrayLike;
      }
      case (obj: JSReceiver): {
        ConstructByJSReceiver(obj) otherwise IfConstructByArrayLike,
            IfIteratorNotCallable;
      }
      // The first argument was a number or fell through and is treated as
      // a number. https://tc39.github.io/ecma262/#sec-typedarray-length
      case (lengthObj: JSAny): {
        goto IfConstructByLength(lengthObj);
      }
    }
  } label IfConstructByLength(lengthObj: JSAny) {
    return ConstructByLength(target, newTarget, lengthObj);
  } label IfConstructByArrayLike(arrayLike: JSReceiver, length: uintptr) {
    const map = GetDerivedMap(target, newTarget);
    // 5. Let elementSize be the Number value of the Element Size value in Table
    // 56 for constructorName.
    const elementsInfo = GetTypedArrayElementsInfo(map);
    return ConstructByArrayLike(map, arrayLike, length, elementsInfo);
  } label IfIteratorNotCallable(_value: JSAny) deferred {
    ThrowTypeError(
        MessageTemplate::kFirstArgumentIteratorSymbolNonCallable,
        'TypedArray\'s constructor');
  }
}

transitioning macro TypedArraySpeciesCreate(
    implicit context: Context)(methodName: constexpr string,
    numArgs: constexpr int31, exemplar: JSTypedArray, arg0: JSAny, arg1: JSAny,
    arg2: JSAny): JSTypedArray {
  const defaultConstructor = GetDefaultConstructor(exemplar);

  try {
    if (!IsPrototypeTypedArrayPrototype(exemplar.map)) goto IfSlow;
    if (IsTypedArraySpeciesProtectorCellInvalid()) goto IfSlow;

    const typedArray = CreateTypedArray(
        context, defaultConstructor, defaultConstructor, arg0, arg1, arg2);

    // It is assumed that the CreateTypedArray builtin does not produce a
    // typed array that fails ValidateTypedArray
    dcheck(!IsDetachedBuffer(typedArray.buffer));

    return typedArray;
  } label IfSlow deferred {
    const constructor =
        Cast<Constructor>(SpeciesConstructor(exemplar, defaultConstructor))
        otherwise unreachable;

    // TODO(pwong): Simplify and remove numArgs when varargs are supported in
    // macros.
    let newObj: JSAny = Undefined;
    if constexpr (numArgs == 1) {
      newObj = Construct(constructor, arg0);
    } else {
      dcheck(numArgs == 3);
      newObj = Construct(constructor, arg0, arg1, arg2);
    }

    return ValidateTypedArray(context, newObj, methodName);
  }
}

@export
transitioning macro TypedArraySpeciesCreateByLength(
    implicit context: Context)(methodName: constexpr string,
    exemplar: JSTypedArray, length: uintptr): JSTypedArray {
  const numArgs: constexpr int31 = 1;
  // TODO(v8:4153): pass length further as uintptr.
  const typedArray: JSTypedArray = TypedArraySpeciesCreate(
      methodName, numArgs, exemplar, Convert<Number>(length), Undefined,
      Undefined);
  try {
    const createdArrayLength =
        LoadJSTypedArrayLengthAndCheckDetached(typedArray)
        otherwise DetachedOrOutOfBounds;
    if (createdArrayLength < length) deferred {
        ThrowTypeError(MessageTemplate::kTypedArrayTooShort);
      }
  } label DetachedOrOutOfBounds {
    ThrowTypeError(MessageTemplate::kTypedArrayTooShort);
  }
  return typedArray;
}

transitioning macro TypedArraySpeciesCreateByBuffer(
    implicit context: Context)(methodName: constexpr string,
    exemplar: JSTypedArray, buffer: JSArrayBuffer, beginByteOffset: uintptr,
    newLength: NumberOrUndefined): JSTypedArray {
  const numArgs: constexpr int31 = 3;
  // TODO(v8:4153): pass length further as uintptr.
  const typedArray: JSTypedArray = TypedArraySpeciesCreate(
      methodName, numArgs, exemplar, buffer, Convert<Number>(beginByteOffset),
      newLength);
  return typedArray;
}

transitioning macro TypedArrayCreateSameType(
    implicit context: Context)(exemplar: JSTypedArray,
    newLength: uintptr): JSTypedArray {
  const constructor = GetDefaultConstructor(exemplar);
  const typedArray = CreateTypedArray(
      context, constructor, constructor, Convert<Number>(newLength), Undefined,
      Undefined);
  dcheck(!IsDetachedBuffer(typedArray.buffer));
  // The elements kind are equal up to resizability / growability. The newly
  // created TypedArray is never backed by a resizable / growable buffer.
  dcheck(
      GetNonRabGsabElementsKind(exemplar.elements_kind) ==
      typedArray.elements_kind);
  return typedArray;
}
}