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

library dart._js_helper;

import 'dart:async' show Zone;
import 'dart:collection';

import 'dart:_foreign_helper'
    show DART_RUNTIME_LIBRAY, JS, JS_CLASS_REF, JS_GET_FLAG, JSExportName;

import 'dart:_interceptors';
import 'dart:_internal'
    show
        EfficientLengthIterable,
        HideEfficientLengthIterable,
        MappedIterable,
        IterableElementError,
        SubListIterable,
        patch;

import 'dart:_native_typed_data';
import 'dart:_rti' as rti show pairwiseIsTest, evaluateRtiForRecord, Rti;
import 'dart:_runtime' as dart;

part 'annotations.dart';
part 'linked_hash_map.dart';
part 'linked_hash_set.dart';
part 'identity_hash_map.dart';
part 'identity_hash_set.dart';
part 'custom_hash_map.dart';
part 'custom_hash_set.dart';
part 'native_helper.dart';
part 'regexp_helper.dart';
part 'string_helper.dart';
part 'js_rti.dart';

/// Adapts a JS `[Symbol.iterator]` to a Dart `get iterator`.
///
/// This is the inverse of `JsIterator`, for classes where we can more
/// efficiently obtain a JS iterator instead of a Dart one.
///
// TODO(jmesserly): this adapter is to work around
// https://github.com/dart-lang/sdk/issues/28320
class DartIterator<E> implements Iterator<E> {
  final _jsIterator;
  E? _current;

  DartIterator(this._jsIterator);

  E get current => _current as E;

  bool moveNext() {
    final ret = JS('', '#.next()', _jsIterator);
    _current = JS('', '#.value', ret);
    return JS<bool>('!', '!#.done', ret);
  }
}

/// Used to compile `sync*`.
class SyncIterable<E> extends Iterable<E> {
  final Function() _initGenerator;
  SyncIterable(this._initGenerator);

  @JSExportName('Symbol.iterator')
  _jsIterator() => _initGenerator();

  get iterator => DartIterator(_initGenerator());
}

class Primitives {
  static int? parseInt(@nullCheck String source, int? _radix) {
    var re = JS('', r'/^\s*[+-]?((0x[a-f0-9]+)|(\d+)|([a-z0-9]+))\s*$/i');
    // This isn't reified List<String?>?, but it's safe to use as long as we use
    // it locally and don't expose it to user code.
    var match = JS<List<String?>?>('', '#.exec(#)', re, source);
    int digitsIndex = 1;
    int hexIndex = 2;
    int decimalIndex = 3;
    if (match == null) {
      // TODO(sra): It might be that the match failed due to unrecognized U+0085
      // spaces.  We could replace them with U+0020 spaces and try matching
      // again.
      return null;
    }
    var decimalMatch = match[decimalIndex];
    if (_radix == null) {
      if (decimalMatch != null) {
        // Cannot fail because we know that the digits are all decimal.
        return JS<int>('!', r'parseInt(#, 10)', source);
      }
      if (match[hexIndex] != null) {
        // Cannot fail because we know that the digits are all hex.
        return JS<int>('!', r'parseInt(#, 16)', source);
      }
      return null;
    }
    @notNull
    var radix = _radix;
    if (radix < 2 || radix > 36) {
      throw RangeError.range(radix, 2, 36, 'radix');
    }
    if (radix == 10 && decimalMatch != null) {
      // Cannot fail because we know that the digits are all decimal.
      return JS<int>('!', r'parseInt(#, 10)', source);
    }
    // If radix >= 10 and we have only decimal digits the string is safe.
    // Otherwise we need to check the digits.
    if (radix < 10 || decimalMatch == null) {
      // We know that the characters must be ASCII as otherwise the
      // regexp wouldn't have matched. Lowercasing by doing `| 0x20` is thus
      // guaranteed to be a safe operation, since it preserves digits
      // and lower-cases ASCII letters.
      int maxCharCode;
      if (radix <= 10) {
        // Allow all digits less than the radix. For example 0, 1, 2 for
        // radix 3.
        // "0".codeUnitAt(0) + radix - 1;
        maxCharCode = (0x30 - 1) + radix;
      } else {
        // Letters are located after the digits in ASCII. Therefore we
        // only check for the character code. The regexp above made already
        // sure that the string does not contain anything but digits or
        // letters.
        // "a".codeUnitAt(0) + (radix - 10) - 1;
        maxCharCode = (0x61 - 10 - 1) + radix;
      }
      assert(match[digitsIndex] is String);
      String digitsPart = JS<String>('!', '#[#]', match, digitsIndex);
      for (int i = 0; i < digitsPart.length; i++) {
        int characterCode = digitsPart.codeUnitAt(i) | 0x20;
        if (characterCode > maxCharCode) {
          return null;
        }
      }
    }
    // The above matching and checks ensures the source has at least one digits
    // and all digits are suitable for the radix, so parseInt cannot return NaN.
    return JS<int>('!', r'parseInt(#, #)', source, radix);
  }

