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

extern macro ReplaceSymbolConstant(): Symbol;

extern macro StringBuiltinsAssembler::GetSubstitution(
    implicit context: Context)(String, Smi, Smi, String): String;

transitioning macro
ThrowIfNotGlobal(implicit context: Context)(searchValue: JSAny): void {
  let shouldThrow: bool;
  typeswitch (searchValue) {
    case (fastRegExp: FastJSRegExp): {
      shouldThrow = !fastRegExp.global;
    }
    case (Object): {
      const flags = GetProperty(searchValue, 'flags');
      RequireObjectCoercible(flags, 'String.prototype.replaceAll');
      shouldThrow =
          StringIndexOf(ToString_Inline(flags), StringConstant('g'), 0) == -1;
    }
  }
  if (shouldThrow) {
    ThrowTypeError(
        MessageTemplate::kRegExpGlobalInvokedOnNonGlobal,
        'String.prototype.replaceAll');
  }
}

// https://tc39.es/ecma262/#sec-string.prototype.replaceall
transitioning javascript builtin StringPrototypeReplaceAll(
    js-implicit context: NativeContext, receiver: JSAny)(
    searchValue: JSAny, replaceValue: JSAny): JSAny {
  // 1. Let O be ? RequireObjectCoercible(this value).
  RequireObjectCoercible(receiver, 'String.prototype.replaceAll');

  // 2. If searchValue is neither undefined nor null, then
  if (searchValue != Undefined && searchValue != Null) {
    // a. Let isRegExp be ? IsRegExp(searchString).
    // b. If isRegExp is true, then
    //   i. Let flags be ? Get(searchValue, "flags").
    //  ii. Perform ? RequireObjectCoercible(flags).
    // iii. If ? ToString(flags) does not contain "g", throw a
    //      TypeError exception.
    if (regexp::IsRegExp(searchValue)) {
      ThrowIfNotGlobal(searchValue);
    }

    // TODO(joshualitt): We could easily add fast paths for string
    //                   searchValues and potential FastRegExps.
    // c. Let replacer be ? GetMethod(searchValue, @@replace).
    // d. If replacer is not undefined, then
    //   i. Return ? Call(replacer, searchValue, « O, replaceValue »).
    try {
      const replacer = GetMethod(searchValue, ReplaceSymbolConstant())
          otherwise ReplaceSymbolIsNullOrUndefined;
      return Call(context, replacer, searchValue, receiver, replaceValue);
    } label ReplaceSymbolIsNullOrUndefined {}
  }

  // 3. Let string be ? ToString(O).
  const string = ToString_Inline(receiver);

  // 4. Let searchString be ? ToString(searchValue).
  const searchString = ToString_Inline(searchValue);

  // 5. Let functionalReplace be IsCallable(replaceValue).
  let replaceValueArg = replaceValue;
  const functionalReplace = Is<Callable>(replaceValue);

  // 6. If functionalReplace is false, then
  if (!functionalReplace) {
    // a. Let replaceValue be ? ToString(replaceValue).
    replaceValueArg = ToString_Inline(replaceValue);
  }

  // 7. Let searchLength be the length of searchString.
  const searchLength = searchString.length_smi;

  // 8. Let advanceBy be max(1, searchLength).
  const advanceBy = SmiMax(1, searchLength);

  // We combine the two loops from the spec into one to avoid
  // needing a growable array.
  //
  // 9. Let matchPositions be a new empty List.
  // 10. Let position be ! StringIndexOf(string, searchString, 0).
  // 11. Repeat, while position is not -1
  //   a. Append position to the end of matchPositions.
  //   b. Let position be ! StringIndexOf(string, searchString,
  //                                      position + advanceBy).
  // 12. Let endOfLastMatch be 0.
  // 13. Let result be the empty string value.
  // 14. For each position in matchPositions, do
  let endOfLastMatch: Smi = 0;
  let result: String = kEmptyString;
  let position = AbstractStringIndexOf(string, searchString, 0);
  while (position != -1) {
    // a. If functionalReplace is true, then
    // b. Else,
    let replacement: String;
    if (functionalReplace) {
      // i. Let replacement be ? ToString(? Call(replaceValue, undefined,
      //                                         « searchString, position,
      //                                           string »).
      replacement = ToString_Inline(Call(
          context, UnsafeCast<Callable>(replaceValueArg), Undefined,
          searchString, position, string));
    } else {
      // i. Assert: Type(replaceValue) is String.
      const replaceValueString = UnsafeCast<String>(replaceValueArg);

      // ii. Let captures be a new empty List.
      // iii. Let replacement be GetSubstitution(searchString,
      //                                         string, position, captures,
      //                                         undefined, replaceValue).
      // Note: Instead we just call a simpler GetSubstitution primitive.
      const matchEndPosition = position + searchLength;
      replacement = GetSubstitution(
          string, position, matchEndPosition, replaceValueString);
    }

    // c. Let stringSlice be the substring of string consisting of the code
    //    units from endOfLastMatch (inclusive) up through position
    //    (exclusive).
    const stringSlice = string::SubString(
        string, Unsigned(SmiUntag(endOfLastMatch)),
        Unsigned(SmiUntag(position)));

    // d. Let result be the string-concatenation of result, stringSlice,
    //    and replacement.
    // TODO(joshualitt): This leaves a completely degenerate ConsString tree.
    //                   We could be smarter here.
    result = result + stringSlice + replacement;

    // e. Let endOfLastMatch be position + searchLength.
    endOfLastMatch = position + searchLength;

    position =
        AbstractStringIndexOf(string, searchString, position + advanceBy);
  }

  // 15. If endOfLastMatch < the length of string, then
  if (endOfLastMatch < string.length_smi) {
    // a. Let result be the string-concatenation of result and the substring
    //    of string consisting of the code units from endOfLastMatch
    //    (inclusive) up through the final code unit of string (inclusive).
    result = result +
        string::SubString(
                 string, Unsigned(SmiUntag(endOfLastMatch)),
                 Unsigned(string.length_intptr));
  }

  // 16. Return result.
  return result;
}