// 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.

// Helpers for working with JS.
library dart._js_helper;

import 'dart:_error_utils';
import 'dart:_internal';
import 'dart:_js_types' as js_types;
import 'dart:_string';
import 'dart:_wasm';
import 'dart:async';
import 'dart:js_interop';
import 'dart:js_interop' as interop;
import 'dart:js_interop_unsafe';
import 'dart:typed_data';

part 'regexp_helper.dart';

/// [JSValue] is just a box for a `ref null extern` that is not a JS `null` or
/// `undefined`.
///
/// This is the type that all JS interop types (`JSAny`, `JSNumber` etc.) are an
/// extension of.
class JSValue extends JSExternWrapper {
  /// This reference is always non-null and it never points to a JS `undefined`.
  /// We currently don't make it non-nullable as that makes it impossible to
  /// dummy-initialize locals with `JSValue` type.
  JSValue(WasmExternRef? ref) : assert(!isDartNull(ref)), super(ref);

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

  static T boxT<T>(WasmExternRef? ref) => unsafeCastOpaque<T>(box(ref));

  @pragma('wasm:prefer-inline')
  static WasmExternRef? unbox(JSValue? v) =>
      v == null ? WasmExternRef.nullRef : v.wrappedExternRef;

  @override
  bool operator ==(Object that) =>
      that is JSValue && areEqualInJS(wrappedExternRef, that.wrappedExternRef);

  // Because [JSValue] is a subtype of [Object] it can be used in Dart
  // collections. Unfortunately, JS does not expose an efficient hash code
  // operation. To avoid surprising behavior, we force all [JSValue]s to fall
  // back to differentiation via equality, essentially making [Set] and [Map]
  // a regular linked list when the keys are [JSValue]. This behavior is not
  // intuitive.
  // TODO(joshualitt): There are a lot of different directions we can go, but
  // the most straightforward to expose `JSMap` and `JSSet` from JS for users
  // who need to efficiently manage JS objects in collections.
  @override
  int get hashCode => 0;

  @override
  String toString() => stringify(wrappedExternRef);

  bool get isExternalizedDartValue => isWasmGCStruct(wrappedExternRef);
}

// Extension helpers to convert to an externref.
// TODO(srujzs): We should rename these to `getAsExternRef` so they don't
// collide with instance members of box objects.

extension DoubleToExternRef on double? {
  WasmExternRef? get toExternRef =>
      this == null ? WasmExternRef.nullRef : toJSNumber(this!);
}

extension StringToExternRef on String? {
  WasmExternRef? get toExternRef => this == null
      ? WasmExternRef.nullRef
      : jsStringFromDartString(this!).wrappedExternRef;
}

extension JSValueToExternRef on JSValue? {
  WasmExternRef? get toExternRef => JSValue.unbox(this);
}

extension JSAnyToExternRef on JSAny? {
  WasmExternRef? get toExternRef => JSValue.unbox(this as JSValue?);
}

// For `dartify` and `jsify`, we match the conflation of `JSUndefined`, `JSNull`
// and `null`.
@pragma('wasm:entry-point')
bool isDartNull(WasmExternRef? ref) => ref.isNull || isJSUndefined(ref);

class JSArrayIteratorAdapter<T> implements Iterator<T> {
  final JSArray array;
  int index = -1;

  JSArrayIteratorAdapter(this.array);

  @override
  bool moveNext() {
    index++;
    int length = array.length;
    if (index > length) {
      throw 'Iterator out of bounds';
    }
    return index < length;
  }

  @override
  T get current => dartifyRaw(array[index].toExternRef) as T;
}

/// [JSArrayIterableAdapter] lazily adapts a [JSArray] to Dart's [Iterable]
/// interface.
class JSArrayIterableAdapter<T> extends EfficientLengthIterable<T>
    implements HideEfficientLengthIterable<T> {
  final JSArray array;

  JSArrayIterableAdapter(this.array);

  @override
  Iterator<T> get iterator => JSArrayIteratorAdapter<T>(array);

  @override
  int get length => array.length;
}