  static double? parseDouble(@nullCheck String source) {
    // Notice that JS parseFloat accepts garbage at the end of the string.
    // Accept only:
    // - [+/-]NaN
    // - [+/-]Infinity
    // - a Dart double literal
    // We do allow leading or trailing whitespace.
    if (!JS(
      'bool',
      r'/^\s*[+-]?(?:Infinity|NaN|'
          r'(?:\.\d+|\d+(?:\.\d*)?)(?:[eE][+-]?\d+)?)\s*$/.test(#)',
      source,
    )) {
      return null;
    }
    var result = JS<double>('!', r'parseFloat(#)', source);
    if (result.isNaN) {
      var trimmed = source.trim();
      if (trimmed == 'NaN' || trimmed == '+NaN' || trimmed == '-NaN') {
        return result;
      }
      return null;
    }
    return result;
  }

  static bool? parseBool(
    @nullCheck String source,
    @nullCheck bool caseSensitive,
  ) {
    if (caseSensitive) {
      return JS('bool', r'# == "true" || # != "false" && null', source, source);
    }
    return _compareIgnoreCase(source, "true")
        ? true
        : _compareIgnoreCase(source, "false")
        ? false
        : null;
  }

  /// Compares a string against an ASCII lower-case letter-only string.
  ///
  /// Returns `true` if the [input] has the same length and same letters
  /// as [lowerCaseTarget], `false` if not.
  static bool _compareIgnoreCase(String input, String lowerCaseTarget) {
    if (input.length != lowerCaseTarget.length) return false;
    var delta = 0x20;
    for (var i = 0; i < input.length; i++) {
      delta |= input.codeUnitAt(i) ^ lowerCaseTarget.codeUnitAt(i);
    }
    return delta == 0x20;
  }

  static String stringSafeToString(String str) {
    return JS<String>('!', 'JSON.stringify(#)', str);
  }

  static String safeToString(obj) {
    if (obj == null || obj is num || obj is bool) {
      return obj.toString();
    }
    if (obj is String) {
      return stringSafeToString(obj);
    }
    if (obj is dart.RecordImpl) {
      return dart.recordSafeToString(obj);
    }
    return "Instance of '${dart.typeName(dart.getReifiedType(obj))}'";
  }

  /// `r"$".codeUnitAt(0)`
  static const int DOLLAR_CHAR_VALUE = 36;

  static int dateNow() => JS<int>('!', r'Date.now()');

  static void initTicker() {
    if (timerFrequency != 0) return;
    // Start with low-resolution. We overwrite the fields if we find better.
    timerFrequency = 1000;
    if (JS<bool>('!', 'typeof window == "undefined"')) return;
    var jsWindow = JS('var', 'window');
    if (jsWindow == null) return;
    var performance = JS('var', '#.performance', jsWindow);
    if (performance == null) return;
    if (JS<bool>('!', 'typeof #.now != "function"', performance)) return;
    timerFrequency = 1000000;
    timerTicks = () => (1000 * JS<num>('!', '#.now()', performance)).floor();
  }

  /// 0 frequency indicates the default uninitialized state.
  static int timerFrequency = 0;
  static int Function() timerTicks = dateNow; // Low-resolution version.

  static bool get isD8 {
    return JS(
      'bool',
      'typeof version == "function"'
          ' && typeof os == "object" && "system" in os',
    );
  }

  static bool get isJsshell {
    return JS(
      'bool',
      'typeof version == "function" && typeof system == "function"',
    );
  }

  static String currentUri() {
    // In a browser return self.location.href.
    if (JS<bool>('!', '!!#.location', dart.global_)) {
      return JS<String>('!', '#.location.href', dart.global_);
    }

    // TODO(vsm): Consider supporting properly in non-browser settings.
    return '';
  }

