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

// ignore: deprecated_member_use
import 'dart:js_util' as js_util;

import 'package:js/js.dart';

@JS()
@staticInterop
class Window extends EventTarget {}

extension WindowExtension on Window {
  @pragma('dart2js:as:trust')
  Window get parent => js_util.getProperty<dynamic>(this, 'parent') as Window;
  external Location get location;
  Console get console => js_util.getProperty(this, 'console') as Console;
  CSSStyleDeclaration? getComputedStyle(Element elt, [String? pseudoElt]) =>
      js_util.callMethod(this, 'getComputedStyle', <Object>[
        elt,
        if (pseudoElt != null) pseudoElt
      ]) as CSSStyleDeclaration?;
  external Navigator get navigator;
  void postMessage(Object message, String targetOrigin,
          [List<MessagePort>? messagePorts]) =>
      js_util.callMethod(this, 'postMessage', <Object?>[
        js_util.jsify(message),
        targetOrigin,
        if (messagePorts != null) js_util.jsify(messagePorts)
      ]);
}

@JS('window')
external Window get window;

@JS()
@staticInterop
class Console {}

extension ConsoleExtension on Console {
  external void log(Object? object);
  external void warn(Object? object);
}

@JS()
@staticInterop
class Document extends Node {}

extension DocumentExtension on Document {
  external Element? querySelector(String selectors);
  Element createElement(String name, [Object? options]) => js_util.callMethod(
          this, 'createElement', <Object>[name, if (options != null) options])
      as Element;
}

@JS()
@staticInterop
class HTMLDocument extends Document {}

extension HTMLDocumentExtension on HTMLDocument {
  external HTMLBodyElement? get body;
  external String? get title;
}

@JS('document')
external HTMLDocument get document;

@JS()
@staticInterop
class Navigator {}

extension NavigatorExtension on Navigator {
  external String get userAgent;
}

@JS()
@staticInterop
class Element extends Node {}

extension DomElementExtension on Element {
  external DomTokenList get classList;
}

@JS()
@staticInterop
class HTMLElement extends Element {}

@JS()
@staticInterop
class HTMLBodyElement extends HTMLElement {}

@JS()
@staticInterop
class Node extends EventTarget {}

extension NodeExtension on Node {
  external Node appendChild(Node node);
  void remove() {
    if (parentNode != null) {
      final parent = parentNode!;
      parent.removeChild(this);
    }
  }

  external Node removeChild(Node child);
  external Node? get parentNode;
}

@JS()
@staticInterop
class EventTarget {}

extension EventTargetExtension on EventTarget {
  void addEventListener(String type, EventListener? listener,
      [bool? useCapture]) {
    if (listener != null) {
      js_util.callMethod<void>(this, 'addEventListener',
          <Object>[type, listener, if (useCapture != null) useCapture]);
    }
  }

  void removeEventListener(String type, EventListener? listener,
      [bool? useCapture]) {
    if (listener != null) {
      js_util.callMethod<void>(this, 'removeEventListener',
          <Object>[type, listener, if (useCapture != null) useCapture]);
    }
  }
}

typedef EventListener = void Function(Event event);

@JS()
@staticInterop
class Event {}

extension EventExtension on Event {
  external void stopPropagation();
}

@JS()
@staticInterop
class MessageEvent extends Event {}

extension MessageEventExtension on MessageEvent {
  dynamic get data => js_util.dartify(js_util.getProperty(this, 'data'));
  external String get origin;
  List<MessagePort> get ports =>
      js_util.getProperty<List>(this, 'ports').cast<MessagePort>();

  /// The source may be a `WindowProxy`, a `MessagePort`, or a `ServiceWorker`.
  ///
  /// When a message is sent from an iframe through `window.parent.postMessage`
  /// the source will be a `WindowProxy` which has the same methods as [Window].
  @pragma('dart2js:as:trust')
  MessageEventSource get source =>
      js_util.getProperty<dynamic>(this, 'source') as MessageEventSource;
}

@JS()
@staticInterop
class MessageEventSource {}

extension MessageEventSourceExtension on MessageEventSource {
  @pragma('dart2js:as:trust')
  MessageEventSourceLocation? get location =>
      js_util.getProperty<dynamic>(this, 'location')
          as MessageEventSourceLocation;
}

@JS()
@staticInterop
class MessageEventSourceLocation {}

extension MessageEventSourceLocationExtension on MessageEventSourceLocation {
  external String? get href;
}

@JS()
@staticInterop
class Location {}

extension LocationExtension on Location {
  external String get href;
  external String get origin;
}

@JS()
@staticInterop
class MessagePort extends EventTarget {}

extension MessagePortExtension on MessagePort {
  void postMessage(Object? message) => js_util.callMethod(this, 'postMessage',
      <Object>[if (message != null) js_util.jsify(message) as Object]);
  external void start();
}

@JS()
@staticInterop
class CSSStyleDeclaration {}

@JS()
@staticInterop
class HTMLScriptElement extends HTMLElement {}

extension HTMLScriptElementExtension on HTMLScriptElement {
  external set src(String value);
}

HTMLScriptElement createHTMLScriptElement() =>
    document.createElement('script') as HTMLScriptElement;

@JS()
@staticInterop
class DomTokenList {}

extension DomTokenListExtension on DomTokenList {
  external void add(String value);
  external void remove(String value);
  external bool contains(String token);
}

@JS()
@staticInterop
class HTMLIFrameElement extends HTMLElement {}

extension HTMLIFrameElementExtension on HTMLIFrameElement {
  external String? get src;
  external set src(String? value);
  external Window get contentWindow;
}

HTMLIFrameElement createHTMLIFrameElement() =>
    document.createElement('iframe') as HTMLIFrameElement;

@JS()
@staticInterop
class WebSocket extends EventTarget {}

extension WebSocketExtension on WebSocket {
  external void send(Object? data);
}

WebSocket createWebSocket(String url) =>
    _callConstructor('WebSocket', <Object>[url])! as WebSocket;

@JS()
@staticInterop
class MessageChannel {}

extension MessageChannelExtension on MessageChannel {
  external MessagePort get port1;
  external MessagePort get port2;
}

MessageChannel createMessageChannel() =>
    _callConstructor('MessageChannel', <Object>[])! as MessageChannel;

Object? _findConstructor(String constructorName) =>
    js_util.getProperty(window, constructorName);

Object? _callConstructor(String constructorName, List<Object?> args) {
  final constructor = _findConstructor(constructorName);
  if (constructor == null) {
    return null;
  }
  return js_util.callConstructor(constructor, args);
}

class Subscription {
  final String type;
  final EventTarget target;
  final EventListener listener;

  Subscription(this.target, this.type, this.listener) {
    target.addEventListener(type, listener);
  }

  void cancel() => target.removeEventListener(type, listener);
}