Object jsObjectToDartObject(WasmExternRef? ref) =>
    unsafeCastOpaque<Object>(ref.internalize());

WasmExternRef jsObjectFromDartObject(Object object) =>
    unsafeCastOpaque<WasmAnyRef>(object).externalize();

bool isJSUndefined(WasmExternRef? o) => JS<bool>('o => o === undefined', o);

bool isJSFunction(WasmExternRef? o) =>
    JS<bool>("o => typeof o === 'function'", o);

bool isJSWrappedDartFunction(WasmExternRef? o) => JS<bool>(
  "o => typeof o === 'function' && o[jsWrappedDartFunctionSymbol] === true",
  o,
);

bool isJSObject(WasmExternRef? o) => JS<bool>("o => o instanceof Object", o);

bool isJSSimpleObject(WasmExternRef? o) => JS<bool>("""o => {
            const proto = Object.getPrototypeOf(o);
            return proto === Object.prototype || proto === null;
          }""", o);

bool isJSRegExp(WasmExternRef? o) => JS<bool>("o => o instanceof RegExp", o);

bool areEqualInJS(WasmExternRef? l, WasmExternRef? r) =>
    JS<bool>("(l, r) => l === r", l, r);

@pragma('wasm:entry-point')
double toDartDouble(WasmExternRef? ref) {
  final numberType = _checkNumberType(ref);
  if (numberType != 1) {
    throw ArgumentError('JS value is not a number');
  }
  return _toDartDoubleUnchecked(ref);
}

@pragma('wasm:entry-point')
double? toDartNullableDouble(WasmExternRef? ref) {
  final refType = _checkNumberType(ref);
  if (refType == 0) return null;
  if (refType == 1) return _toDartDoubleUnchecked(ref);
  throw ArgumentError('JS value is not a number');
}

double _toDartDoubleUnchecked(WasmExternRef? ref) => JS<double>("o => o", ref);

int _checkNumberType(WasmExternRef? ref) {
  return JS<WasmI32>("""o => {
      if (o === undefined || o === null) return 0;
      if (typeof o === 'number') return 1;
      return 2;
    }""", ref).toIntUnsigned();
}

int _jsNonNullToInt(WasmExternRef? ref, bool typeIsRight) {
  if (typeIsRight) {
    final dartDouble = _toDartDoubleUnchecked(ref);
    if (dartDouble.isFinite) {
      final dartInt = dartDouble.toInt();
      if (dartInt.toDouble() == dartDouble) {
        return dartInt;
      }
    }
  }
  throw ArgumentError('JS value is not integer');
}

@pragma('wasm:entry-point')
int toDartInt(WasmExternRef? ref) {
  final numberType = _checkNumberType(ref);
  return _jsNonNullToInt(ref, numberType == 1);
}

@pragma('wasm:entry-point')
int? toDartNullableInt(WasmExternRef? ref) {
  final numberType = _checkNumberType(ref);
  if (numberType == 0) return null;
  return _jsNonNullToInt(ref, numberType == 1);
}

@pragma('wasm:entry-point')
WasmExternRef? toJSNumber(double ref) => JS<WasmExternRef?>("o => o", ref);

int _checkBoolType(WasmExternRef? ref) {
  return JS<WasmI32>("""o => {
      if (o === undefined || o === null) return 0;
      if (typeof o === 'boolean') return 1;
      return 2;
    }""", ref).toIntUnsigned();
}

@pragma('wasm:entry-point')
bool toDartBool(WasmExternRef? ref) {
  final refType = _checkBoolType(ref);
  if (refType != 1) {
    throw ArgumentError('JS value is not a boolean');
  }
  return _toDartBoolUnchecked(ref);
}

@pragma('wasm:entry-point')
bool? toDartNullableBool(WasmExternRef? ref) {
  final refType = _checkBoolType(ref);
  if (refType == 0) return null;
  if (refType == 1) return _toDartBoolUnchecked(ref);
  throw ArgumentError('JS value is not a boolean');
}

bool _toDartBoolUnchecked(WasmExternRef? ref) => JS<bool>("o => o", ref);

