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

namespace regexp {

  extern builtin
  SubString(implicit context: Context)(String, Smi, Smi): String;

  extern runtime RegExpExecMultiple(implicit context: Context)(
      JSRegExp, String, RegExpMatchInfo, JSArray): Null|JSArray;
  extern transitioning runtime
  RegExpReplaceRT(Context, JSReceiver, String, Object): String;
  extern transitioning runtime
  StringBuilderConcat(implicit context: Context)(JSArray, Smi, String): String;
  extern transitioning runtime
  StringReplaceNonGlobalRegExpWithFunction(implicit context: Context)(
      String, JSRegExp, Callable): String;

  transitioning macro RegExpReplaceCallableNoExplicitCaptures(implicit context:
                                                                  Context)(
      matchesElements: FixedArray, matchesLength: intptr, string: String,
      replaceFn: Callable) {
    let matchStart: Smi = 0;
    for (let i: intptr = 0; i < matchesLength; i++) {
      typeswitch (matchesElements.objects[i]) {
        // Element represents a slice.
        case (elSmi: Smi): {
          // The slice's match start and end is either encoded as one or two
          // smis. A positive smi indicates a single smi encoding (see
          // ReplacementStringBuilder::AddSubjectSlice()).
          if (elSmi > 0) {
            // For single smi encoding, see
            // StringBuilderSubstringLength::encode() and
            // StringBuilderSubstringPosition::encode().
            const elInt: intptr = Convert<intptr>(elSmi);
            const newMatchStart: intptr = (elInt >> 11) + (elInt & 0x7FF);
            matchStart = Convert<Smi>(newMatchStart);
          } else {
            // For two smi encoding, the length is negative followed by the
            // match start.
            const nextEl: Smi = UnsafeCast<Smi>(matchesElements.objects[++i]);
            matchStart = nextEl - elSmi;
          }
        }
        // Element represents the matched substring, which is then passed to the
        // replace function.
        case (elString: String): {
          const replacementObj: JSAny =
              Call(context, replaceFn, Undefined, elString, matchStart, string);
          const replacement: String = ToString_Inline(replacementObj);
          matchesElements.objects[i] = replacement;
          matchStart += elString.length_smi;
        }
        case (Object): deferred {
          unreachable;
        }
      }
    }
  }

  transitioning macro
  RegExpReplaceCallableWithExplicitCaptures(implicit context: Context)(
      matchesElements: FixedArray, matchesLength: intptr, replaceFn: Callable) {
    for (let i: intptr = 0; i < matchesLength; i++) {
      const elArray =
          Cast<JSArray>(matchesElements.objects[i]) otherwise continue;

      // The JSArray is expanded into the function args by Reflect.apply().
      // TODO(jgruber): Remove indirection through Call->ReflectApply.
      const replacementObj: JSAny = Call(
          context, GetReflectApply(), Undefined, replaceFn, Undefined, elArray);

      // Overwrite the i'th element in the results with the string
      // we got back from the callback function.
      matchesElements.objects[i] = ToString_Inline(replacementObj);
    }
  }

  transitioning macro RegExpReplaceFastGlobalCallable(implicit context:
                                                          Context)(
      regexp: FastJSRegExp, string: String, replaceFn: Callable): String {
    regexp.lastIndex = 0;

    const kInitialCapacity: intptr = 16;
    const kInitialLength: Smi = 0;
    const result: Null|JSArray = RegExpExecMultiple(
        regexp, string, GetRegExpLastMatchInfo(),
        AllocateJSArray(
            ElementsKind::PACKED_ELEMENTS, GetFastPackedElementsJSArrayMap(),
            kInitialCapacity, kInitialLength));

    regexp.lastIndex = 0;

    // If no matches, return the subject string.
    if (result == Null) return string;

    const matches: JSArray = UnsafeCast<JSArray>(result);
    const matchesLength: Smi = Cast<Smi>(matches.length) otherwise unreachable;
    const matchesLengthInt: intptr = Convert<intptr>(matchesLength);
    const matchesElements: FixedArray =
        UnsafeCast<FixedArray>(matches.elements);

    // Reload last match info since it might have changed.
    const nofCaptures: Smi = GetRegExpLastMatchInfo().NumberOfCaptures();

    // If the number of captures is two then there are no explicit captures in
    // the regexp, just the implicit capture that captures the whole match. In
    // this case we can simplify quite a bit and end up with something faster.
    if (nofCaptures == 2) {
      RegExpReplaceCallableNoExplicitCaptures(
          matchesElements, matchesLengthInt, string, replaceFn);
    } else {
      RegExpReplaceCallableWithExplicitCaptures(
          matchesElements, matchesLengthInt, replaceFn);
    }

    return StringBuilderConcat(matches, matchesLength, string);
  }

