// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// Utility methods to manipulate JavaScript objects dynamically.
///
/// This library is typically meant to be used when the names of properties or
/// methods are not known statically. This library is similar to `dart:js_util`,
/// except the methods here are extension methods that use JS types. This
/// allows code using these functions to also be compiled to WebAssembly.
///
/// In general, prefer to write JS interop interfaces and external static
/// interop members using `dart:js_interop`. This library is meant to work
/// around issues and help with migration from older JS interop libraries.
///
/// > [!NOTE]
/// > As the name suggests, usage of this library *can* be unsafe. This means
/// > that safe usage of these methods cannot necessarily be verified
/// > statically. Prefer using statically analyzable values like constants or
/// > literals for property or method names so that usage can be verified. This
/// > library should be used cautiously and only when the same effect cannot be
/// > achieved with static interop.
///
/// {@category Web}
library;

import 'dart:js_interop';

/// Utility methods to check, get, set, and call properties on a [JSObject].
///
/// See the [JavaScript specification](https://tc39.es/ecma262/#sec-object-type)
/// for more details on using properties.
extension JSObjectUnsafeUtilExtension on JSObject {
  /// Shorthand helper for [hasProperty] to check whether this [JSObject]
  /// contains the property key [property], but takes and returns a Dart value.
  bool has(String property) => hasProperty(property.toJS).toDart;

  /// Whether or not this [JSObject] contains the property key [property].
  ///
  /// Uses JavaScript's `in` to check.
  external JSBoolean hasProperty(JSAny property);

  /// Shorthand helper for [getProperty] to get the value of the property key
  /// [property] of this [JSObject], but takes a Dart value.
  JSAny? operator [](String property) => getProperty(property.toJS);

  /// The value of the property key [property] of this [JSObject].
  external R getProperty<R extends JSAny?>(JSAny property);

  /// Shorthand helper for [setProperty] to write the [value] of the property
  /// key [property] of this [JSObject], but takes a Dart value.
  void operator []=(String property, JSAny? value) =>
      setProperty(property.toJS, value);

  /// Write the [value] of property key [property] of this [JSObject].
  external void setProperty(JSAny property, JSAny? value);

  external JSAny? _callMethod(
    JSAny method, [
    JSAny? arg1,
    JSAny? arg2,
    JSAny? arg3,
    JSAny? arg4,
  ]);

  /// Calls [method] on this [JSObject] with up to four arguments.
  ///
  /// Returns the result of calling [method], which must be an [R].
  ///
  /// This helper doesn't allow passing nulls, as it determines whether an
  /// argument is passed based on whether it was null or not. Prefer
  /// [callMethodVarArgs] if you need to pass nulls.
  R callMethod<R extends JSAny?>(
    JSAny method, [
    JSAny? arg1,
    JSAny? arg2,
    JSAny? arg3,
    JSAny? arg4,
  ]) => _callMethod(method, arg1, arg2, arg3, arg4) as R;

  external JSAny? _callMethodVarArgs(JSAny method, [List<JSAny?>? arguments]);

  /// Calls [method] on this [JSObject] with a variable number of [arguments].
  ///
  /// Returns the result of calling [method], which must be an [R].
  R callMethodVarArgs<R extends JSAny?>(
    JSAny method, [
    List<JSAny?>? arguments,
  ]) => _callMethodVarArgs(method, arguments) as R;

  /// Deletes the property with key [property] from this [JSObject].
  ///
  /// Uses JavaScript's `delete` to delete the property.
  external JSBoolean delete(JSAny property);
}

/// Utility methods to call [JSFunction]s as constructors.
extension JSFunctionUnsafeUtilExtension on JSFunction {
  external JSObject _callAsConstructor([
    JSAny? arg1,
    JSAny? arg2,
    JSAny? arg3,
    JSAny? arg4,
  ]);

  /// Calls this [JSFunction] as a constructor with up to four arguments.
  ///
  /// Returns the constructed object, which must be an [R].
  ///
  /// This helper doesn't allow passing nulls, as it determines whether an
  /// argument is passed based on whether it was null or not. Prefer
  /// [callAsConstructorVarArgs] if you need to pass nulls.
  // TODO(srujzs): The type bound should extend `JSObject`.
  R callAsConstructor<R>([
    JSAny? arg1,
    JSAny? arg2,
    JSAny? arg3,
    JSAny? arg4,
  ]) => _callAsConstructor(arg1, arg2, arg3, arg4) as R;

  external JSObject _callAsConstructorVarArgs([List<JSAny?>? arguments]);

  /// Calls this [JSFunction] as a constructor with a variable number of
  /// arguments.
  ///
  /// Returns the constructed [JSObject], which must be an [R].
  R callAsConstructorVarArgs<R extends JSObject>([List<JSAny?>? arguments]) =>
      _callAsConstructorVarArgs(arguments) as R;
}