WasmExternRef? toJSBoolean(bool b) => JS<WasmExternRef?>("b => !!b", b);

int objectLength(WasmExternRef? o) =>
    JS<WasmI32>("o => o.length", o).toIntSigned();

int byteLength(WasmExternRef? o) =>
    JS<WasmI32>("o => o.byteLength", o).toIntSigned();

WasmExternRef? objectReadIndex(WasmExternRef? o, int index) =>
    JS<WasmExternRef?>("(o, i) => o[i]", o, index.toWasmI32());

Function unwrapJSWrappedDartFunction(WasmExternRef? f) =>
    JS<Function>("f => f.dartFunction", f);

external WasmExternRef jsInt8ArrayFromDartInt8List(Int8List l);

external WasmExternRef jsUint8ArrayFromDartUint8List(Uint8List l);

external WasmExternRef jsUint8ClampedArrayFromDartUint8ClampedList(
  Uint8ClampedList l,
);

external WasmExternRef jsInt16ArrayFromDartInt16List(Int16List l);

external WasmExternRef jsUint16ArrayFromDartUint16List(Uint16List l);

external WasmExternRef jsInt32ArrayFromDartInt32List(Int32List l);

external WasmExternRef jsUint32ArrayFromDartUint32List(Uint32List l);

external WasmExternRef jsFloat32ArrayFromDartFloat32List(Float32List l);

external WasmExternRef jsFloat64ArrayFromDartFloat64List(Float64List l);

external WasmExternRef jsDataViewFromDartByteData(ByteData data, int length);

WasmExternRef? _jsifyRawList(List<Object?> list) {
  final length = list.length;
  final result = JSArray<JSAny?>.withLength(length);
  for (int i = 0; i < length; i++) {
    result[i] = JSValue.box(jsifyRaw(list[i])) as JSAny?;
  }
  return (result as JSValue).toExternRef;
}

external JSStringImpl jsStringFromDartString(String s);

WasmExternRef? newObjectRaw() => JS<WasmExternRef?>('() => ({})');

WasmExternRef? newArrayRaw() => JS<WasmExternRef?>('() => []');

WasmExternRef? newArrayFromLengthRaw(int length) =>
    JS<WasmExternRef?>('l => new Array(l)', length.toWasmI32());

WasmExternRef? globalThisRaw() => JS<WasmExternRef?>('() => globalThis');

WasmExternRef? callConstructorVarArgsRaw(
  WasmExternRef? o,
  WasmExternRef? args,
) =>
    // Apply bind to the constructor. We pass `null` as the first argument
    // to `bind.apply` because this is `bind`'s unused context
    // argument(`new` will explicitly create a new context).
    JS<WasmExternRef?>(
      """(constructor, args) => {
      const factoryFunction = constructor.bind.apply(
          constructor, [null, ...args]);
      return new factoryFunction();
    }""",
      o,
      args,
    );

bool hasPropertyRaw(WasmExternRef? o, WasmExternRef? p) =>
    JS<bool>("(o, p) => p in o", o, p);

WasmExternRef? getPropertyRaw(WasmExternRef? o, WasmExternRef? p) =>
    JS<WasmExternRef?>("(o, p) => o[p]", o, p);

WasmExternRef? setPropertyRaw(
  WasmExternRef? o,
  WasmExternRef? p,
  WasmExternRef? v,
) => JS<WasmExternRef?>("(o, p, v) => o[p] = v", o, p, v);

WasmExternRef? callMethodVarArgsRaw(
  WasmExternRef? o,
  WasmExternRef? method,
  WasmExternRef? args,
) => JS<WasmExternRef?>("(o, m, a) => o[m].apply(o, a)", o, method, args);

String typeof(WasmExternRef? object) =>
    JSStringImpl.fromRefUnchecked(JS<WasmExternRef?>("o => typeof o", object));

String stringify(WasmExternRef? object) =>
    JSStringImpl.fromRefUnchecked(JS<WasmExternRef?>("o => String(o)", object));

