import _getIterator from 'babel-runtime/core-js/get-iterator';
import _extends from 'babel-runtime/helpers/extends';
import _objectWithoutProperties from 'babel-runtime/helpers/objectWithoutProperties';
import _Object$keys from 'babel-runtime/core-js/object/keys';
import _typeof from 'babel-runtime/helpers/typeof';
import _WeakMap from 'babel-runtime/core-js/weak-map';
import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
import invariant from 'invariant';
import isEqual from 'lodash/isEqual';
import Relay from 'react-relay';

import getRouteQueries from './utils/getRouteQueries';
import mergeRouteParams from './utils/mergeRouteParams';

var DEFAULT_KEY = '_default';

var QueryAggregator = function () {
  function QueryAggregator(routerProps) {
    _classCallCheck(this, QueryAggregator);

    // We need to use a map to track route indices instead of throwing them on
    // the route itself with a Symbol to ensure that, when rendering on the
    // server, each request generates route indices independently.
    this.routeIndices = new _WeakMap();
    this.lastRouteIndex = 0;

    this.queryConfig = null;
    this.fragmentSpecs = null;

    this.props = {};
    this.readyState = null;

    this.updateQueryConfig(routerProps);
  }

  QueryAggregator.prototype.updateQueryConfig = function updateQueryConfig(routerProps) {
    var _this = this;

    var routes = routerProps.routes;
    var components = routerProps.components;


    var queryConfig = {
      name: null,
      queries: {},
      params: {}
    };
    var fragmentSpecs = {};

    routes.forEach(function (route, i) {
      // We need to merge in the route params regardless of whether the route
      // actually has queries, in case its children depend on its path params.
      queryConfig.params = mergeRouteParams(queryConfig.params, route, routerProps);

      var routeQueries = getRouteQueries(route, routerProps);
      if (!routeQueries) {
        return;
      }

      var routeComponent = components[i];

      var componentMap = void 0;
      var queryMap = void 0;
      if ((typeof routeComponent === 'undefined' ? 'undefined' : _typeof(routeComponent)) === 'object') {
        componentMap = routeComponent;
        queryMap = routeQueries;
      } else {
        var _componentMap, _queryMap;

        componentMap = (_componentMap = {}, _componentMap[DEFAULT_KEY] = routeComponent, _componentMap);
        queryMap = (_queryMap = {}, _queryMap[DEFAULT_KEY] = routeQueries, _queryMap);
      }

      _Object$keys(componentMap).forEach(function (key) {
        var component = componentMap[key];
        var queries = queryMap[key];

        if (!queries) {
          return;
        }

        // In principle not all container component routes have to specify
        // queries, because some of them might somehow receive fragments from
        // their parents, but it would definitely be wrong to specify queries
        // for a component that isn't a container.
        !Relay.isContainer(component) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'relay-router-relay: Route with queries specifies component `%s` ' + 'that is not a Relay container.', component && (component.displayName || component.name)) : invariant(false) : void 0;

        _Object$keys(queries).forEach(function (queryName) {
          var query = queries[queryName];
          var uniqueQueryName = _this.getUniqueQueryName(route, key, queryName);

          // Relay depends on the argument count of the query function, so try
          // to preserve it as well as possible.
          var wrappedQuery = void 0;
          if (query.length === 0) {
            // Relay doesn't like using the exact same query in multiple
            // places, so wrap it to prevent that when sharing queries between
            // routes.
            wrappedQuery = function wrappedQuery() {
              return query();
            };
          } else {
            (function () {
              // When not using the shorthand, we can control the injected
              // params, so restrict them to just the ones for the current route
              // and its ancestors.
              var paramsForRoute = queryConfig.params;

              // We need the query function to have > 0 arguments to hit the code
              // path for non-shorthand queries.
              /* eslint-disable no-unused-vars */
              wrappedQuery = function wrappedQuery(_) {
                return query(component, paramsForRoute);
              };
              /* eslint-enable */
            })();
          }

          queryConfig.queries[uniqueQueryName] = wrappedQuery;
          fragmentSpecs[uniqueQueryName] = { component: component, queryName: queryName };
        });
      });
    });

    queryConfig.name = ['_aggregated'].concat(_Object$keys(queryConfig.queries)).join('_');

    // RootContainer uses referential equality to check for route change, so
    // replace the route object entirely.
    this.queryConfig = queryConfig;
    this.fragmentSpecs = fragmentSpecs;
  };

  QueryAggregator.prototype.getUniqueQueryName = function getUniqueQueryName(route, key, queryName) {
    // There might be some edge case here where the query changes but the route
    // object does not, in which case we'll keep using the old unique name.
    // Anybody who does that deserves whatever they get, though.

    // Prefer an explicit route name if specified.
    if (route.name) {
      // The slightly different template here ensures that we can't have
      // collisions with the below template.
      return '_' + route.name + '_' + key + '_' + queryName;
    }

    // Otherwise, use referential equality on the route name to generate a
    // unique index.
    var routeIndex = this.routeIndices.get(route);
    if (routeIndex === undefined) {
      routeIndex = ++this.lastRouteIndex;
      this.routeIndices.set(route, routeIndex);
    }

    return '__route_' + routeIndex + '_' + key + '_' + queryName;
  };

  QueryAggregator.prototype.setRenderArgs = function setRenderArgs(_ref) {
    var props = _ref.props;

    var readyState = _objectWithoutProperties(_ref, ['props']);

    if (props) {
      this.props = props;
    }

    this.readyState = readyState;
  };

  QueryAggregator.prototype.getRenderArgs = function getRenderArgs(route, key, queries, params) {
    return _extends({}, this.readyState, {
      props: this.getProps(route, key, queries, params)
    });
  };

  QueryAggregator.prototype.getProps = function getProps(route) {
    var key = arguments.length <= 1 || arguments[1] === undefined ? DEFAULT_KEY : arguments[1];
    var queries = arguments[2];
    var params = arguments[3];

    // Check that the subset of parameters used for this route match those used
    // for the fetched data.
    for (var _iterator = _Object$keys(params), _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _getIterator(_iterator);;) {
      var _ref2;

      if (_isArray) {
        if (_i >= _iterator.length) break;
        _ref2 = _iterator[_i++];
      } else {
        _i = _iterator.next();
        if (_i.done) break;
        _ref2 = _i.value;
      }

      var paramName = _ref2;

      if (!isEqual(this.props[paramName], params[paramName])) {
        return null;
      }
    }

    var props = {};
    for (var _iterator2 = _Object$keys(queries), _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _getIterator(_iterator2);;) {
      var _ref3;

      if (_isArray2) {
        if (_i2 >= _iterator2.length) break;
        _ref3 = _iterator2[_i2++];
      } else {
        _i2 = _iterator2.next();
        if (_i2.done) break;
        _ref3 = _i2.value;
      }

      var queryName = _ref3;

      var uniqueQueryName = this.getUniqueQueryName(route, key, queryName);

      var value = this.props[uniqueQueryName];
      if (!value) {
        return null;
      }

      props[queryName] = value;
    }

    // Only return the props for the route if the query config params match and
    // all requested props are available. Otherwise, by assumption, the ready
    // state will have the correct "not ready" state.
    return props;
  };

  // The below methods are required to satisfy the Relay container contract.

  QueryAggregator.prototype.getFragmentNames = function getFragmentNames() {
    return _Object$keys(this.fragmentSpecs);
  };

  QueryAggregator.prototype.getFragment = function getFragment(fragmentName, variableMapping) {
    var _fragmentSpecs$fragme = this.fragmentSpecs[fragmentName];
    var component = _fragmentSpecs$fragme.component;
    var queryName = _fragmentSpecs$fragme.queryName;

    return component.getFragment(queryName, variableMapping);
  };

  QueryAggregator.prototype.hasFragment = function hasFragment(fragmentName) {
    return this.fragmentSpecs[fragmentName] !== undefined;
  };

  QueryAggregator.prototype.hasVariable = function hasVariable(variableName) {
    // It doesn't matter what the component variables are. The only variables
    // we're going to pass down are the ones defined from our route parameters.
    return Object.prototype.hasOwnProperty.call(this.queryConfig.params, variableName);
  };

  return QueryAggregator;
}();

export default QueryAggregator;