  // This is to avoid stack overflows due to very large argument arrays in
  // apply().  It fixes http://dartbug.com/6919
  @notNull
  static String _fromCharCodeApply(List<int> array) {
    const kMaxApply = 500;
    @nullCheck
    int end = array.length;
    if (end <= kMaxApply) {
      return JS<String>('!', r'String.fromCharCode.apply(null, #)', array);
    }
    String result = '';
    for (int i = 0; i < end; i += kMaxApply) {
      int chunkEnd = (i + kMaxApply < end) ? i + kMaxApply : end;
      result = JS(
        'String',
        r'# + String.fromCharCode.apply(null, #.slice(#, #))',
        result,
        array,
        i,
        chunkEnd,
      );
    }
    return result;
  }

  @notNull
  static String stringFromCodePoints(JSArray<int> codePoints) {
    List<int> a = <int>[];
    for (@nullCheck var i in codePoints) {
      if (i <= 0xffff) {
        a.add(i);
      } else if (i <= 0x10ffff) {
        a.add(0xd800 + ((((i - 0x10000) >> 10) & 0x3ff)));
        a.add(0xdc00 + (i & 0x3ff));
      } else {
        throw argumentErrorValue(i);
      }
    }
    return _fromCharCodeApply(a);
  }

  @notNull
  static String stringFromCharCodes(JSArray<int> charCodes) {
    for (@nullCheck var i in charCodes) {
      if (i < 0) throw argumentErrorValue(i);
      if (i > 0xffff) return stringFromCodePoints(charCodes);
    }
    return _fromCharCodeApply(charCodes);
  }

  // [start] and [end] are validated.
  @notNull
  static String stringFromNativeUint8List(
    NativeUint8List charCodes,
    @nullCheck int start,
    @nullCheck int end,
  ) {
    const kMaxApply = 500;
    if (end <= kMaxApply && start == 0 && end == charCodes.length) {
      return JS<String>('!', r'String.fromCharCode.apply(null, #)', charCodes);
    }
    String result = '';
    for (int i = start; i < end; i += kMaxApply) {
      int chunkEnd = (i + kMaxApply < end) ? i + kMaxApply : end;
      result = JS(
        'String',
        r'# + String.fromCharCode.apply(null, #.subarray(#, #))',
        result,
        charCodes,
        i,
        chunkEnd,
      );
    }
    return result;
  }

  @notNull
  static String stringFromCharCode(@nullCheck int charCode) {
    if (0 <= charCode) {
      if (charCode <= 0xffff) {
        return JS<String>('!', 'String.fromCharCode(#)', charCode);
      }
      if (charCode <= 0x10ffff) {
        var bits = charCode - 0x10000;
        var low = 0xDC00 | (bits & 0x3ff);
        var high = 0xD800 | (bits >> 10);
        return JS<String>('!', 'String.fromCharCode(#, #)', high, low);
      }
    }
    throw RangeError.range(charCode, 0, 0x10ffff);
  }

  static String flattenString(String str) {
    return JS<String>('!', "#.charCodeAt(0) == 0 ? # : #", str, str, str);
  }

  static String getTimeZoneName(DateTime receiver) {
    // Firefox and Chrome emit the timezone in parenthesis.
    // Example: "Wed May 16 2012 21:13:00 GMT+0200 (CEST)".
    // We extract this name using a regexp.
    var d = lazyAsJsDate(receiver);
    // In this method all calls to `exec()` include a single capture group and
    // it is only read if there is a match so a value will be present. To avoid
    // extra null checks or casts from dynamic we type the return type of
    // `exec()` to always contain non-nullable Strings.
    var match = JS<List<String>?>('', r'/\((.*)\)/.exec(#.toString())', d);
    if (match != null) return match[1];

    // Internet Explorer 10+ emits the zone name without parenthesis:
    // Example: Thu Oct 31 14:07:44 PDT 2013
    match = JS<List<String>?>(
      '',
      // Thu followed by a space.
      r'/^[A-Z,a-z]{3}\s'
          // Oct 31 followed by space.
          r'[A-Z,a-z]{3}\s\d+\s'
          // Time followed by a space.
          r'\d{2}:\d{2}:\d{2}\s'
          // The time zone name followed by a space.
          r'([A-Z]{3,5})\s'
          // The year.
          r'\d{4}$/'
          '.exec(#.toString())',
      d,
    );
    if (match != null) return match[1];

    // IE 9 and Opera don't provide the zone name. We fall back to emitting the
    // UTC/GMT offset.
    // Example (IE9): Wed Nov 20 09:51:00 UTC+0100 2013
    //       (Opera): Wed Nov 20 2013 11:03:38 GMT+0100
    match = JS<List<String>?>(
      '',
      r'/(?:GMT|UTC)[+-]\d{4}/.exec(#.toString())',
      d,
    );
    if (match != null) return match[0];
    return "";
  }

