// Copyright (c) 2022, 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.

// Prototype js util library for wasm.
library dart.js_util_wasm;

import 'dart:wasm';

/// [JSValue] is the root of the JS interop object hierarchy.
class JSValue {
  final WasmAnyRef _ref;

  JSValue(this._ref);

  static JSValue? box(WasmAnyRef? ref) => ref == null ? null : JSValue(ref);

  WasmAnyRef toAnyRef() => _ref;
  String toString() => _jsStringToDartString(_ref);
  List<Object?> toObjectList() => _jsArrayToDartList(_ref);
  Object toObject() => _jsObjectToDartObject(_ref);
}

/// Raw private JS functions.
external WasmAnyRef _jsObjectFromDartObject(Object object);

external Object _jsObjectToDartObject(WasmAnyRef ref);

@pragma("wasm:import", "dart2wasm.arrayFromDartList")
external WasmAnyRef _jsArrayFromDartList(List<Object?> list);

@pragma("wasm:import", "dart2wasm.arrayToDartList")
external List<Object?> _jsArrayToDartList(WasmAnyRef list);

@pragma("wasm:import", "dart2wasm.stringFromDartString")
external WasmAnyRef _jsStringFromDartString(String string);

@pragma("wasm:import", "dart2wasm.stringToDartString")
external String _jsStringToDartString(WasmAnyRef string);

/// Raw public JS functions.
/// These are public temporarily to give performance conscious users an escape
/// hatch while we decide what this API will actually look like. They may
/// become private in the future, or disappear entirely. For descriptions of the
/// API, please see the corresponding non-raw functions.
@pragma("wasm:import", "dart2wasm.eval")
external void evalRaw(WasmAnyRef code);

@pragma("wasm:import", "dart2wasm.dartify")
external WasmAnyRef? dartifyRaw(WasmAnyRef? object);

@pragma("wasm:import", "dart2wasm.newObject")
external WasmAnyRef newObjectRaw();

@pragma("wasm:import", "dart2wasm.globalThis")
external WasmAnyRef globalThisRaw();

@pragma("wasm:import", "dart2wasm.callConstructorVarArgs")
external WasmAnyRef callConstructorVarArgsRaw(
    WasmAnyRef o, WasmAnyRef name, WasmAnyRef args);

@pragma("wasm:import", "dart2wasm.hasProperty")
external bool hasPropertyRaw(WasmAnyRef o, WasmAnyRef name);

@pragma("wasm:import", "dart2wasm.getProperty")
external WasmAnyRef? getPropertyRaw(WasmAnyRef o, WasmAnyRef name);

@pragma("wasm:import", "dart2wasm.setProperty")
external WasmAnyRef? setPropertyRaw(
    WasmAnyRef o, WasmAnyRef name, WasmAnyRef? value);

@pragma("wasm:import", "dart2wasm.callMethodVarArgs")
external WasmAnyRef? callMethodVarArgsRaw(
    WasmAnyRef o, WasmAnyRef method, WasmAnyRef? args);

WasmAnyRef? jsifyRaw(Object? object) {
  if (object == null) {
    return null;
  } else if (object is JSValue) {
    return object.toAnyRef();
  } else if (object is String) {
    return _jsStringFromDartString(object);
  } else if (object is List<Object?>) {
    return _jsArrayFromDartList(object);
  } else {
    return _jsObjectFromDartObject(object);
  }
}

/// Conversion functions.
/// TODO(joshualitt): Only a small set of types currently work:
///   JS -> Dart:
///     null
///     strings
///     arrays
///     opaque Dart objects passed to JS
///   Dart -> JS:
///     null
///     boolean
///     doubles
///     strings
///     lists
///     opaque JS objects passed to Dart
/// In the future we would like to support more types, at least maps,
/// and to fix some of the issues returning some types from JS.

/// Extension methods for conversions.
extension StringToJS on String {
  JSValue toJS() => JSValue(_jsStringFromDartString(this));
}

extension ListOfObjectToJS on List<Object?> {
  JSValue toJS() => JSValue(_jsArrayFromDartList(this));
}

extension ObjectToJS on Object {
  JSValue toJS() => JSValue(_jsObjectFromDartObject(this));
}

/// Recursively converts objects from Dart to JS.
JSValue? jsify(Object? object) => JSValue.box(jsifyRaw(object));

/// Recursively converts objects from JS to Dart.
Object? dartify(JSValue? object) => object == null
    ? null
    : _jsObjectToDartObject(dartifyRaw(object.toAnyRef())!);

/// js util methods.
/// These are low level calls into JS, and require care to use correctly.

/// Evals a snippet of JS code in a Dart string.
void eval(String code) => evalRaw(code.toJS().toAnyRef());

/// Creates a new JS object literal and returns it.
JSValue newObject() => JSValue(newObjectRaw());

/// Returns a reference to `globalThis`.
JSValue globalThis() => JSValue(globalThisRaw());

/// Gets a [String] name property off of a JS object [o], invokes it as
/// a constructor with a JS array of arguments [args], and returns the
/// constructed JS object.
JSValue callConstructorVarArgs(JSValue o, String name, List<JSValue?> args) =>
    JSValue(callConstructorVarArgsRaw(
        o.toAnyRef(), name.toJS().toAnyRef(), args.toJS().toAnyRef()));

/// Checks for a [String] name on a JS object [o].
bool hasProperty(JSValue o, String name) =>
    hasPropertyRaw(o.toAnyRef(), name.toJS().toAnyRef());

/// Gets a JS property with [String] name off of a JS object [o].
JSValue? getProperty(JSValue o, String name) =>
    JSValue.box(getPropertyRaw(o.toAnyRef(), name.toJS().toAnyRef()));

/// Sets a JS property with [String] name on JS object [o] to the JS value
/// [value], then returns [value].
JSValue? setProperty(JSValue o, String name, JSValue? value) => JSValue.box(
    setPropertyRaw(o.toAnyRef(), name.toJS().toAnyRef(), value?.toAnyRef()));

/// Calls a JS method with a [String] name on JS object [o] with a JS array
/// of arguments [args] and returns the resulting JS value.
JSValue? callMethodVarArgs(JSValue o, String method, List<JSValue?> args) =>
    JSValue.box(callMethodVarArgsRaw(
        o.toAnyRef(), method.toJS().toAnyRef(), args.toJS().toAnyRef()));
