// Copyright 2018 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.

module array {
  // Naming convention from elements.cc. We have a similar intent but implement
  // fastpaths using generics instead of using a class hierarchy for elements
  // kinds specific implementations.
  type GenericElementsAccessor;
  type FastPackedSmiElements;
  type FastPackedObjectElements;
  type FastPackedDoubleElements;
  type FastSmiOrObjectElements;
  type FastDoubleElements;
  type DictionaryElements;

  macro GetLengthProperty(context: Context, o: Object): Number {
    if (BranchIfFastJSArray(o, context)) {
      let a: JSArray = unsafe_cast<JSArray>(o);
      return a.length_fast;
    } else
      deferred {
        return ToLength_Inline(context, GetProperty(context, o, 'length'));
      }
  }

  macro FastArraySplice(
      context: Context, args: constexpr Arguments, o: Object,
      originalLengthNumber: Number, actualStartNumber: Number, insertCount: Smi,
      actualDeleteCountNumber: Number): Object
  labels Bailout {
    let originalLength: Smi = cast<Smi>(originalLengthNumber) otherwise Bailout;
    let actualStart: Smi = cast<Smi>(actualStartNumber) otherwise Bailout;
    let actualDeleteCount: Smi =
        cast<Smi>(actualDeleteCountNumber) otherwise Bailout;
    let lengthDelta: Smi = insertCount - actualDeleteCount;
    let newLength: Smi = originalLength + lengthDelta;

    let a: JSArray = cast<JSArray>(o) otherwise Bailout;

    let map: Map = a.map;
    if (!IsPrototypeInitialArrayPrototype(context, map)) goto Bailout;
    if (IsNoElementsProtectorCellInvalid()) goto Bailout;
    if (IsArraySpeciesProtectorCellInvalid()) goto Bailout;

    // Fast path only works on fast elements kind and with writable length.
    let elementsKind: ElementsKind = EnsureArrayPushable(map) otherwise Bailout;
    if (!IsFastElementsKind(elementsKind)) goto Bailout;

    // For now, only support non-double fast elements
    if (!IsFastSmiOrTaggedElementsKind(elementsKind)) goto Bailout;

    if (IsFastSmiElementsKind(elementsKind)) {
      for (let e: Object of args [2: ]) {
        if (TaggedIsNotSmi(e)) goto Bailout;
      }
    }

    // Make sure that the length hasn't been changed by side-effect.
    let length: Smi = cast<Smi>(a.length) otherwise Bailout;
    if (originalLength != length) goto Bailout;

    let deletedResult: JSArray =
        ExtractFastJSArray(context, a, actualStart, actualDeleteCount);

    if (newLength == 0) {
      a.elements = kEmptyFixedArray;
      a.length = 0;
      return deletedResult;
    }

    let elements: FixedArray = cast<FixedArray>(a.elements) otherwise Bailout;
    let elementsMap: Map = elements.map;

    // If the source is a COW array or the spliced array is larger then the
    // source array, then allocate a new FixedArray to hold the result.
    let newElements: FixedArray = elements;
    if ((elementsMap == kCOWMap) || (lengthDelta > 0)) {
      newElements = ExtractFixedArray(
          elements, 0, actualStart, newLength, kAllFixedArrays);
      newElements.map = elementsMap;
      a.elements = newElements;
    }

    // Double check that the array is still in fast elements mode
    assert(IsFastSmiElementsKind(a.map.elements_kind));

    // Copy over inserted elements.
    let k: Smi = actualStart;
    if (insertCount > 0) {
      for (let e: Object of args [2: ]) {
        newElements[k++] = e;
      }
    }

    // Copy over elements after deleted elements.
    let count: Smi = length - actualStart - actualDeleteCount;
    while (count > 0) {
      let e: Object = elements[k - lengthDelta];
      newElements[k++] = e;
      count--;
    }

    // Fill rest of spliced FixedArray with the hole, but only if the
    // destination FixedArray is the original array's, since otherwise the array
    // is pre-filled with holes.
    if (elements == newElements) {
      let limit: Smi = elements.length;
      while (k < limit) {
        newElements[k++] = Hole;
      }
    }

    // Update the array's length after all the FixedArray shuffling is done.
    a.length = newLength;

    return deletedResult;
  }