  static int getTimeZoneOffsetInSeconds(DateTime receiver) {
    // Note that JavaScript's Date and and Dart's DateTime disagree on the sign
    // of the offset. Subtract to avoid -0.0. The offset in minutes could
    // contain 'seconds' as fractional minutes.
    num offsetInMinutes = JS(
      'num',
      r'#.getTimezoneOffset()',
      lazyAsJsDate(receiver),
    );
    return (0 - offsetInMinutes * 60).toInt();
  }

  static int? valueFromDecomposedDate(
    @nullCheck int years,
    @nullCheck int month,
    @nullCheck int day,
    @nullCheck int hours,
    @nullCheck int minutes,
    @nullCheck int seconds,
    @nullCheck int milliseconds,
    @nullCheck int microseconds,
    @nullCheck bool isUtc,
  ) {
    final int MAX_MILLISECONDS_SINCE_EPOCH = 8640000000000000;
    var jsMonth = month - 1;
    // The JavaScript Date constructor 'corrects' year NN to 19NN. Sidestep that
    // correction by adjusting years out of that range and compensating with an
    // adjustment of months. This hack should not be sensitive to leap years but
    // use 400 just in case.
    if (0 <= years && years < 100) {
      years += 400;
      jsMonth -= 400 * 12;
    }
    // JavaScript `Date` does not handle microseconds, so ensure the provided
    // microseconds is in range [0..999].
    final remainder = microseconds % 1000;
    milliseconds += (microseconds - remainder) ~/ 1000;
    microseconds = remainder;
    int value;
    if (isUtc) {
      value = JS<int>(
        '!',
        r'Date.UTC(#, #, #, #, #, #, #)',
        years,
        jsMonth,
        day,
        hours,
        minutes,
        seconds,
        milliseconds,
      );
    } else {
      value = JS<int>(
        '!',
        r'new Date(#, #, #, #, #, #, #).valueOf()',
        years,
        jsMonth,
        day,
        hours,
        minutes,
        seconds,
        milliseconds,
      );
    }
    if (value.isNaN ||
        value < -MAX_MILLISECONDS_SINCE_EPOCH ||
        value > MAX_MILLISECONDS_SINCE_EPOCH ||
        value == MAX_MILLISECONDS_SINCE_EPOCH && microseconds != 0) {
      return null;
    }
    return value;
  }

  // Lazily keep a JS Date stored in the JS object.
  static lazyAsJsDate(DateTime receiver) {
    if (JS<bool>('!', r'#.date === (void 0)', receiver)) {
      JS(
        'void',
        r'#.date = new Date(#)',
        receiver,
        receiver.millisecondsSinceEpoch,
      );
    }
    return JS('var', r'#.date', receiver);
  }

  // The getters for date and time parts below add a positive integer to ensure
  // that the result is really an integer, because the JavaScript implementation
  // may return -0.0 instead of 0.

  static int getYear(DateTime receiver) {
    return (receiver.isUtc)
        ? JS<int>('!', r'(#.getUTCFullYear() + 0)', lazyAsJsDate(receiver))
        : JS<int>('!', r'(#.getFullYear() + 0)', lazyAsJsDate(receiver));
  }

  static int getMonth(DateTime receiver) {
    return (receiver.isUtc)
        ? JS<int>('!', r'#.getUTCMonth() + 1', lazyAsJsDate(receiver))
        : JS<int>('!', r'#.getMonth() + 1', lazyAsJsDate(receiver));
  }

  static int getDay(DateTime receiver) {
    return (receiver.isUtc)
        ? JS<int>('!', r'(#.getUTCDate() + 0)', lazyAsJsDate(receiver))
        : JS<int>('!', r'(#.getDate() + 0)', lazyAsJsDate(receiver));
  }

  static int getHours(DateTime receiver) {
    return (receiver.isUtc)
        ? JS<int>('!', r'(#.getUTCHours() + 0)', lazyAsJsDate(receiver))
        : JS<int>('!', r'(#.getHours() + 0)', lazyAsJsDate(receiver));
  }

  static int getMinutes(DateTime receiver) {
    return (receiver.isUtc)
        ? JS<int>('!', r'(#.getUTCMinutes() + 0)', lazyAsJsDate(receiver))
        : JS<int>('!', r'(#.getMinutes() + 0)', lazyAsJsDate(receiver));
  }