  transitioning macro RegExpReplaceFastString(implicit context: Context)(
      regexp: JSRegExp, string: String, replaceString: String): String {
    // The fast path is reached only if {receiver} is an unmodified JSRegExp
    // instance, {replace_value} is non-callable, and ToString({replace_value})
    // does not contain '$', i.e. we're doing a simple string replacement.
    let result: String = kEmptyString;
    let lastMatchEnd: Smi = 0;
    let unicode: bool = false;
    const replaceLength: Smi = replaceString.length_smi;
    const fastRegexp = UnsafeCast<FastJSRegExp>(regexp);
    const global: bool = fastRegexp.global;

    if (global) {
      unicode = fastRegexp.unicode;
      fastRegexp.lastIndex = 0;
    }

    while (true) {
      const match: RegExpMatchInfo =
          RegExpPrototypeExecBodyWithoutResultFast(regexp, string)
          otherwise break;
      const matchStart: Smi = match.GetStartOfCapture(0);
      const matchEnd: Smi = match.GetEndOfCapture(0);

      // TODO(jgruber): We could skip many of the checks that using SubString
      // here entails.
      result = result + SubString(string, lastMatchEnd, matchStart);
      lastMatchEnd = matchEnd;

      if (replaceLength != 0) result = result + replaceString;

      // Non-global case ends here after the first replacement.
      if (!global) break;

      // If match is the empty string, we have to increment lastIndex.
      if (matchEnd == matchStart) {
        typeswitch (regexp) {
          case (fastRegexp: FastJSRegExp): {
            fastRegexp.lastIndex =
                AdvanceStringIndexFast(string, fastRegexp.lastIndex, unicode);
          }
          case (Object): {
            const lastIndex: JSAny = SlowLoadLastIndex(regexp);
            const thisIndex: Number = ToLength_Inline(lastIndex);
            const nextIndex: Number =
                AdvanceStringIndexSlow(string, thisIndex, unicode);
            SlowStoreLastIndex(regexp, nextIndex);
          }
        }
      }
    }

    return result + SubString(string, lastMatchEnd, string.length_smi);
  }

  transitioning builtin RegExpReplace(implicit context: Context)(
      regexp: FastJSRegExp, string: String, replaceValue: JSAny): String {
    // TODO(pwong): Remove assert when all callers (StringPrototypeReplace) are
    // from Torque.
    assert(Is<FastJSRegExp>(regexp));

    // 2. Is {replace_value} callable?
    typeswitch (replaceValue) {
      case (replaceFn: Callable): {
        return regexp.global ?
            RegExpReplaceFastGlobalCallable(regexp, string, replaceFn) :
            StringReplaceNonGlobalRegExpWithFunction(string, regexp, replaceFn);
      }
      case (JSAny): {
        const stableRegexp: JSRegExp = regexp;
        const replaceString: String = ToString_Inline(replaceValue);

        try {
          // ToString(replaceValue) could potentially change the shape of the
          // RegExp object. Recheck that we are still on the fast path and bail
          // to runtime otherwise.
          const fastRegexp = Cast<FastJSRegExp>(stableRegexp) otherwise Runtime;
          if (StringIndexOf(
                  replaceString, SingleCharacterStringConstant('$'), 0) != -1) {
            goto Runtime;
          }

          return RegExpReplaceFastString(fastRegexp, string, replaceString);
        }
        label Runtime deferred {
          return RegExpReplaceRT(context, stableRegexp, string, replaceString);
        }
      }
    }
  }

  const kRegExpReplaceCalledOnSlowRegExp: constexpr int31
  generates 'v8::Isolate::kRegExpReplaceCalledOnSlowRegExp';

  transitioning javascript builtin RegExpPrototypeReplace(
      js-implicit context: NativeContext,
      receiver: JSAny)(...arguments): JSAny {
    const methodName: constexpr string = 'RegExp.prototype.@@replace';

    // RegExpPrototypeReplace is a bit of a beast - a summary of dispatch logic:
    //
    // if (!IsFastRegExp(receiver)) CallRuntime(RegExpReplace)
    // if (IsCallable(replace)) {
    //   if (IsGlobal(receiver)) {
    //     // Called 'fast-path' but contains several runtime calls.
    //     RegExpReplaceFastGlobalCallable()
    //   } else {
    //     CallRuntime(StringReplaceNonGlobalRegExpWithFunction)
    //   }
    // } else {
    //   if (replace.contains("$")) {
    //     CallRuntime(RegExpReplace)
    //   } else {
    //     RegExpReplaceFastString()
    //   }
    // }

    const string: JSAny = arguments[0];
    const replaceValue: JSAny = arguments[1];

    // Let rx be the this value.
    // If Type(rx) is not Object, throw a TypeError exception.
    const rx = Cast<JSReceiver>(receiver)
        otherwise ThrowTypeError(
        MessageTemplate::kIncompatibleMethodReceiver, methodName);

    // Let S be ? ToString(string).
    const s = ToString_Inline(string);

    // Fast-path checks: 1. Is the {receiver} an unmodified JSRegExp instance?
    try {
      const fastRx: FastJSRegExp = Cast<FastJSRegExp>(rx) otherwise Runtime;
      return RegExpReplace(fastRx, s, replaceValue);
    }
    label Runtime deferred {
      IncrementUseCounter(
          context, SmiConstant(kRegExpReplaceCalledOnSlowRegExp));
      return RegExpReplaceRT(context, rx, s, replaceValue);
    }
  }

}