// Copyright 2021 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-utils-inl.h"
#include "src/codegen/compiler.h"
#include "src/logging/counters.h"
#include "src/objects/js-shadow-realm-inl.h"

namespace v8 {
namespace internal {

// https://tc39.es/proposal-shadowrealm/#sec-shadowrealm-constructor
BUILTIN(ShadowRealmConstructor) {
  HandleScope scope(isolate);
  // 1. If NewTarget is undefined, throw a TypeError exception.
  if (args.new_target()->IsUndefined(isolate)) {  // [[Call]]
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate, NewTypeError(MessageTemplate::kConstructorNotFunction,
                              isolate->factory()->ShadowRealm_string()));
  }
  // [[Construct]]
  Handle<JSFunction> target = args.target();
  Handle<JSReceiver> new_target = Handle<JSReceiver>::cast(args.new_target());

  // 3. Let realmRec be CreateRealm().
  // 5. Let context be a new execution context.
  // 6. Set the Function of context to null.
  // 7. Set the Realm of context to realmRec.
  // 8. Set the ScriptOrModule of context to null.
  // 10. Perform ? SetRealmGlobalObject(realmRec, undefined, undefined).
  // 11. Perform ? SetDefaultGlobalBindings(O.[[ShadowRealm]]).
  // 12. Perform ? HostInitializeShadowRealm(O.[[ShadowRealm]]).
  // These steps are combined in
  // Isolate::RunHostCreateShadowRealmContextCallback and Context::New.
  // The host operation is hoisted for not creating a half-initialized
  // ShadowRealm object, which can fail the heap verification.
  Handle<NativeContext> native_context;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, native_context,
      isolate->RunHostCreateShadowRealmContextCallback());

  // 2. Let O be ? OrdinaryCreateFromConstructor(NewTarget,
  // "%ShadowRealm.prototype%", « [[ShadowRealm]], [[ExecutionContext]] »).
  Handle<JSObject> result;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, result,
      JSObject::New(target, new_target, Handle<AllocationSite>::null()));
  Handle<JSShadowRealm> O = Handle<JSShadowRealm>::cast(result);

  // 4. Set O.[[ShadowRealm]] to realmRec.
  // 9. Set O.[[ExecutionContext]] to context.
  O->set_native_context(*native_context);

  // 13. Return O.
  return *O;
}

namespace {

// https://tc39.es/proposal-shadowrealm/#sec-getwrappedvalue
MaybeHandle<Object> GetWrappedValue(Isolate* isolate,
                                    Handle<NativeContext> creation_context,
                                    Handle<Object> value) {
  // 1. If Type(value) is Object, then
  if (!value->IsJSReceiver()) {
    // 2. Return value.
    return value;
  }
  // 1a. If IsCallable(value) is false, throw a TypeError exception.
  if (!value->IsCallable()) {
    // The TypeError thrown is created with creation Realm's TypeError
    // constructor instead of the executing Realm's.
    THROW_NEW_ERROR_RETURN_VALUE(
        isolate,
        NewError(Handle<JSFunction>(creation_context->type_error_function(),
                                    isolate),
                 MessageTemplate::kNotCallable, value),
        {});
  }
  // 1b. Return ? WrappedFunctionCreate(callerRealm, value).
  return JSWrappedFunction::Create(isolate, creation_context,
                                   Handle<JSReceiver>::cast(value));
}

}  // namespace