  static int getSeconds(DateTime receiver) {
    return (receiver.isUtc)
        ? JS<int>('!', r'(#.getUTCSeconds() + 0)', lazyAsJsDate(receiver))
        : JS<int>('!', r'(#.getSeconds() + 0)', lazyAsJsDate(receiver));
  }

  static int getMilliseconds(DateTime receiver) {
    return (receiver.isUtc)
        ? JS<int>('!', r'(#.getUTCMilliseconds() + 0)', lazyAsJsDate(receiver))
        : JS<int>('!', r'(#.getMilliseconds() + 0)', lazyAsJsDate(receiver));
  }

  static int getWeekday(DateTime receiver) {
    int weekday =
        (receiver.isUtc)
            ? JS<int>('!', r'#.getUTCDay() + 0', lazyAsJsDate(receiver))
            : JS<int>('!', r'#.getDay() + 0', lazyAsJsDate(receiver));
    // Adjust by one because JS weeks start on Sunday.
    return (weekday + 6) % 7 + 1;
  }

  static Object? getProperty(Object? object, Object key) {
    if (object == null || object is bool || object is num || object is String) {
      throw argumentErrorValue(object);
    }
    return JS('var', '#[#]', object, key);
  }

  static void setProperty(object, key, value) {
    if (object == null || object is bool || object is num || object is String) {
      throw argumentErrorValue(object);
    }
    JS('void', '#[#] = #', object, key, value);
  }
}

/// Diagnoses an indexing error. Returns the ArgumentError or RangeError that
/// describes the problem.
Error diagnoseIndexError(indexable, int index) {
  int length = indexable.length;
  // The following returns the same error that would be thrown by calling
  // [IndexError.check] with no optional parameters
  // provided.
  if (index < 0 || index >= length) {
    return IndexError.withLength(
      index,
      length,
      indexable: indexable,
      name: 'index',
    );
  }
  // The above should always match, but if it does not, use the following.
  return RangeError.value(index, 'index');
}

/// Diagnoses a range error. Returns the ArgumentError or RangeError that
/// describes the problem.
Error diagnoseRangeError(int? start, int? end, int length) {
  if (start == null) {
    return ArgumentError.value(start, 'start');
  }
  if (start < 0 || start > length) {
    return RangeError.range(start, 0, length, 'start');
  }
  if (end != null) {
    if (end < start || end > length) {
      return RangeError.range(end, start, length, 'end');
    }
  }
  // The above should always match, but if it does not, use the following.
  return ArgumentError.value(end, "end");
}

@notNull
int stringLastIndexOfUnchecked(receiver, element, start) =>
    JS<int>('!', r'#.lastIndexOf(#, #)', receiver, element, start);

/// 'factory' for constructing ArgumentError.value to keep the call sites small.
ArgumentError argumentErrorValue(object) {
  return ArgumentError.value(object);
}

void throwArgumentErrorValue(value) {
  throw argumentErrorValue(value);
}

checkInt(value) {
  if (value is! int) throw argumentErrorValue(value);
  return value;
}

throwRuntimeError(message) {
  throw RuntimeError(message);
}

throwConcurrentModificationError(collection) {
  throw ConcurrentModificationError(collection);
}

class JsNoSuchMethodError extends Error implements NoSuchMethodError {
  final String? _message;
  final String? _method;
  final String? _receiver;

  JsNoSuchMethodError(this._message, match)
    : _method = match == null ? null : JS('String|Null', '#.method', match),
      _receiver = match == null ? null : JS('String|Null', '#.receiver', match);

  String toString() {
    if (_method == null) return 'NoSuchMethodError: $_message';
    if (_receiver == null) {
      return "NoSuchMethodError: method not found: '$_method' ($_message)";
    }
    return "NoSuchMethodError: "
        "method not found: '$_method' on '$_receiver' ($_message)";
  }
}

class UnknownJsTypeError extends Error {
  final String _message;

  UnknownJsTypeError(this._message);

  String toString() => _message.isEmpty ? 'Error' : 'Error: $_message';
}

/// Called by generated code to build a map literal. [keyValuePairs] is
/// a list of key, value, key, value, ..., etc.
fillLiteralMap(keyValuePairs, Map result) {
  // TODO(johnniwinther): Use JSArray to optimize this code instead of calling
  // [getLength] and [getIndex].
  int index = 0;
  int length = getLength(keyValuePairs);
  while (index < length) {
    var key = getIndex(keyValuePairs, index++);
    var value = getIndex(keyValuePairs, index++);
    result[key] = value;
  }
  return result;
}

