// Copyright 2021 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'dart:js_util';

import 'package:js/js.dart';
import 'package:js/js_util.dart';

import 'utils.dart';
import 'utils.dart' as utils;

@JS("util")
external Object? get _util;

@JS("util.inspect.custom")
external Symbol get _inspectSymbol;

@JS("Object.getPrototypeOf")
external Object? _getPrototypeOf(Object object);

@JS("Object.setPrototypeOf")
external void _setPrototypeOf(Object object, Object prototype);

@JS("Object.create")
external Object _create(Object object);

/// The JavaScript constructor function that operates as a class.
///
/// We use this to perform reflection operations.
@JS()
@anonymous
class JSClass {
  external Object get prototype;
  external String get name;
}

/// Returns the reflected [JSClass] for a given object.
JSClass getJSClass(Object object) =>
    getProperty(object, "constructor") as JSClass;

/// Creates a JS class with the given [name] and [constructor].
///
/// The [constructor] should take an initial `self` parameter, representing the
/// object being constructed.
JSClass createJSClass(String name, Function constructor) =>
    allowInteropCaptureThisNamed(name, constructor) as JSClass;

/// Extension methods to make working reflectively with JS classes easier and
/// more readable.
extension JSClassExtension on JSClass {
  /// Constructs an instance of this class.
  Object construct(List<Object?> arguments) =>
      callConstructor(this, arguments) as Object;

  /// Sets a new name for this class.
  void setName(String name) {
    utils.defineGetter(this, 'name', value: name);
  }

  /// Returns this class's superclass.
  JSClass get superclass =>
      getProperty(_getPrototypeOf(prototype)!, 'constructor') as JSClass;

  /// Injects [superclass] as this class's immediate superclass, otherwise
  /// preserving the inheritance chain.
  void injectSuperclass(JSClass superclass) {
    _setPrototypeOf(superclass.prototype, this.superclass.prototype);
    _setPrototypeOf(prototype, _create(superclass.prototype));
  }

  /// Sets the custom inspect logic for this class to [body].
  void setCustomInspect(String inspect(Object self)) {
    if (_util == null) return;
    setProperty(
      prototype,
      _inspectSymbol,
      allowInteropCaptureThis((Object self, _, __, [___]) => inspect(self)),
    );
  }

  /// Defines a static method with the given [name] and [body].
  void defineStaticMethod(String name, Function body) {
    setProperty(this, name, allowInteropNamed(name, body));
  }

  /// A shorthand for calling [defineStaticMethod] multiple times.
  void defineStaticMethods(Map<String, Function> methods) {
    methods.forEach(defineStaticMethod);
  }

  /// Defines a method with the given [name] and [body].
  ///
  /// The [body] should take an initial `self` parameter, representing the
  /// instance on which the method is being called.
  void defineMethod(String name, Function body) {
    setProperty(prototype, name, allowInteropCaptureThisNamed(name, body));
  }

  /// A shorthand for calling [defineMethod] multiple times.
  void defineMethods(Map<String, Function> methods) {
    methods.forEach(defineMethod);
  }

  /// Defines a getter with the given [name] and [body].
  ///
  /// The [body] should take an initial `self` parameter, representing the
  /// instance on which the method is being called.
  void defineGetter(String name, Function body) {
    utils.defineGetter(prototype, name, get: body);
  }

  /// A shorthand for calling [defineGetter] multiple times.
  void defineGetters(Map<String, Function> getters) {
    getters.forEach(defineGetter);
  }
}