/// `Promise.then` call where [failureFunc] can be a JS function that expects
/// two arguments, the first being the error, and the second being whether the
/// error was undefined.
///
/// The second argument is needed as dart2wasm implicitly converts all JS
/// `undefined`s to Dart `null` when boxing JS values.
void promiseThenWithIsUndefined(
  WasmExternRef? promise,
  WasmExternRef? successFunc,
  WasmExternRef? failureFunc,
) => JS<void>(
  "(p, s, f) => p.then(s, (e) => f(e, e === undefined))",
  promise,
  successFunc,
  failureFunc,
);

Future<T> externPromiseToFuture<T>(WasmExternRef? jsPromise) {
  final completer = Completer<T>();
  final success = (JSAny? r) {
    // Note that we explicitly type the parameter as `JSAny?` instead of `T`.
    // This is because if there's a `TypeError` with the cast, we want to
    // bubble that up through the completer, so we end up doing a try-catch
    // here to do so.
    try {
      final value = r as T;
      completer.complete(value);
    } catch (e) {
      completer.completeError(e);
    }
  }.toJS;
  final error = (JSAny? e, bool isUndefined) {
    // `e` is null when the original error is either JS `null` or JS
    // `undefined`.
    if (e == null) {
      completer.completeError(NullRejectionException(isUndefined));
      return;
    }
    completer.completeError(e);
  }.toJS;
  promiseThenWithIsUndefined(jsPromise, success.toExternRef, error.toExternRef);
  return completer.future;
}

// Currently, `allowInterop` returns a Function type. This is unfortunate for
// Dart2wasm because it means arbitrary Dart functions can flow to JS util
// calls. Our only solutions is to cache every function called with
// `allowInterop` and to replace them with the wrapped variant when they flow
// to JS.
// NOTE: We are not currently replacing functions returned from JS.
final Map<Function, JSValue> functionToJSWrapper = Map.identity();

WasmExternRef? jsArrayBufferFromDartByteBuffer(ByteBuffer buffer) {
  ByteData byteData = ByteData.view(buffer);
  WasmExternRef? dataView = jsDataViewFromDartByteData(
    byteData,
    byteData.lengthInBytes,
  );
  return getPropertyRaw(dataView, 'buffer'.toExternRef);
}

WasmExternRef? jsifyRaw(Object? o) {
  if (o == null) return WasmExternRef.nullRef;
  if (o is bool) return toJSBoolean(o);
  if (o is num) return jsifyNum(o);
  if (o is JSValue) return jsifyJSValue(o);
  if (o is String) return jsifyString(o);
  if (o is js_types.JSArrayBase) {
    if (o is js_types.JSInt8ArrayImpl) return jsifyJSInt8ArrayImpl(o);
    if (o is js_types.JSUint8ArrayImpl) return jsifyJSUint8ArrayImpl(o);
    if (o is js_types.JSUint8ClampedArrayImpl) {
      return jsifyJSUint8ClampedArrayImpl(o);
    }
    if (o is js_types.JSInt16ArrayImpl) return jsifyJSInt16ArrayImpl(o);
    if (o is js_types.JSUint16ArrayImpl) return jsifyJSUint16ArrayImpl(o);
    if (o is js_types.JSInt32ArrayImpl) return jsifyJSInt32ArrayImpl(o);
    if (o is js_types.JSUint32ArrayImpl) return jsifyJSUint32ArrayImpl(o);
    if (o is js_types.JSFloat32ArrayImpl) return jsifyJSFloat32ArrayImpl(o);
    if (o is js_types.JSFloat64ArrayImpl) return jsifyJSFloat64ArrayImpl(o);
  } else if (o is TypedData) {
    if (o is Int8List) return jsInt8ArrayFromDartInt8List(o);
    if (o is Uint8List) return jsUint8ArrayFromDartUint8List(o);
    if (o is Uint8ClampedList) {
      return jsUint8ClampedArrayFromDartUint8ClampedList(o);
    }
    if (o is Int16List) return jsInt16ArrayFromDartInt16List(o);
    if (o is Uint16List) return jsUint16ArrayFromDartUint16List(o);
    if (o is Int32List) return jsInt32ArrayFromDartInt32List(o);
    if (o is Uint32List) return jsUint32ArrayFromDartUint32List(o);
    if (o is Float32List) return jsFloat32ArrayFromDartFloat32List(o);
    if (o is Float64List) return jsFloat64ArrayFromDartFloat64List(o);
    if (o is js_types.JSDataViewImpl) return jsifyJSDataViewImpl(o);
    if (o is ByteData) return jsifyByteData(o);
  } else if (o is List<Object?>) {
    // TODO(srujzs): Once `package:js` support is fully removed, we should
    // remove this as it'll be dead code. `jsify` will convert iterables
    // differently, and `dart:js_interop` `external` conversions shouldn't come
    // across this code.
    return _jsifyRawList(o);
  } else if (o is ByteBuffer) {
    if (o is js_types.JSArrayBufferImpl) return jsifyJSArrayBufferImpl(o);
    return jsArrayBufferFromDartByteBuffer(o);
  } else if (o is Function) {
    // TODO(srujzs): Once `package:js` support is fully removed, we should
    // remove this to unify with the JS backends, which don't do this
    // conversion.
    return jsifyFunction(o);
  } else {
    return jsObjectFromDartObject(o);
  }
}

