Sky Event Model

import 'dart:collection';
import 'dart:async';

class ExceptionAndStackTrace<T> {
  const ExceptionAndStackTrace(this.exception, this.stackTrace);
  final T exception;
  final StackTrace stackTrace;
}

class ExceptionListException<T> extends IterableMixin<ExceptionAndStackTrace<T>> implements Exception {
  List<ExceptionAndStackTrace<T>> _exceptions;
  void add(T exception, [StackTrace stackTrace = null]) {
    if (_exceptions == null)
      _exceptions = new List<ExceptionAndStackTrace<T>>();
    _exceptions.add(new ExceptionAndStackTrace<T>(exception, stackTrace));
  }
  int get length => _exceptions == null ? 0 : _exceptions.length;
  Iterator<ExceptionAndStackTrace<T>> get iterator => _exceptions.iterator;
}

typedef bool Filter<T>(T t);
typedef void Handler<T>(T t);

class DispatcherController<T> {
  DispatcherController() : dispatcher = new Dispatcher<T>();
  final Dispatcher<T> dispatcher;
  void add(T data) => dispatcher._add(data);
}

class Dispatcher<T> {
  List<Pair<Handler, ZoneUnaryCallback>> _listeners;
  void listen(Handler<T> handler) {
    // you should not throw out of this handler
    if (_listeners == null)
      _listeners = new List<Pair<Handler, ZoneUnaryCallback>>();
    _listeners.add(new Pair<Handler, ZoneUnaryCallback>(handler, Zone.current.bindUnaryCallback(handler)));
  }
  bool unlisten(Handler<T> handler) {
    if (_listeners == null)
      return false;
    var target = _listeners.lastWhere((v) => v.a == handler, orElse: () => null);
    if (target == null)
      return false;
    _listeners.removeAt(_listeners.lastIndexOf(target));
    return true;
  }
  void _add(T data) {
    if (_listeners == null)
      return;
    ExceptionListException exceptions = new ExceptionListException();
    // we make a copy of the list here so that the listeners can
    // mutate our list without worry
    _listeners.toList().forEach((Pair<Handler, ZoneUnaryCallback> item) {
      try {
        item.b(data);
      } catch (exception, stackTrace) {
        exceptions.add(exception, stackTrace);
      }
    });
    if (exceptions.length > 0)
      throw exceptions;
  }

  Dispatcher<T> where(Filter<T> filter) => new WhereDispatcher<T>(this, filter);

  Dispatcher<T> until(Filter<T> filter) {
    var subdispatcher = new Dispatcher<T>();
    Handler handler;
    handler = (T data) {
      if (filter(data))
        unlisten(handler);
      else
        subdispatcher._add(data);
    };
    listen(handler);
    return subdispatcher;
  }

  Future<T> firstWhere(Filter<T> filter) {
    Completer completer = new Completer();
    Handler handler;
    handler = (T data) {
      if (filter(data)) {
        completer.complete(data);
        unlisten(handler);
      }
    };
    listen(handler);
    return completer.future;
  }
}

class WhereDispatcher<T> extends Dispatcher {
  WhereDispatcher(this.parent, this.filter) : super();
  Dispatcher parent;
  Filter filter;

  void listen(Handler<T> handler) {
    if (_listeners == null || _listeners.length == 0)
      parent.listen(_handler);
    super.listen(handler);
  }
  bool unlisten(Handler<T> handler) {
    var result = super.unlisten(handler);
    if (result && _listeners.length == 0)
      parent.unlisten(_handler);
    return result;
  }
  void _handler(T data) {
    if (filter(data))
      _add(data);
  }
}

abstract class Event<ReturnType> {
  Event() { init(); }
  void init() { }

  bool get bubbles;

  EventTarget _target;
  EventTarget get target => _target;

  EventTarget _currentTarget;
  EventTarget get currentTarget => _currentTarget;

  bool handled; // precise semantics depend on the event type, but in general, set this when you set result
  ReturnType result;

  bool resultIsCompatible(dynamic candidate) => candidate is ReturnType;

  // TODO(ianh): abstract API for doing things at shadow tree boundaries 
  // TODO(ianh): do events get blocked at scope boundaries, e.g. focus events when both sides are in the scope?
  // TODO(ianh): do events get retargetted, e.g. focus when leaving a custom element?
  // e.g. sent from inside a shadow tree, when exiting the shadow tree, focus event should:
  //  - disappear if we're moving from one to another element
  //  - be targetted if it's going to another node in a different scope
}

class EventTarget {
  EventTarget() : _eventsController = new DispatcherController<Event>();

  Dispatcher get events => _eventsController.dispatcher;
  EventTarget get parentNode;

  List<EventTarget> getEventDispatchChain() {
    if (this.parentNode == null) {
      return [this];
    } else {
      var result = this.parentNode.getEventDispatchChain();
      result.insert(0, this);
      return result;
    }
  }

  final DispatcherController _eventsController;

  dynamic dispatchEvent(Event event, { dynamic defaultResult: null }) { // O(N*M) where N is the length of the chain and M is the average number of listeners per link in the chain
    // note: this will throw an ExceptionListException<ExceptionListException> if any of the listeners threw
    assert(event != null); // event must be non-null
    event.handled = false;
    assert(event.resultIsCompatible(defaultResult));
    event.result = defaultResult;
    event._target = this;
    var chain;
    if (event.bubbles)
      chain = this.getEventDispatchChain();
    else
      chain = [this];
    var exceptions = new ExceptionListException<ExceptionListException>();
    for (var link in chain) {
      try {
        link._dispatchEventLocally(event);
      } on ExceptionListException catch (e) {
        exceptions.add(e);
      }
    }
    if (exceptions.length > 0)
      throw exceptions;
    return event.result;
  }

  void _dispatchEventLocally(Event event) {
    event._currentTarget = this;
    _eventsController.add(event);
  }
}