bool jsHasOwnProperty(jsObject, String property) {
  return JS<bool>('!', r'#.hasOwnProperty(#)', jsObject, property);
}

jsPropertyAccess(jsObject, String property) {
  return JS('var', r'#[#]', jsObject, property);
}

/// A metadata annotation describing the types instantiated by a native element.
///
/// The annotation is valid on a native method and a field of a native class.
///
/// By default, a field of a native class is seen as an instantiation point for
/// all native classes that are a subtype of the field's type, and a native
/// method is seen as an instantiation point fo all native classes that are a
/// subtype of the method's return type, or the argument types of the declared
/// type of the method's callback parameter.
///
/// An @[Creates] annotation overrides the default set of instantiated types.
/// If one or more @[Creates] annotations are present, the type of the native
/// element is ignored, and the union of @[Creates] annotations is used instead.
/// The names in the strings are resolved and the program will fail to compile
/// with dart2js if they do not name types.
///
/// The argument to [Creates] is a string.  The string is parsed as the names of
/// one or more types, separated by vertical bars `|`.  There are some special
/// names:
///
/// * `=Object`. This means 'exactly Object', which is a plain JavaScript object
///   with properties and none of the subtypes of Object.
///
/// Example: we may know that a method always returns a specific implementation:
///
///     @Creates('_NodeList')
///     List<Node> getElementsByTagName(String tag) native;
///
/// Useful trick: A method can be marked as not instantiating any native classes
/// with the annotation `@Creates('Null')`.  This is useful for fields on native
/// classes that are used only in Dart code.
///
///     @Creates('Null')
///     var _cachedFoo;
class Creates {
  final String types;
  const Creates(this.types);
}

/// A metadata annotation describing the types returned or yielded by a native
/// element.
///
/// The annotation is valid on a native method and a field of a native class.
///
/// By default, a native method or field is seen as returning or yielding all
/// subtypes if the method return type or field type.  This annotation allows a
/// more precise set of types to be specified.
///
/// See [Creates] for the syntax of the argument.
///
/// Example: IndexedDB keys are numbers, strings and JavaScript Arrays of keys.
///
///     @Returns('String|num|JSExtendableArray')
///     dynamic key;
///
///     // Equivalent:
///     @Returns('String') @Returns('num') @Returns('JSExtendableArray')
///     dynamic key;
class Returns {
  final String types;
  const Returns(this.types);
}

/// A metadata annotation placed on native methods and fields of native classes
/// to specify the JavaScript name.
///
/// This example declares a Dart field + getter + setter called `$dom_title`
/// that corresponds to the JavaScript property `title`.
///
///     class Document native "*Foo" {
///       @JSName('title')
///       String $dom_title;
///     }
class JSName {
  final String name;
  const JSName(this.name);
}

/// Special interface recognized by the compiler and implemented by DOM
/// objects that support integer indexing. This interface is not
/// visible to anyone, and is only injected into special libraries.
abstract class JavaScriptIndexingBehavior<E> extends JSMutableIndexable<E> {}

/// Thrown by type assertions that fail.
class TypeErrorImpl extends Error implements TypeError {
  final String _message;

  TypeErrorImpl(this._message);

  String toString() => _message;
}

/// Error thrown when a runtime error occurs.
class RuntimeError extends Error {
  final message;
  RuntimeError(this.message);
  String toString() => "RuntimeError: $message";
}

class DeferredNotLoadedError extends Error implements NoSuchMethodError {
  String enclosingLibrary;
  String importPrefix;

  DeferredNotLoadedError(this.enclosingLibrary, this.importPrefix);

  String toString() {
    return 'Deferred import $importPrefix (from $enclosingLibrary) was not loaded.';
  }
}

/// Error thrown by DDC when an `assert()` fails (with or without a message).
class AssertionErrorImpl extends AssertionError {
  final String? _fileUri;
  final int? _line;
  final int? _column;
  final String? _conditionSource;

  AssertionErrorImpl(
    Object? message, [
    this._fileUri,
    this._line,
    this._column,
    this._conditionSource,
  ]) : super(message);

  String toString() {
    var failureMessage = "";
    if (_fileUri != null &&
        _line != null &&
        _column != null &&
        _conditionSource != null) {
      failureMessage += "$_fileUri:${_line}:${_column}\n$_conditionSource\n";
    }
    failureMessage +=
        message != null ? Error.safeToString(message) : "is not true";

    return "Assertion failed: $failureMessage";
  }
}