WasmExternRef? jsifyInt(int i) {
  const int minI31 = -(1 << 30);
  const int maxI31 = (1 << 30) - 1;

  // Pass small ints as `i31ref` to avoid allocation.
  if (i >= minI31 && i <= maxI31) {
    return WasmI31Ref.fromI32(WasmI32.fromInt(i)).externalize();
  }

  return toJSNumber(i.toDouble());
}

WasmExternRef? jsifyNum(num o) =>
    o is int ? jsifyInt(o) : toJSNumber(unsafeCast<double>(o));

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSValue(JSValue o) => o.toExternRef;

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyString(String o) => jsStringFromDartString(o).toExternRef;

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSInt8ArrayImpl(js_types.JSInt8ArrayImpl o) =>
    o.toJSArrayExternRef();

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSUint8ArrayImpl(js_types.JSUint8ArrayImpl o) =>
    o.toJSArrayExternRef();

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSUint8ClampedArrayImpl(
  js_types.JSUint8ClampedArrayImpl o,
) => o.toJSArrayExternRef();

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSInt16ArrayImpl(js_types.JSInt16ArrayImpl o) =>
    o.toJSArrayExternRef();

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSUint16ArrayImpl(js_types.JSUint16ArrayImpl o) =>
    o.toJSArrayExternRef();

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSInt32ArrayImpl(js_types.JSInt32ArrayImpl o) =>
    o.toJSArrayExternRef();

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSUint32ArrayImpl(js_types.JSUint32ArrayImpl o) =>
    o.toJSArrayExternRef();

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSFloat32ArrayImpl(js_types.JSFloat32ArrayImpl o) =>
    o.toJSArrayExternRef();

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSFloat64ArrayImpl(js_types.JSFloat64ArrayImpl o) =>
    o.toJSArrayExternRef();

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSDataViewImpl(js_types.JSDataViewImpl o) =>
    o.wrappedExternRef;

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyJSArrayBufferImpl(js_types.JSArrayBufferImpl o) =>
    o.wrappedExternRef;

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyByteData(ByteData o) =>
    jsDataViewFromDartByteData(o, o.lengthInBytes);

@pragma('wasm:prefer-inline')
WasmExternRef? jsifyFunction(Function o) {
  assert(
    functionToJSWrapper.containsKey(o),
    'Must call `allowInterop` on functions before they flow to JS',
  );
  return functionToJSWrapper[o]!.toExternRef;
}

bool isWasmGCStruct(WasmExternRef? ref) => ref.internalize()?.isObject ?? false;