  // https://tc39.github.io/ecma262/#sec-array.prototype.splice
  javascript builtin ArraySpliceTorque(
      context: Context, receiver: Object, ...arguments): Object {
    // 1. Let O be ? ToObject(this value).
    let o: JSReceiver = ToObject(context, receiver);

    // 2. Let len be ? ToLength(? Get(O, "length")).
    let len: Number = GetLengthProperty(context, o);

    // 3. Let relativeStart be ? ToInteger(start).
    let start: Object = arguments[0];
    let relativeStart: Number = ToInteger_Inline(context, start);

    // 4. If relativeStart < 0, let actualStart be max((len + relativeStart),
    // 0);
    //    else let actualStart be min(relativeStart, len).
    let actualStart: Number = relativeStart < 0 ?
        max((len + relativeStart), 0) :
        min(relativeStart, len);

    let insertCount: Smi;
    let actualDeleteCount: Number;
    // 5. If the Number of actual arguments is 0, then
    if (arguments.length == 0) {
      // a. Let insertCount be 0.
      insertCount = 0;
      // b. Let actualDeleteCount be 0.
      actualDeleteCount = 0;
      // 6. Else if the Number of actual arguments is 1, then
    } else if (arguments.length == 1) {
      // a. Let insertCount be 0.
      insertCount = 0;
      // b. Let actualDeleteCount be len - actualStart.
      actualDeleteCount = len - actualStart;
      // 7. Else,
    } else {
      // a. Let insertCount be the Number of actual arguments minus 2.
      insertCount = convert<Smi>(arguments.length) - 2;
      // b. Let dc be ? ToInteger(deleteCount).
      let deleteCount: Object = arguments[1];
      let dc: Number = ToInteger_Inline(context, deleteCount);
      // c. Let actualDeleteCount be min(max(dc, 0), len - actualStart).
      actualDeleteCount = min(max(dc, 0), len - actualStart);
    }

    // 8. If len + insertCount - actualDeleteCount > 2^53-1, throw a
    //    Bailout exception.
    if (len + insertCount - actualDeleteCount > kMaxSafeInteger) {
      ThrowRangeError(context, kInvalidArrayLength);
    }

    try {
      return FastArraySplice(
          context, arguments, o, len, actualStart, insertCount,
          actualDeleteCount) otherwise Bailout;
    }
    label Bailout {}
    // If the fast case fails, just continue with the slow, correct,
    // spec-compliant case.

    // 9. Let A be ? ArraySpeciesCreate(O, actualDeleteCount).
    let a: Object = ArraySpeciesCreate(context, o, actualDeleteCount);

    // 10. Let k be 0.
    let k: Number = 0;

    // 11. Repeat, while k < actualDeleteCount
    while (k < actualDeleteCount) {
      // a. Let from be ! ToString(actualStart + k).
      let from: String = ToString_Inline(context, actualStart + k);

      // b. Let fromPresent be ? HasProperty(O, from).
      let fromPresent: Oddball = HasProperty(context, o, from);

      // c. If fromPresent is true, then
      if (fromPresent == True) {
        // i. Let fromValue be ? Get(O, from).
        let fromValue: Object = GetProperty(context, o, from);

        // ii. Perform ? CreateDataPropertyOrThrow(A, ! ToString(k), fromValue).
        CreateDataProperty(context, a, ToString_Inline(context, k), fromValue);
      }

      // d. Increment k by 1.
      k = k + 1;
    }

    // 12. Perform ? Set(A, "length", actualDeleteCount, true).
    SetProperty(context, a, 'length', actualDeleteCount);

    // 13. Let items be a List whose elements are, in left-to-right order,
    //     the portion of the actual argument list starting with the third
    //     argument. The list is empty if fewer than three arguments were
    //     passed.
    // 14. Let itemCount be the Number of elements in items.
    let itemCount: Number = insertCount;

    // 15. If itemCount < actualDeleteCount, then
    if (itemCount < actualDeleteCount) {
      // a. Let k be actualStart.
      let k: Number = actualStart;

      // b. Repeat, while k < (len - actualDeleteCount)
      while (k < (len - actualDeleteCount)) {
        // i. Let from be ! ToString(k + actualDeleteCount).
        let from: String = ToString_Inline(context, k + actualDeleteCount);
        // ii. Let to be ! ToString(k + itemCount).
        let to: String = ToString_Inline(context, k + itemCount);

        // iii. Let fromPresent be ? HasProperty(O, from).
        let fromPresent: Oddball = HasProperty(context, o, from);

        // iv. If fromPresent is true, then
        if (fromPresent == True) {
          // 1. Let fromValue be ? Get(O, from).
          let fromValue: Object = GetProperty(context, o, from);

          // 2. Perform ? Set(O, to, fromValue, true).
          SetProperty(context, o, to, fromValue);

          // v. Else fromPresent is false,
        } else {
          // 1. Perform ? DeletePropertyOrThrow(O, to).
          DeleteProperty(context, o, to, kStrict);
        }
        // vi. Increase k by 1.
        k = k + 1;
      }

      // c. Let k be len.
      k = len;
      // d. Repeat, while k > (len - actualDeleteCount + itemCount)
      while (k > (len - actualDeleteCount + itemCount)) {
        // i. Perform ? DeletePropertyOrThrow(O, ! ToString(k - 1)).
        DeleteProperty(context, o, ToString_Inline(context, k - 1), kStrict);

        // ii. Decrease k by 1.
        k = k - 1;
      }
      // 16. Else if itemCount > actualDeleteCount, then
    } else if (itemCount > actualDeleteCount) {
      // a. Let k be (len - actualDeleteCount).
      let k: Number = len - actualDeleteCount;

      // b. Repeat, while k > actualStart
      while (k > actualStart) {
        // i. Let from be ! ToString(k + actualDeleteCount - 1).
        let from: String = ToString_Inline(context, k + actualDeleteCount - 1);

        // ii. Let to be ! ToString(k + itemCount - 1).
        let to: String = ToString_Inline(context, k + itemCount - 1);

        // iii. Let fromPresent be ? HasProperty(O, from).
        let fromPresent: Oddball = HasProperty(context, o, from);

        // iv. If fromPresent is true, then
        if (fromPresent == True) {
          // 1. Let fromValue be ? Get(O, from).
          let fromValue: Object = GetProperty(context, o, from);

          // 2. Perform ? Set(O, to, fromValue, true).
          SetProperty(context, o, to, fromValue);

          // v. Else fromPresent is false,
        } else {
          // 1. Perform ? DeletePropertyOrThrow(O, to).
          DeleteProperty(context, o, to, kStrict);
        }

        // vi. Decrease k by 1.
        k = k - 1;
      }
    }

    // 17. Let k be actualStart.
    k = actualStart;

    // 18. Repeat, while items is not empty
    //   a. Remove the first element from items and let E be the value of that
    //   element.
    if (arguments.length > 2) {
      for (let e: Object of arguments [2: ]) {
        // b. Perform ? Set(O, ! ToString(k), E, true).
        SetProperty(context, o, ToString_Inline(context, k), e);

        // c. Increase k by 1.
        k = k + 1;
      }
    }

    // 19. Perform ? Set(O, "length", len - actualDeleteCount + itemCount,
    // true).
    SetProperty(context, o, 'length', len - actualDeleteCount + itemCount);

    return a;
  }
}