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

namespace array_map {
  transitioning javascript builtin
  ArrayMapLoopEagerDeoptContinuation(
      js-implicit context: Context, receiver: Object)(
      callback: Object, thisArg: Object, array: Object, initialK: Object,
      length: Object): Object {
    // All continuation points in the optimized filter implementation are
    // after the ToObject(O) call that ensures we are dealing with a
    // JSReceiver.
    //
    // Also, this great mass of casts is necessary because the signature
    // of Torque javascript builtins requires Object type for all parameters
    // other than {context}.
    const jsreceiver = Cast<JSReceiver>(receiver) otherwise unreachable;
    const callbackfn = Cast<Callable>(callback) otherwise unreachable;
    const outputArray = Cast<JSReceiver>(array) otherwise unreachable;
    const numberK = Cast<Number>(initialK) otherwise unreachable;
    const numberLength = Cast<Number>(length) otherwise unreachable;

    return ArrayMapLoopContinuation(
        jsreceiver, callbackfn, thisArg, outputArray, jsreceiver, numberK,
        numberLength);
  }

  transitioning javascript builtin
  ArrayMapLoopLazyDeoptContinuation(
      js-implicit context: Context, receiver: Object)(
      callback: Object, thisArg: Object, array: Object, initialK: Object,
      length: Object, result: Object): Object {
    // All continuation points in the optimized filter implementation are
    // after the ToObject(O) call that ensures we are dealing with a
    // JSReceiver.
    const jsreceiver = Cast<JSReceiver>(receiver) otherwise unreachable;
    const callbackfn = Cast<Callable>(callback) otherwise unreachable;
    const outputArray = Cast<JSReceiver>(array) otherwise unreachable;
    let numberK = Cast<Number>(initialK) otherwise unreachable;
    const numberLength = Cast<Number>(length) otherwise unreachable;

    // This custom lazy deopt point is right after the callback. map() needs
    // to pick up at the next step, which is setting the callback result in
    // the output array. After incrementing k, we can glide into the loop
    // continuation builtin.

    // iii. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue).
    FastCreateDataProperty(outputArray, numberK, result);

    // 7d. Increase k by 1.
    numberK = numberK + 1;

    return ArrayMapLoopContinuation(
        jsreceiver, callbackfn, thisArg, outputArray, jsreceiver, numberK,
        numberLength);
  }

  transitioning builtin ArrayMapLoopContinuation(implicit context: Context)(
      _receiver: JSReceiver, callbackfn: Callable, thisArg: Object,
      array: JSReceiver, o: JSReceiver, initialK: Number,
      length: Number): Object {
    // 6. Let k be 0.
    // 7. Repeat, while k < len
    for (let k: Number = initialK; k < length; k++) {
      // 7a. Let Pk be ! ToString(k).
      // k is guaranteed to be a positive integer, hence ToString is
      // side-effect free and HasProperty/GetProperty do the conversion inline.

      // 7b. Let kPresent be ? HasProperty(O, Pk).
      const kPresent: Boolean = HasProperty_Inline(o, k);

      // 7c. If kPresent is true, then:
      if (kPresent == True) {
        //  i. Let kValue be ? Get(O, Pk).
        const kValue: Object = GetProperty(o, k);

        // ii. Let mapped_value be ? Call(callbackfn, T, kValue, k, O).
        const mappedValue: Object =
            Call(context, callbackfn, thisArg, kValue, k, o);

        // iii. Perform ? CreateDataPropertyOrThrow(A, Pk, mapped_value).
        FastCreateDataProperty(array, k, mappedValue);
      }

      // 7d. Increase k by 1. (done by the loop).
    }

    // 8. Return A.
    return array;
  }

  struct Vector {
    ReportSkippedElement() {
      this.skippedElements = true;
    }

    CreateJSArray(implicit context: Context)(validLength: Smi): JSArray {
      const length: Smi = this.fixedArray.length;
      assert(validLength <= length);
      let kind: ElementsKind = PACKED_SMI_ELEMENTS;
      if (!this.onlySmis) {
        if (this.onlyNumbers) {
          kind = PACKED_DOUBLE_ELEMENTS;
        } else {
          kind = PACKED_ELEMENTS;
        }
      }

      if (this.skippedElements || validLength < length) {
        // We also need to create a holey output array if we are
        // bailing out of the fast path partway through the array.
        // This is indicated by {validLength} < {length}.
        // Who knows if the bailout condition will continue to fill in
        // every element?
        kind = FastHoleyElementsKind(kind);
      }

      const map: Map = LoadJSArrayElementsMap(kind, LoadNativeContext(context));
      let a: JSArray;

      if (IsDoubleElementsKind(kind)) {
        // We need to allocate and copy.
        // First, initialize the elements field before allocation to prevent
        // heap corruption.
        const elements: FixedDoubleArray = AllocateFixedDoubleArrayWithHoles(
            SmiUntag(length), kAllowLargeObjectAllocation);
        a = NewJSArray(map, this.fixedArray);
        for (let i: Smi = 0; i < validLength; i++) {
          typeswitch (this.fixedArray.objects[i]) {
            case (n: Number): {
              elements.floats[i] = Convert<float64>(n);
            }
            case (h: HeapObject): {
              assert(h == Hole);
            }
          }
        }
        a.elements = elements;
      } else {
        // Simply install the given fixedArray in {vector}.
        a = NewJSArray(map, this.fixedArray);
      }

      // Paranoia. the FixedArray now "belongs" to JSArray {a}.
      this.fixedArray = kEmptyFixedArray;
      return a;
    }