/// Container class for constants that represent the possible types of a
/// [WasmExternRef] that can then be used in a [dartifyRaw] call.
///
/// The values within this class should correspond to the values returned by
/// [externRefType] and should be updated if that function is updated. Constants
/// are preferred over enums for performance.
abstract final class ExternRefType {
  static const int null_ = 0;
  static const int undefined = 1;
  static const int boolean = 2;
  static const int number = 3;
  static const int string = 4;
  static const int array = 5;
  static const int int8Array = 6;
  static const int uint8Array = 7;
  static const int uint8ClampedArray = 8;
  static const int int16Array = 9;
  static const int uint16Array = 10;
  static const int int32Array = 11;
  static const int uint32Array = 12;
  static const int float32Array = 13;
  static const int float64Array = 14;
  static const int dataView = 15;
  static const int arrayBuffer = 16;
  static const int sharedArrayBuffer = 17;
  static const int promise = 18;
  static const int unknown = 19;
}

/// Returns an integer representing the type of [ref] that corresponds to one of
/// the constant values in [ExternRefType].
///
/// If this function is updated to return different values, [ExternRefType]
/// should be updated as well.
int externRefType(WasmExternRef? ref) {
  if (ref.isNull) return ExternRefType.null_;
  final val = JS<WasmI32>('''
  o => {
    if (o === undefined) return 1;
    var type = typeof o;
    if (type === 'boolean') return 2;
    if (type === 'number') return 3;
    if (type === 'string') return 4;
    if (o instanceof Array) return 5;
    if (ArrayBuffer.isView(o)) {
      if (o instanceof Int8Array) return 6;
      if (o instanceof Uint8Array) return 7;
      if (o instanceof Uint8ClampedArray) return 8;
      if (o instanceof Int16Array) return 9;
      if (o instanceof Uint16Array) return 10;
      if (o instanceof Int32Array) return 11;
      if (o instanceof Uint32Array) return 12;
      if (o instanceof Float32Array) return 13;
      if (o instanceof Float64Array) return 14;
      if (o instanceof DataView) return 15;
    }
    if (o instanceof ArrayBuffer) return 16;
    // Feature check for `SharedArrayBuffer` before doing a type-check.
    if (globalThis.SharedArrayBuffer !== undefined &&
        o instanceof SharedArrayBuffer) {
        return 17;
    }
    if (o instanceof Promise) return 18;
    return 19;
  }
  ''', ref).toIntUnsigned();
  return val;
}

/// Non-recursively converts [ref] from a JS value to a Dart value for some JS
/// types.
///
/// If [refType] is not null, it is treated as one of the values from
/// [ExternRefType]. Otherwise, this method calls [externRefType] to determine
/// the right [ExternRefType].
Object? dartifyRaw(WasmExternRef? ref, [int? refType]) {
  refType ??= externRefType(ref);
  return switch (refType) {
    ExternRefType.null_ || ExternRefType.undefined => null,
    ExternRefType.boolean => _toDartBoolUnchecked(ref),
    ExternRefType.number => _toDartDoubleUnchecked(ref),
    ExternRefType.string => JSStringImpl.fromRefUnchecked(ref),
    ExternRefType.array => toDartList(ref),
    ExternRefType.int8Array => js_types.JSInt8ArrayImpl.fromRefUnchecked(ref),
    ExternRefType.uint8Array => js_types.JSUint8ArrayImpl.fromRefUnchecked(ref),
    ExternRefType.uint8ClampedArray =>
      js_types.JSUint8ClampedArrayImpl.fromRefUnchecked(ref),
    ExternRefType.int16Array => js_types.JSInt16ArrayImpl.fromRefUnchecked(ref),
    ExternRefType.uint16Array => js_types.JSUint16ArrayImpl.fromRefUnchecked(
      ref,
    ),
    ExternRefType.int32Array => js_types.JSInt32ArrayImpl.fromRefUnchecked(ref),
    ExternRefType.uint32Array => js_types.JSUint32ArrayImpl.fromRefUnchecked(
      ref,
    ),
    ExternRefType.float32Array => js_types.JSFloat32ArrayImpl.fromRefUnchecked(
      ref,
    ),
    ExternRefType.float64Array => js_types.JSFloat64ArrayImpl.fromRefUnchecked(
      ref,
    ),
    ExternRefType.arrayBuffer || ExternRefType.sharedArrayBuffer =>
      js_types.JSArrayBufferImpl.fromRefUnchecked(ref),
    ExternRefType.dataView => js_types.JSDataViewImpl.fromRefUnchecked(ref),
    ExternRefType.promise => externPromiseToFuture<JSValue?>(ref),
    ExternRefType.unknown =>
      isJSWrappedDartFunction(ref)
          ? unwrapJSWrappedDartFunction(ref)
          : isWasmGCStruct(ref)
          ? jsObjectToDartObject(ref)
          : JSValue(ref),
    _ => () {
      // Assert that we've handled everything in the range.
      assert(refType! >= 0 && refType >= ExternRefType.unknown);
      throw 'Unhandled dartifyRaw type case: $refType';
    }(),
  };
}

