library testability.browser_testability;

import 'package:angular2/core.dart';
import 'package:angular2/platform/common_dom.dart';

import 'dart:html';
import 'dart:js' as js;

// Work around http://dartbug.com/17752, copied from
// https://github.com/angular/angular.dart/blob/master/lib/introspection.dart
// Proxies a Dart function that accepts up to 10 parameters.
js.JsFunction _jsFunction(Function fn) {
  const Object X = __varargSentinel;
  return new js.JsFunction.withThis((thisArg,
      [o1 = X,
      o2 = X,
      o3 = X,
      o4 = X,
      o5 = X,
      o6 = X,
      o7 = X,
      o8 = X,
      o9 = X,
      o10 = X]) {
    return __invokeFn(fn, o1, o2, o3, o4, o5, o6, o7, o8, o9, o10);
  });
}

const Object __varargSentinel = const Object();

__invokeFn(fn, o1, o2, o3, o4, o5, o6, o7, o8, o9, o10) {
  var args = [o1, o2, o3, o4, o5, o6, o7, o8, o9, o10];
  while (args.length > 0 && identical(args.last, __varargSentinel)) {
    args.removeLast();
  }
  return _jsify(Function.apply(fn, args));
}

// Helper function to JSify a Dart object.  While this is *required* to JSify
// the result of a scope.eval(), other uses are not required and are used to
// work around http://dartbug.com/17752 in a convenient way (that bug affects
// dart2js in checked mode.)
_jsify(var obj) {
  if (obj == null || obj is js.JsObject) {
    return obj;
  }
  if (obj is _JsObjectProxyable) {
    return obj._toJsObject();
  }
  if (obj is Function) {
    return _jsFunction(obj);
  }
  if ((obj is Map) || (obj is Iterable)) {
    var mappedObj = (obj is Map)
        ? new Map.fromIterables(obj.keys, obj.values.map(_jsify))
        : obj.map(_jsify);
    if (obj is List) {
      return new js.JsArray.from(mappedObj);
    } else {
      return new js.JsObject.jsify(mappedObj);
    }
  }
  return obj;
}

abstract class _JsObjectProxyable {
  js.JsObject _toJsObject();
}

class PublicTestability implements _JsObjectProxyable {
  Testability _testability;
  PublicTestability(Testability testability) {
    this._testability = testability;
  }

  bool isStable() {
    return this._testability.isStable();
  }

  whenStable(Function callback) {
    return this._testability.whenStable(callback);
  }

  findBindings(Element elem, String binding, bool exactMatch) {
    return this._testability.findBindings(elem, binding, exactMatch);
  }

  js.JsObject _toJsObject() {
    return _jsify({
      'findBindings': (bindingString, [exactMatch, allowNonElementNodes]) =>
          findBindings(bindingString, exactMatch, allowNonElementNodes),
      'isStable': () => isStable(),
      'whenStable': (callback) => whenStable((didWork) => callback.apply([didWork]))
    })..['_dart_'] = this;
  }
}

class BrowserGetTestability implements GetTestability {
  const BrowserGetTestability();

  static init() {
    setTestabilityGetter(const BrowserGetTestability());
  }

  void addToWindow(TestabilityRegistry registry) {
    var jsRegistry = js.context['ngTestabilityRegistries'];
    if (jsRegistry == null) {
      js.context['ngTestabilityRegistries'] = jsRegistry = new js.JsArray();
      js.context['getAngularTestability'] =
          _jsify((Element elem, [bool findInAncestors = true]) {
            var registry = js.context['ngTestabilityRegistries'];
            for (int i = 0; i < registry.length; i++) {
              var result = registry[i]
                  .callMethod('getAngularTestability', [elem, findInAncestors]);
              if (result != null) return result;
            }
            throw 'Could not find testability for element.';
          });
      var getAllAngularTestabilities = () {
        var registry = js.context['ngTestabilityRegistries'];
        var result = [];
        for (int i = 0; i < registry.length; i++) {
          var testabilities =
              registry[i].callMethod('getAllAngularTestabilities');
          if (testabilities != null) result.addAll(testabilities);
        }
        return _jsify(result);
      };
      js.context['getAllAngularTestabilities'] = 
          _jsify(getAllAngularTestabilities);

      var whenAllStable = _jsify((callback) {
        var testabilities = getAllAngularTestabilities();
        var count = testabilities.length;
        var didWork = false;
        var decrement = _jsify((bool didWork_) {
          didWork = didWork || didWork_;
          count--;
          if (count == 0) {
            callback.apply([didWork]);
          }
        });
        testabilities.forEach((testability) {
          testability.callMethod('whenStable', [decrement]);
        });
      });
      if (js.context['frameworkStabilizers'] == null) {
        js.context['frameworkStabilizers'] = new js.JsArray();
      }
      js.context['frameworkStabilizers'].add(whenAllStable);
    }
    jsRegistry.add(this._createRegistry(registry));
  }

  findTestabilityInTree(TestabilityRegistry registry, dynamic elem, bool findInAncestors) {
    if (elem == null) {
      return null;
    }
    var t = registry.getTestability(elem);
    if (t != null) {
      return t;
    } else if (!findInAncestors) {
      return null;
    }
    if (DOM.isShadowRoot(elem)) {
      return this.findTestabilityInTree(registry, DOM.getHost(elem), true);
    }
    return this.findTestabilityInTree(registry, DOM.parentElement(elem), true);
  }

  js.JsObject _createRegistry(TestabilityRegistry registry) {
    var object = new js.JsObject(js.context['Object']);
    object['getAngularTestability'] =
        _jsify((Element elem, bool findInAncestors) {
          var testability = registry.findTestabilityInTree(elem, findInAncestors);
          return testability == null
              ? null
              : _jsify(new PublicTestability(testability));
        });
    object['getAllAngularTestabilities'] = _jsify(() {
      var publicTestabilities = registry
          .getAllTestabilities()
          .map((testability) => new PublicTestability(testability));
      return _jsify(publicTestabilities);
    });
    return object;
  }
}