    StoreResult(implicit context: Context)(index: Smi, result: Object) {
      typeswitch (result) {
        case (s: Smi): {
          this.fixedArray.objects[index] = s;
        }
        case (s: HeapNumber): {
          this.onlySmis = false;
          this.fixedArray.objects[index] = s;
        }
        case (s: HeapObject): {
          this.onlySmis = false;
          this.onlyNumbers = false;
          this.fixedArray.objects[index] = s;
        }
      }
    }

    fixedArray: FixedArray;
    onlySmis: bool;         // initially true.
    onlyNumbers: bool;      // initially true.
    skippedElements: bool;  // initially false.
  }

  macro NewVector(implicit context: Context)(length: Smi): Vector {
    const fixedArray = length > 0 ?
        AllocateFixedArrayWithHoles(
            SmiUntag(length), kAllowLargeObjectAllocation) :
        kEmptyFixedArray;
    return Vector{
      fixedArray,
      onlySmis: true,
      onlyNumbers: true,
      skippedElements: false
    };
  }

  transitioning macro FastArrayMap(implicit context: Context)(
      fastO: FastJSArrayForRead, len: Smi, callbackfn: Callable,
      thisArg: Object): JSArray
      labels Bailout(JSArray, Smi) {
    let k: Smi = 0;
    let fastOW = NewFastJSArrayForReadWitness(fastO);
    let vector = NewVector(len);

    // Build a fast loop over the smi array.
    // 7. Repeat, while k < len.
    try {
      for (; k < len; k++) {
        fastOW.Recheck() otherwise goto PrepareBailout(k);

        // Ensure that we haven't walked beyond a possibly updated length.
        if (k >= fastOW.Get().length) goto PrepareBailout(k);

        try {
          const value: Object = fastOW.LoadElementNoHole(k)
              otherwise FoundHole;
          const result: Object =
              Call(context, callbackfn, thisArg, value, k, fastOW.Get());
          vector.StoreResult(k, result);
        }
        label FoundHole {
          // Our output array must necessarily be holey because of holes in
          // the input array.
          vector.ReportSkippedElement();
        }
      }
    }
    label PrepareBailout(k: Smi) deferred {
      // Transform {vector} into a JSArray and bail out.
      goto Bailout(vector.CreateJSArray(k), k);
    }

    return vector.CreateJSArray(len);
  }

  // https://tc39.github.io/ecma262/#sec-array.prototype.map
  transitioning javascript builtin
  ArrayMap(js-implicit context: Context, receiver: Object)(...arguments):
      Object {
    try {
      if (IsNullOrUndefined(receiver)) goto NullOrUndefinedError;

      // 1. Let O be ? ToObject(this value).
      const o: JSReceiver = ToObject_Inline(context, receiver);

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

      // 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
      if (arguments.length == 0) goto TypeError;

      const callbackfn = Cast<Callable>(arguments[0]) otherwise TypeError;

      // 4. If thisArg is present, let T be thisArg; else let T be undefined.
      const thisArg: Object = arguments.length > 1 ? arguments[1] : Undefined;

      let array: JSReceiver;
      let k: Number = 0;
      try {
        // 5. Let A be ? ArraySpeciesCreate(O, len).
        if (IsArraySpeciesProtectorCellInvalid()) goto SlowSpeciesCreate;
        const o: FastJSArrayForRead = Cast<FastJSArrayForRead>(receiver)
            otherwise SlowSpeciesCreate;
        const smiLength: Smi = Cast<Smi>(len)
            otherwise SlowSpeciesCreate;

        return FastArrayMap(o, smiLength, callbackfn, thisArg)
            otherwise Bailout;
      }
      label SlowSpeciesCreate {
        array = ArraySpeciesCreate(context, receiver, len);
      }
      label Bailout(output: JSArray, kValue: Smi) deferred {
        array = output;
        k = kValue;
      }

      return ArrayMapLoopContinuation(o, callbackfn, thisArg, array, o, k, len);
    }
    label TypeError deferred {
      ThrowTypeError(kCalledNonCallable, arguments[0]);
    }
    label NullOrUndefinedError deferred {
      ThrowTypeError(kCalledOnNullOrUndefined, 'Array.prototype.map');
    }
  }
}