List<double> jsFloatTypedArrayToDartFloatTypedData(
  WasmExternRef? ref,
  List<double> makeTypedData(int size),
) {
  int length = objectLength(ref);
  List<double> list = makeTypedData(length);
  for (int i = 0; i < length; i++) {
    list[i] = toDartDouble(objectReadIndex(ref, i));
  }
  return list;
}

List<int> jsIntTypedArrayToDartIntTypedData(
  WasmExternRef? ref,
  List<int> makeTypedData(int size),
) {
  int length = objectLength(ref);
  List<int> list = makeTypedData(length);
  for (int i = 0; i < length; i++) {
    list[i] = toDartDouble(objectReadIndex(ref, i)).toInt();
  }
  return list;
}

JSArray<T> toJSArray<T extends JSAny?>(List<T> list) {
  final length = list.length;

  if (length <= 4) {
    if (length == 0) {
      return JSArray<T>.withLength(0);
    }
    final list0 = list[0].toExternRef;
    if (length == 1) {
      return JSValue(JS<WasmExternRef>("o => [o]", list0)) as JSArray<T>;
    }
    final list1 = list[1].toExternRef;
    if (length == 2) {
      return JSValue(JS<WasmExternRef>("(o0, o1) => [o0, o1]", list0, list1))
          as JSArray<T>;
    }
    final list2 = list[2].toExternRef;
    if (length == 3) {
      return JSValue(
        JS<WasmExternRef>("(o0, o1, o2) => [o0, o1, o2]", list0, list1, list2),
      ) as JSArray<T>;
    }
    final list3 = list[3].toExternRef;
    if (length == 4) {
      return JSValue(
        JS<WasmExternRef>(
          "(o0, o1, o2, o3) => [o0, o1, o2, o3]",
          list0,
          list1,
          list2,
          list3,
        ),
      ) as JSArray<T>;
    }
  }

  JSArray<T> result = JSArray<T>.withLength(length);
  for (int i = 0; i < length; i++) {
    result[i] = list[i];
  }
  return result;
}

@pragma('wasm:entry-point')
List<Object?> toDartList(WasmExternRef? ref) => List<Object?>.generate(
  objectLength(ref),
  (int n) => dartifyRaw(objectReadIndex(ref, n)),
);

@pragma('wasm:entry-point')
List<Object?>? toDartNullableList(WasmExternRef? ref) {
  if (ref.isNull || isJSUndefined(ref)) return null;
  return toDartList(ref);
}

// These two trivial helpers are needed to work around an issue with tearing off
// functions that take / return [WasmExternRef].
bool _isDartFunctionWrapped<F extends Function>(F f) =>
    functionToJSWrapper.containsKey(f);

F _wrapDartFunction<F extends Function>(F f, WasmExternRef ref) {
  functionToJSWrapper[f] = JSValue(ref);
  return f;
}