/// Creates a random number with 64 bits of randomness.
///
/// This will be truncated to the 53 bits available in a double.
int random64() {
  // TODO(lrn): Use a secure random source.
  int int32a = JS("int", "(Math.random() * 0x100000000) >>> 0");
  int int32b = JS("int", "(Math.random() * 0x100000000) >>> 0");
  return int32a + int32b * 0x100000000;
}

class BooleanConversionAssertionError extends AssertionError {
  toString() => 'Failed assertion: boolean expression must not be null';
}

// Hook to register new global object.  This is invoked from dart:html
// whenever a new window is accessed for the first time.
void registerGlobalObject(object) {
  try {
    if (dart.polyfill(object)) {
      dart.applyAllExtensions(object);
    }
  } catch (e) {
    // This may fail due to cross-origin errors.  In that case, we shouldn't
    // need to polyfill as we can't get objects from that frame.

    // TODO(vsm): Detect this more robustly - ideally before we try to polyfill.
  }
}

/// Expose browser JS classes.
void applyExtension(name, nativeObject) {
  dart.applyExtension(name, nativeObject);
}

/// Hook to apply extensions on native JS classes defined in a native unit test.
void applyTestExtensions(List<String> names) {
  names.forEach(dart.applyExtensionForTesting);
}

/// Used internally by DDC to map ES6 symbols to Dart.
class PrivateSymbol implements Symbol {
  // TODO(jmesserly): could also get this off the native symbol instead of
  // storing it. Mirrors already does this conversion.
  final String _name;
  final Object _nativeSymbol;

  const PrivateSymbol(this._name, this._nativeSymbol);

  static String getName(Symbol symbol) => (symbol as PrivateSymbol)._name;

  static Object? getNativeSymbol(Symbol symbol) {
    if (symbol is PrivateSymbol) return symbol._nativeSymbol;
    return null;
  }

  bool operator ==(other) =>
      other is PrivateSymbol &&
      _name == other._name &&
      identical(_nativeSymbol, other._nativeSymbol);

  get hashCode => _name.hashCode;

  // TODO(jmesserly): is this equivalent to _nativeSymbol toString?
  toString() => 'Symbol("$_name")';
}

/// Asserts that if [value] is a function, it is a JavaScript function or has
/// been wrapped by [allowInterop].
///
/// This function does not recurse if [value] is a collection.
void assertInterop(Object? value) {
  if (value is Function) dart.assertInterop(value);
}

/// Like [assertInterop], except iterates over a list of arguments
/// non-recursively.
void assertInteropArgs(List<Object?> args) => args.forEach(assertInterop);

/// Wraps the given [callback] within the current Zone.
void Function(T)? wrapZoneUnaryCallback<T>(void Function(T)? callback) {
  // For performance reasons avoid wrapping if we are in the root zone.
  if (Zone.current == Zone.root) return callback;
  if (callback == null) return null;
  return Zone.current.bindUnaryCallbackGuarded(callback);
}

/// Returns a JavaScript predicate that tests if the argument is a record with
/// the given shape and fields types.
///
/// Only called from the `dart:_rti` library but requires specific knowledge of
/// the record representation in DDC. There is a duplicate version of this
/// method in the dart2js version of this library.
///
/// The shape is determined by the number of fields and the [partialShapeTag].
/// [fieldRtis] contains the Rti type objects for each field in order of
/// positionals followed by the sorted named elements.
Object? createRecordTypePredicate(String partialShapeTag, JSArray fieldRtis) {
  var shapeKey = JS<String>(
    '!',
    '#.length + ";" + #',
    fieldRtis,
    partialShapeTag,
  );
  return (obj) {
    return JS<bool>(
          '!',
          '# instanceof #',
          obj,
          JS_CLASS_REF(dart.RecordImpl),
        ) &&
        JS<dart.Shape>('!', '#[#]', obj, dart.shapeProperty) ==
            JS<dart.Shape?>('', '#.get(#)', dart.shapes, shapeKey) &&
        rti.pairwiseIsTest(
          fieldRtis,
          JS<JSArray>('!', '#[#]', obj, dart.valuesProperty),
        );
  };
}

