// Copyright 2024 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/builtins/builtins.h"
#include "src/common/globals.h"
#include "src/handles/maybe-handles.h"
#include "src/objects/casting.h"
#include "src/objects/contexts.h"
#include "src/objects/heap-object.h"
#include "src/objects/js-disposable-stack-inl.h"
#include "src/objects/js-disposable-stack.h"
#include "src/objects/js-function.h"

namespace v8 {
namespace internal {

// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-disposablestack
BUILTIN(DisposableStackConstructor) {
  const char* const kMethodName = "DisposableStack";
  HandleScope scope(isolate);
  isolate->CountUsage(v8::Isolate::kExplicitResourceManagement);

  // 1. If NewTarget is undefined, throw a TypeError exception.
  if (IsUndefined(*args.new_target(), isolate)) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate, NewTypeError(MessageTemplate::kConstructorNotFunction,
                              isolate->factory()->NewStringFromAsciiChecked(
                                  kMethodName)));
  }

  // 2. Let disposableStack be ? OrdinaryCreateFromConstructor(NewTarget,
  //    "%DisposableStack.prototype%", « [[DisposableState]],
  //    [[DisposeCapability]] »).
  DirectHandle<Map> map;
  DirectHandle<JSFunction> target = args.target();
  DirectHandle<JSReceiver> new_target = Cast<JSReceiver>(args.new_target());

  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, map, JSFunction::GetDerivedMap(isolate, target, new_target));

  DirectHandle<JSSyncDisposableStack> disposable_stack =
      isolate->factory()->NewJSSyncDisposableStack(map);
  // 3. Set disposableStack.[[DisposableState]] to pending.
  // 4. Set disposableStack.[[DisposeCapability]] to NewDisposeCapability().
  JSDisposableStackBase::InitializeJSDisposableStackBase(isolate,
                                                         disposable_stack);
  // 5. Return disposableStack.
  return *disposable_stack;
}

// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-disposablestack.prototype.use
BUILTIN(DisposableStackPrototypeUse) {
  const char* const kMethodName = "DisposableStack.prototype.use";
  HandleScope scope(isolate);

  // 1. Let disposableStack be the this value.
  // 2. Perform ? RequireInternalSlot(disposableStack, [[DisposableState]]).
  CHECK_RECEIVER(JSSyncDisposableStack, disposable_stack, kMethodName);
  DirectHandle<JSAny> value = args.at<JSAny>(1);

  // 3. If disposableStack.[[DisposableState]] is disposed, throw a
  //    ReferenceError exception.
  if (disposable_stack->state() == DisposableStackState::kDisposed) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate,
        NewReferenceError(
            MessageTemplate::kDisposableStackIsDisposed,
            isolate->factory()->NewStringFromAsciiChecked(kMethodName)));
  }

  // 4. Perform ? AddDisposableResource(disposableStack.[[DisposeCapability]],
  // value, sync-dispose).

  //    (a. If V is either null or undefined and hint is sync-dispose, then
  //       i. Return unused.)
  if (IsNullOrUndefined(*value)) {
    return *value;
  }

  DirectHandle<Object> method;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, method,
      JSDisposableStackBase::CheckValueAndGetDisposeMethod(
          isolate, value, DisposeMethodHint::kSyncDispose));

  JSDisposableStackBase::Add(isolate, disposable_stack, value, method,
                             DisposeMethodCallType::kValueIsReceiver,
                             DisposeMethodHint::kSyncDispose);

  // 5. Return value.
  return *value;
}

BUILTIN(DisposableStackPrototypeDispose) {
  const char* const kMethodName = "DisposableStack.prototype.dispose";
  HandleScope scope(isolate);

  // 1. Let disposableStack be the this value.
  // 2. Perform ? RequireInternalSlot(disposableStack, [[DisposableState]]).
  CHECK_RECEIVER(JSSyncDisposableStack, disposable_stack, kMethodName);

  // 3. If disposableStack.[[DisposableState]] is disposed, return undefined.
  if (disposable_stack->state() == DisposableStackState::kDisposed) {
    return ReadOnlyRoots(isolate).undefined_value();
  }

  // 4. Set disposableStack.[[DisposableState]] to disposed.
  disposable_stack->set_state(DisposableStackState::kDisposed);

  // 5. Return ? DisposeResources(disposableStack.[[DisposeCapability]],
  //    NormalCompletion(undefined)).
  DirectHandle<Object> result;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, result,
      JSDisposableStackBase::DisposeResources(
          isolate, disposable_stack, DisposableStackResourcesType::kAllSync));
  return ReadOnlyRoots(isolate).undefined_value();
}

BUILTIN(DisposableStackPrototypeGetDisposed) {
  const char* const kMethodName = "get DisposableStack.prototype.disposed";
  HandleScope scope(isolate);

  // 1. Let disposableStack be the this value.
  // 2. Perform ? RequireInternalSlot(disposableStack, [[DisposableState]]).
  CHECK_RECEIVER(JSSyncDisposableStack, disposable_stack, kMethodName);

  // 3. If disposableStack.[[DisposableState]] is disposed, return true.
  if (disposable_stack->state() == DisposableStackState::kDisposed) {
    return ReadOnlyRoots(isolate).true_value();
  }
  // 4. Otherwise, return false.
  return ReadOnlyRoots(isolate).false_value();
}

// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-disposablestack.prototype.adopt
BUILTIN(DisposableStackPrototypeAdopt) {
  const char* const kMethodName = "DisposableStack.prototype.adopt";
  HandleScope scope(isolate);
  DirectHandle<Object> value = args.at(1);
  DirectHandle<Object> on_dispose = args.at(2);

  // 1. Let disposableStack be the this value.
  // 2. Perform ? RequireInternalSlot(disposableStack, [[DisposableState]]).
  CHECK_RECEIVER(JSSyncDisposableStack, disposable_stack, kMethodName);

  // 3. If disposableStack.[[DisposableState]] is disposed, throw a
  //    ReferenceError exception.
  if (disposable_stack->state() == DisposableStackState::kDisposed) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate,
        NewReferenceError(
            MessageTemplate::kDisposableStackIsDisposed,
            isolate->factory()->NewStringFromAsciiChecked(kMethodName)));
  }

  // 4. If IsCallable(onDispose) is false, throw a TypeError exception.
  if (!IsCallable(*on_dispose)) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate, NewTypeError(MessageTemplate::kNotCallable, on_dispose));
  }

  // 5. Let closure be a new Abstract Closure with no parameters that captures
  //    value and onDispose and performs the following steps when called:
  //      a. Return ? Call(onDispose, undefined, « value »).
  // 6. Let F be CreateBuiltinFunction(closure, 0, "", « »).
  // 7. Perform ? AddDisposableResource(disposableStack.[[DisposeCapability]],
  //    undefined, sync-dispose, F).
  // Instead of creating an abstract closure and a function, we pass
  // DisposeMethodCallType::kArgument so at the time of disposal, the value will
  // be passed as the argument to the method.
  JSDisposableStackBase::Add(isolate, disposable_stack, value, on_dispose,
                             DisposeMethodCallType::kValueIsArgument,
                             DisposeMethodHint::kSyncDispose);

  // 8. Return value.
  return *value;
}

// https://arai-a.github.io/ecma262-compare/?pr=3000&id=sec-disposablestack.prototype.defer
BUILTIN(DisposableStackPrototypeDefer) {
  const char* const kMethodName = "DisposableStack.prototype.defer";
  HandleScope scope(isolate);
  DirectHandle<Object> on_dispose = args.at(1);

  // 1. Let disposableStack be the this value.
  // 2. Perform ? RequireInternalSlot(disposableStack, [[DisposableState]]).
  CHECK_RECEIVER(JSSyncDisposableStack, disposable_stack, kMethodName);

  // 3. If disposableStack.[[DisposableState]] is disposed, throw a
  // ReferenceError exception.
  if (disposable_stack->state() == DisposableStackState::kDisposed) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate,
        NewReferenceError(
            MessageTemplate::kDisposableStackIsDisposed,
            isolate->factory()->NewStringFromAsciiChecked(kMethodName)));
  }

  // 4. If IsCallable(onDispose) is false, throw a TypeError exception.
  if (!IsCallable(*on_dispose)) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate, NewTypeError(MessageTemplate::kNotCallable, on_dispose));
  }

  // 5. Perform ? AddDisposableResource(disposableStack.[[DisposeCapability]],
  // undefined, sync-dispose, onDispose).
  JSDisposableStackBase::Add(isolate, disposable_stack,
                             isolate->factory()->undefined_value(), on_dispose,
                             DisposeMethodCallType::kValueIsReceiver,
                             DisposeMethodHint::kSyncDispose);

  // 6. Return undefined.
  return ReadOnlyRoots(isolate).undefined_value();
}

BUILTIN(DisposableStackPrototypeMove) {
  const char* const kMethodName = "DisposableStack.prototype.move";
  HandleScope scope(isolate);

  // 1. Let disposableStack be the this value.
  // 2. Perform ? RequireInternalSlot(disposableStack, [[DisposableState]]).
  CHECK_RECEIVER(JSSyncDisposableStack, disposable_stack, kMethodName);

  // 3. If disposableStack.[[DisposableState]] is disposed, throw a
  //    ReferenceError exception.
  if (disposable_stack->state() == DisposableStackState::kDisposed) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate,
        NewReferenceError(
            MessageTemplate::kDisposableStackIsDisposed,
            isolate->factory()->NewStringFromAsciiChecked(kMethodName)));
  }

  // 4. Let newDisposableStack be ?
  //    OrdinaryCreateFromConstructor(%DisposableStack%,
  //    "%DisposableStack.prototype%", « [[DisposableState]],
  //     [[DisposeCapability]] »).
  // 5. Set newDisposableStack.[[DisposableState]] to pending.

  Tagged<JSFunction> constructor_function =
      Cast<JSFunction>(isolate->native_context()->GetNoCell(
          Context::JS_DISPOSABLE_STACK_FUNCTION_INDEX));
  DirectHandle<Map> map(constructor_function->initial_map(), isolate);

  DirectHandle<JSSyncDisposableStack> new_disposable_stack =
      isolate->factory()->NewJSSyncDisposableStack(map);

  // 6. Set newDisposableStack.[[DisposeCapability]] to
  //    disposableStack.[[DisposeCapability]].
  new_disposable_stack->set_stack(disposable_stack->stack());
  new_disposable_stack->set_length(disposable_stack->length());
  new_disposable_stack->set_state(DisposableStackState::kPending);
  new_disposable_stack->set_error(*(isolate->factory()->uninitialized_value()));
  new_disposable_stack->set_error_message(
      *(isolate->factory()->uninitialized_value()));

  // 7. Set disposableStack.[[DisposeCapability]] to NewDisposeCapability().
  disposable_stack->set_stack(ReadOnlyRoots(isolate).empty_fixed_array());
  disposable_stack->set_length(0);
  disposable_stack->set_error(*(isolate->factory()->uninitialized_value()));
  disposable_stack->set_error_message(
      *(isolate->factory()->uninitialized_value()));

  // 8. Set disposableStack.[[DisposableState]] to disposed.
  disposable_stack->set_state(DisposableStackState::kDisposed);

  // 9. Return newDisposableStack.
  return *new_disposable_stack;
}

}  // namespace internal
}  // namespace v8