/// Takes a [codeTemplate] string which must represent a valid JS function, and
/// a list of optional arguments. The [codeTemplate] will be inserted into the
/// JS runtime, and the call to [JS] will be replaced by a call to an external
/// static method stub that imports the JS function.
///
/// We will replace the enclosing procedure itself if:
///   1) The enclosing procedure is static.
///   2) The enclosing procedure has a body with a single statement, and that
///      statement is just the `StaticInvocation` of [JS] itself.
///   3) All of the arguments to [JS] are `VariableGet`s.
external T JS<T>(
  String codeTemplate, [
  arg0,
  arg1,
  arg2,
  arg3,
  arg4,
  arg5,
  arg6,
  arg7,
  arg8,
  arg9,
  arg10,
  arg11,
  arg12,
  arg13,
  arg14,
  arg51,
  arg16,
  arg17,
  arg18,
  arg19,
]);

/// Represents a JS `null` or `undefined` thrown from JS and caught in Wasm.
///
/// The class name is copied from the dart2js class for the same thing, for
/// compatibility.
///
/// This class is allocated by the generated code.
class NullThrownFromJavaScriptException implements Exception {
  /// Whether the reference was `null`. If not, then it must be pointing to a JS
  /// `undefined`.
  final bool _isNull;

  const NullThrownFromJavaScriptException.fromNull() : _isNull = true;

  const NullThrownFromJavaScriptException.fromUndefined() : _isNull = false;

  /// `toString` copied from dart2js's `NullThrownFromJavaScriptException` for
  /// compatibility.
  @override
  String toString() =>
      "Throw of null ('${_isNull ? 'null' : 'undefined'}' from JavaScript)";
}

/// Box an exception caught from JS, the same way as dart2js.
///
/// When the exception value is `null` or `undefined`, this returns a
/// [NullThrownFromJavaScriptException].
///
/// Otherwise it returns a `JSValue`.
///
/// This is called by the generated code and passed a JS exception as
/// `externref`, caught using the `WebAssembly.JSTag` exception tag. The return
/// value will be used to assign the exception variable in `catch` blocks, so it
/// needs to have type `Object`.
@pragma('wasm:entry-point')
Object boxJsException(WasmExternRef? ref) {
  if (ref.isNull) return NullThrownFromJavaScriptException.fromNull();
  if (isJSUndefined(ref))
    return NullThrownFromJavaScriptException.fromUndefined();
  return JSValue(ref);
}

/// Get the stack trace of an exception value thrown in JS and caught in Wasm.
///
/// This is called by the generated code, with the same argument as
/// [boxJsException].
///
/// The return value will be assigned to the stack trace variables in `catch`
/// blocks, so it needs to have type [StackTrace].
@pragma('wasm:entry-point')
StackTrace jsExceptionStackTrace(WasmExternRef? ref) => JavaScriptStack(
  JS<WasmExternRef?>("""
      (exn) => {
        if (exn instanceof Error) {
          return exn.stack;
        } else {
          return null;
        }
      }
    """, ref),
);

class JavaScriptStack extends JSExternWrapper implements StackTrace {
  final bool _fromCurrent;

  JavaScriptStack(WasmExternRef? ref) : _fromCurrent = false, super(ref);

  // Note: We remove the first four frames to prevent including
  // `StackTrace.current`, other current helpers and the JS interop function.
  // On Chrome, the first line is not a frame but a line with just "Error",
  // sometimes with details:
  // "Error: ...". Also remove that line.
  late final String _stringified = wrappedExternRef.isNull
      ? ""
      : JSStringImpl.fromRefUnchecked(
          _fromCurrent
              ? JS<WasmExternRef?>(r"""(exn) => {
            let stackString = exn.toString();
            let frames = stackString.split('\n');
            let drop = 4;
            if (frames[0].startsWith('Error')) {
                drop += 1;
            }
            return frames.slice(drop).join('\n');
          }""", wrappedExternRef)
              : wrappedExternRef,
        );

  @pragma("wasm:never-inline")
  JavaScriptStack.current()
    : _fromCurrent = true,
      super(JS<WasmExternRef?>("() => new Error().stack"));

  @override
  String toString() => _stringified;
}

base class JSExternWrapper {
  final WasmExternRef? _externRef;

  JSExternWrapper(this._externRef);
}

extension JSExternWrapperExt on JSExternWrapper {
  @pragma("wasm:prefer-inline")
  WasmExternRef? get wrappedExternRef => _externRef;
}