/// Returns the Rti for the provided [record].
///
/// Is called from the `dart:_rti` library but requires specific knowledge of
/// the record representation in DDC. There is a duplicate version of this
/// method in the dart2js version of this library.
///
/// Calls [rti.evaluateRtiForRecord] with components of the [record].
rti.Rti getRtiForRecord(Object? record) {
  var recordObj = JS<dart.RecordImpl>('!', '#', record);
  var recipeBuffer = StringBuffer('+');
  var shape = JS<dart.Shape>('!', '#[#]', recordObj, dart.shapeProperty);
  var values = JS<JSArray>('!', '#[#]', recordObj, dart.valuesProperty);
  var named = shape.named;
  if (named != null) recipeBuffer.writeAll(named, ',');
  recipeBuffer.write('(');
  var elementCount = values.length;
  recipeBuffer.writeAll([for (var i = 1; i <= elementCount; i++) i], ',');
  recipeBuffer.write(')');

  return rti.evaluateRtiForRecord(recipeBuffer.toString(), values);
}

/// A marker interface for classes with 'trustworthy' implementations of `get
/// runtimeType`.
///
/// Generally, overrides of `get runtimeType` are not used in displaying the
/// types of irritants in TypeErrors or computing the structural `runtimeType`
/// of records. Instead the Rti (aka 'true') type is used.
///
/// The 'true' type is sometimes confusing because it shows implementation
/// details, e.g. the true type of `42` is `JSInt` and `2.1` is `JSNumNotInt`.
///
/// For a limited number of implementation classes we tell a 'white lie' that
/// the value is of another type, e.g. that `42` is an `int` and `2.1` is
/// `double`. This is achieved by overriding `get runtimeType` to return the
/// desired type, and marking the implementation class type with `implements
/// [TrustedGetRuntimeType]`.
///
/// [TrustedGetRuntimeType] is not exposed outside the `dart:` libraries so
/// users cannot tell lies.
///
/// The `Type` returned by a trusted `get runtimeType` must be an instance of
/// the system `Type`, which is guaranteed by using a type literal. Type
/// literals can be generic and dependent on type variables, e.g. `List<E>`.
///
/// Care needs to taken to ensure that the runtime does not get caught telling
/// lies. Generally, a class's `runtimeType` lies by returning an abstract
/// supertype of the class.  Since both the the marker interface and `get
/// runtimeType` are inherited, there should be no way in which a user can
/// extend the class or implement interface of the class.
// TODO(48585): Move this class back to the dart:_rti library when old DDC
// runtime type system has been removed.
abstract class TrustedGetRuntimeType {}

/// Wraps the JavaScript `Object.getPrototypeOf()` method returning the
/// `__proto__` of [obj].
///
/// This method is equivalent to:
///
///    JS<Object?>('', 'Object.getPrototypeOf(#)', obj);
///
/// but the code is generated by the compiler directly (a low-tech way of
/// inlining).
///
/// This helper should always be used in place of `JS('', '#.__proto__', obj)`
/// to avoid prototype pollution issues.
/// See: https://github.com/tc39/proposal-symbol-proto
external Object? jsObjectGetPrototypeOf(@notNull Object obj);

/// Wraps the JavaScript `Object.setPrototypeOf()` method setting the
/// `__proto__` of [obj] to [prototype] and returning [obj].
///
/// This method is equivalent to:
///
///    JS<Object>('!', 'Object.setPrototypeOf(#, #)', obj, prototype);
///
/// but the code is generated by the compiler directly (a low-tech way of
/// inlining).
///
/// This helper should always be used in place of
/// `JS('', '#.__proto__ = #', obj, prototype)` to avoid prototype
/// pollution issues.
/// See: https://github.com/tc39/proposal-symbol-proto
@notNull
external Object jsObjectSetPrototypeOf(@notNull Object obj, Object? prototype);

/// The global context that "static interop" members use for namespaces.
///
/// For example, an interop library with no library-level `@JS` annotation and a
/// top-level external member named or renamed to 'foo' will lower a call to
/// that member as `<staticInteropGlobalContext>.foo`. The same applies for any
/// external constructors or class/extension type static members.
///
/// If the library does have a `@JS` annotation with a value, the call then gets
/// lowered to `<staticInteropGlobalContext>.<libraryJSAnnotationValue>.foo`.
///
/// To see which members get lowered with this, see the transformation in
/// `pkg/_js_interop_checks/lib/src/js_util_optimizer.dart`.
///
/// This should match the global context that non-static interop members use.
/// Note that this is external. We could implement it here, but DDC will not
/// inline the call so this will be an extra level of indirection. DDC manually
/// inlines this method in the compiler instead.
external Object get staticInteropGlobalContext;

/// Return a fresh object literal.
T createObjectLiteral<T>() => JS('PlainJavaScriptObject', '{}');