// https://tc39.es/proposal-shadowrealm/#sec-shadowrealm.prototype.evaluate
BUILTIN(ShadowRealmPrototypeEvaluate) {
  HandleScope scope(isolate);

  Handle<Object> source_text = args.atOrUndefined(isolate, 1);
  // 1. Let O be this value.
  Handle<Object> receiver = args.receiver();

  Factory* factory = isolate->factory();

  // 2. Perform ? ValidateShadowRealmObject(O).
  if (!receiver->IsJSShadowRealm()) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate, NewTypeError(MessageTemplate::kIncompatibleMethodReceiver));
  }
  Handle<JSShadowRealm> shadow_realm = Handle<JSShadowRealm>::cast(receiver);

  // 3. If Type(sourceText) is not String, throw a TypeError exception.
  if (!source_text->IsString()) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate,
        NewTypeError(MessageTemplate::kInvalidShadowRealmEvaluateSourceText));
  }

  // 4. Let callerRealm be the current Realm Record.
  Handle<NativeContext> caller_context = isolate->native_context();

  // 5. Let evalRealm be O.[[ShadowRealm]].
  Handle<NativeContext> eval_context =
      Handle<NativeContext>(shadow_realm->native_context(), isolate);
  // 6. Return ? PerformShadowRealmEval(sourceText, callerRealm, evalRealm).

  // PerformShadowRealmEval
  // https://tc39.es/proposal-shadowrealm/#sec-performshadowrealmeval
  // 1. Perform ? HostEnsureCanCompileStrings(callerRealm, evalRealm).
  // Run embedder pre-checks before executing the source code.
  MaybeHandle<String> validated_source;
  bool unhandled_object;
  std::tie(validated_source, unhandled_object) =
      Compiler::ValidateDynamicCompilationSource(isolate, eval_context,
                                                 source_text);
  if (unhandled_object) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate,
        NewTypeError(MessageTemplate::kInvalidShadowRealmEvaluateSourceText));
  }

  Handle<JSObject> eval_global_proxy(eval_context->global_proxy(), isolate);
  MaybeHandle<Object> result;
  bool is_parse_failed = false;
  {
    // 8. If runningContext is not already suspended, suspend runningContext.
    // 9. Let evalContext be a new ECMAScript code execution context.
    // 10. Set evalContext's Function to null.
    // 11. Set evalContext's Realm to evalRealm.
    // 12. Set evalContext's ScriptOrModule to null.
    // 13. Set evalContext's VariableEnvironment to varEnv.
    // 14. Set evalContext's LexicalEnvironment to lexEnv.
    // 15. Push evalContext onto the execution context stack; evalContext is now
    // the running execution context.
    SaveAndSwitchContext save(isolate, *eval_context);

    // 2. Perform the following substeps in an implementation-defined order,
    // possibly interleaving parsing and error detection:
    // 2a. Let script be ParseText(! StringToCodePoints(sourceText), Script).
    // 2b. If script is a List of errors, throw a SyntaxError exception.
    // 2c. If script Contains ScriptBody is false, return undefined.
    // 2d. Let body be the ScriptBody of script.
    // 2e. If body Contains NewTarget is true, throw a SyntaxError
    // exception.
    // 2f. If body Contains SuperProperty is true, throw a SyntaxError
    // exception.
    // 2g. If body Contains SuperCall is true, throw a SyntaxError exception.
    // 3. Let strictEval be IsStrict of script.
    // 4. Let runningContext be the running execution context.
    // 5. Let lexEnv be NewDeclarativeEnvironment(evalRealm.[[GlobalEnv]]).
    // 6. Let varEnv be evalRealm.[[GlobalEnv]].
    // 7. If strictEval is true, set varEnv to lexEnv.
    Handle<JSFunction> function;
    MaybeHandle<JSFunction> maybe_function =
        Compiler::GetFunctionFromValidatedString(eval_context, validated_source,
                                                 NO_PARSE_RESTRICTION,
                                                 kNoSourcePosition);
    if (maybe_function.is_null()) {
      is_parse_failed = true;
    } else {
      function = maybe_function.ToHandleChecked();

      // 16. Let result be EvalDeclarationInstantiation(body, varEnv,
      // lexEnv, null, strictEval).
      // 17. If result.[[Type]] is normal, then
      // 20a. Set result to the result of evaluating body.
      // 18. If result.[[Type]] is normal and result.[[Value]] is empty, then
      // 21a. Set result to NormalCompletion(undefined).
      result =
          Execution::Call(isolate, function, eval_global_proxy, 0, nullptr);

      // 19. Suspend evalContext and remove it from the execution context stack.
      // 20. Resume the context that is now on the top of the execution context
      // stack as the running execution context. Done by the scope.
    }
  }

  if (result.is_null()) {
    DCHECK(isolate->has_pending_exception());
    Handle<Object> pending_exception =
        Handle<Object>(isolate->pending_exception(), isolate);
    isolate->clear_pending_exception();
    if (is_parse_failed) {
      Handle<JSObject> error_object = Handle<JSObject>::cast(pending_exception);
      Handle<String> message = Handle<String>::cast(JSReceiver::GetDataProperty(
          isolate, error_object, factory->message_string()));

      return isolate->ReThrow(
          *factory->NewError(isolate->syntax_error_function(), message));
    }
    // 21. If result.[[Type]] is not normal, throw a TypeError exception.
    Handle<String> string =
        Object::NoSideEffectsToString(isolate, pending_exception);
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate,
        NewTypeError(MessageTemplate::kCallShadowRealmEvaluateThrew, string));
  }
  // 22. Return ? GetWrappedValue(callerRealm, result.[[Value]]).
  Handle<Object> wrapped_result;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, wrapped_result,
      GetWrappedValue(isolate, caller_context, result.ToHandleChecked()));
  return *wrapped_result;
}

}  // namespace internal
}  // namespace v8
