// Copyright 2020 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 runtime {
extern runtime
ShrinkFinalizationRegistryUnregisterTokenMap(
    Context, JSFinalizationRegistry): void;
extern runtime JSFinalizationRegistryRegisterWeakCellWithUnregisterToken(
    implicit context: Context)(JSFinalizationRegistry, WeakCell): void;
}

namespace weakref {
extern transitioning macro
RemoveFinalizationRegistryCellFromUnregisterTokenMap(
    JSFinalizationRegistry, WeakCell): void;

macro SplitOffTail(weakCell: WeakCell): WeakCell|Undefined {
  const weakCellTail = weakCell.next;
  weakCell.next = Undefined;
  typeswitch (weakCellTail) {
    case (Undefined): {
    }
    case (tailIsNowAHead: WeakCell): {
      dcheck(tailIsNowAHead.prev == weakCell);
      tailIsNowAHead.prev = Undefined;
    }
  }
  return weakCellTail;
}

transitioning macro
PopClearedCell(finalizationRegistry: JSFinalizationRegistry): WeakCell|
    Undefined {
  typeswitch (finalizationRegistry.cleared_cells) {
    case (Undefined): {
      return Undefined;
    }
    case (weakCell: WeakCell): {
      dcheck(weakCell.prev == Undefined);
      finalizationRegistry.cleared_cells = SplitOffTail(weakCell);

      // If the WeakCell has an unregister token, remove the cell from the
      // unregister token linked lists and and the unregister token from
      // key_map. This doesn't shrink key_map, which is done manually after
      // the cleanup loop to avoid a runtime call.
      if (weakCell.unregister_token != Undefined) {
        RemoveFinalizationRegistryCellFromUnregisterTokenMap(
            finalizationRegistry, weakCell);
      }

      return weakCell;
    }
  }
}

transitioning macro PushCell(
    finalizationRegistry: JSFinalizationRegistry, cell: WeakCell): void {
  cell.next = finalizationRegistry.active_cells;
  typeswitch (finalizationRegistry.active_cells) {
    case (Undefined): {
    }
    case (oldHead: WeakCell): {
      oldHead.prev = cell;
    }
  }
  finalizationRegistry.active_cells = cell;
}

transitioning macro
FinalizationRegistryCleanupLoop(implicit context: Context)(
    finalizationRegistry: JSFinalizationRegistry, callback: Callable): void {
  while (true) {
    const weakCellHead = PopClearedCell(finalizationRegistry);
    typeswitch (weakCellHead) {
      case (Undefined): {
        break;
      }
      case (weakCell: WeakCell): {
        try {
          Call(context, callback, Undefined, weakCell.holdings);
        } catch (e, message) {
          runtime::ShrinkFinalizationRegistryUnregisterTokenMap(
              context, finalizationRegistry);
          ReThrowWithMessage(context, e, message);
        }
      }
    }
  }

  runtime::ShrinkFinalizationRegistryUnregisterTokenMap(
      context, finalizationRegistry);
}

transitioning javascript builtin
FinalizationRegistryConstructor(
    js-implicit context: NativeContext, receiver: JSAny, newTarget: JSAny,
    target: JSFunction)(cleanupCallback: JSAny): JSFinalizationRegistry {
  // 1. If NewTarget is undefined, throw a TypeError exception.
  if (newTarget == Undefined) {
    ThrowTypeError(
        MessageTemplate::kConstructorNotFunction, 'FinalizationRegistry');
  }
  // 2. If IsCallable(cleanupCallback) is false, throw a TypeError exception.
  const cleanupCallback = Cast<Callable>(cleanupCallback) otherwise
  ThrowTypeError(MessageTemplate::kWeakRefsCleanupMustBeCallable);
  // 3. Let finalizationRegistry be ? OrdinaryCreateFromConstructor(NewTarget,
  // "%FinalizationRegistryPrototype%", « [[Realm]], [[CleanupCallback]],
  // [[Cells]] »).
  const map = GetDerivedMap(target, UnsafeCast<JSReceiver>(newTarget));
  const finalizationRegistry = UnsafeCast<JSFinalizationRegistry>(
      AllocateFastOrSlowJSObjectFromMap(map));
  // 4. Let fn be the active function object.
  // 5. Set finalizationRegistry.[[Realm]] to fn.[[Realm]].
  finalizationRegistry.native_context = context;
  // 6. Set finalizationRegistry.[[CleanupCallback]] to cleanupCallback.
  finalizationRegistry.cleanup = cleanupCallback;
  finalizationRegistry.flags =
      SmiTag(FinalizationRegistryFlags{scheduled_for_cleanup: false});
  // 7. Set finalizationRegistry.[[Cells]] to be an empty List.
  dcheck(finalizationRegistry.active_cells == Undefined);
  dcheck(finalizationRegistry.cleared_cells == Undefined);
  dcheck(finalizationRegistry.key_map == Undefined);
  // 8. Return finalizationRegistry.
  return finalizationRegistry;
}

transitioning javascript builtin
FinalizationRegistryRegister(
    js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
  // 1. Let finalizationRegistry be the this value.
  // 2. Perform ? RequireInternalSlot(finalizationRegistry, [[Cells]]).
  const finalizationRegistry = Cast<JSFinalizationRegistry>(receiver) otherwise
  ThrowTypeError(
      MessageTemplate::kIncompatibleMethodReceiver,
      'FinalizationRegistry.prototype.register', receiver);
  // 3. If Type(target) is not Object, throw a TypeError exception.
  const target = Cast<JSReceiver>(arguments[0]) otherwise ThrowTypeError(
      MessageTemplate::kWeakRefsRegisterTargetMustBeObject);
  const heldValue = arguments[1];
  // 4. If SameValue(target, heldValue), throw a TypeError exception.
  if (target == heldValue) {
    ThrowTypeError(
        MessageTemplate::kWeakRefsRegisterTargetAndHoldingsMustNotBeSame);
  }
  // 5. If Type(unregisterToken) is not Object,
  //   a. If unregisterToken is not undefined, throw a TypeError exception.
  //   b. Set unregisterToken to empty.
  const unregisterTokenRaw = arguments[2];
  let unregisterToken: JSReceiver|Undefined;
  typeswitch (unregisterTokenRaw) {
    case (Undefined): {
      unregisterToken = Undefined;
    }
    case (unregisterTokenObj: JSReceiver): {
      unregisterToken = unregisterTokenObj;
    }
    case (JSAny): deferred {
      ThrowTypeError(
          MessageTemplate::kWeakRefsUnregisterTokenMustBeObject,
          unregisterTokenRaw);
    }
  }
  // 6. Let cell be the Record { [[WeakRefTarget]] : target, [[HeldValue]]:
  //    heldValue, [[UnregisterToken]]: unregisterToken }.
  // Allocate the WeakCell object in the old space, because 1) WeakCell weakness
  // handling is only implemented in the old space 2) they're supposedly
  // long-living. TODO(marja, gsathya): Support WeakCells in Scavenger.
  const cell = new (Pretenured) WeakCell{
    map: GetWeakCellMap(),
    finalization_registry: finalizationRegistry,
    target: target,
    unregister_token: unregisterToken,
    holdings: heldValue,
    prev: Undefined,
    next: Undefined,
    key_list_prev: Undefined,
    key_list_next: Undefined
  };
  // 7. Append cell to finalizationRegistry.[[Cells]].
  PushCell(finalizationRegistry, cell);
  if (unregisterToken != Undefined) {
    // If an unregister token is provided, a runtime call is needed to
    // do some OrderedHashTable operations and register the mapping.
    // See v8:10705.
    runtime::JSFinalizationRegistryRegisterWeakCellWithUnregisterToken(
        finalizationRegistry, cell);
  }
  // 8. Return undefined.
  return Undefined;
}

transitioning javascript builtin
FinalizationRegistryPrototypeCleanupSome(
    js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny {
  // 1. Let finalizationRegistry be the this value.
  //
  // 2. Perform ? RequireInternalSlot(finalizationRegistry, [[Cells]]).
  const methodName: constexpr string =
      'FinalizationRegistry.prototype.cleanupSome';
  const finalizationRegistry =
      Cast<JSFinalizationRegistry>(receiver) otherwise ThrowTypeError(
          MessageTemplate::kIncompatibleMethodReceiver, methodName, receiver);

  let callback: Callable;
  if (arguments[0] != Undefined) {
    // 4. If callback is not undefined and IsCallable(callback) is
    //    false, throw a TypeError exception.
    callback = Cast<Callable>(arguments[0]) otherwise ThrowTypeError(
        MessageTemplate::kWeakRefsCleanupMustBeCallable, arguments[0]);
  } else {
    callback = finalizationRegistry.cleanup;
  }

  FinalizationRegistryCleanupLoop(finalizationRegistry, callback);
  return Undefined;
}
}