/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

(function () {

  'use strict';

  // keep a reference to compileProvider so we can register new component-directives
  // on-the-fly based on $routeProvider configuration
  // TODO: remove this– right now you can only bootstrap one Angular app with this hack
  var $compileProvider, $q, $injector;

  /**
   * This module loads services that mimic ngRoute's configuration, and includes
   * an anchor link directive that intercepts clicks to routing.
   *
   * This module is intended to be used as a stop-gap solution for projects upgrading from ngRoute.
   * It intentionally does not implement all features of ngRoute.
   */
  angular.module('ngRouteShim', [])
    .provider('$route', $RouteProvider)
    .config(['$compileProvider', function (compileProvider) {
      $compileProvider = compileProvider;
    }])
    .factory('$routeParams', $routeParamsFactory)
    .directive('a', anchorLinkDirective)

    // Connects the legacy $routeProvider config shim to Component Router's config.
    .run(['$route', '$rootRouter', function ($route, $rootRouter) {
      $route.$$subscribe(function (routeDefinition) {
        if (!angular.isArray(routeDefinition)) {
          routeDefinition = [routeDefinition];
        }
        $rootRouter.config(routeDefinition);
      });
    }]);


  /**
   * A shimmed out provider that provides the same API as ngRoute's $routeProvider, but uses these calls
   * to configure Component Router.
   */
  function $RouteProvider() {

    var routes = [];
    var subscriptionFn = null;

    var routeMap = {};

    // Stats for which routes are skipped
    var skipCount = 0;
    var successCount = 0;
    var allCount = 0;

    function consoleMetrics() {
      return '(' + skipCount + ' skipped / ' + successCount + ' success / ' + allCount + ' total)';
    }


    /**
     * @ngdoc method
     * @name $routeProvider#when
     *
     * @param {string} path Route path (matched against `$location.path`). If `$location.path`
     *    contains redundant trailing slash or is missing one, the route will still match and the
     *    `$location.path` will be updated to add or drop the trailing slash to exactly match the
     *    route definition.
     *
     * @param {Object} route Mapping information to be assigned to `$route.current` on route
     *    match.
     */
    this.when = function(path, route) {
      //copy original route object to preserve params inherited from proto chain
      var routeCopy = angular.copy(route);

      allCount++;

      if (angular.isDefined(routeCopy.reloadOnSearch)) {
        console.warn('Route for "' + path + '" uses "reloadOnSearch" which is not implemented.');
      }
      if (angular.isDefined(routeCopy.caseInsensitiveMatch)) {
        console.warn('Route for "' + path + '" uses "caseInsensitiveMatch" which is not implemented.');
      }

      // use new wildcard format
      path = reformatWildcardParams(path);

      if (path[path.length - 1] == '*') {
        skipCount++;
        console.warn('Route for "' + path + '" ignored because it ends with *. Skipping.', consoleMetrics());
        return this;
      }

      if (path.indexOf('?') > -1) {
        skipCount++;
        console.warn('Route for "' + path + '" ignored because it has optional parameters. Skipping.', consoleMetrics());
        return this;
      }

      if (typeof route.redirectTo == 'function') {
        skipCount++;
        console.warn('Route for "' + path + '" ignored because lazy redirecting to a function is not yet implemented. Skipping.', consoleMetrics());
        return this;
      }


      var routeDefinition = {
        path: path,
        data: routeCopy
      };

      routeMap[path] = routeCopy;

      if (route.redirectTo) {
        routeDefinition.redirectTo = [routeMap[route.redirectTo].name];
      } else {
        if (routeCopy.controller && !routeCopy.controllerAs) {
          console.warn('Route for "' + path + '" should use "controllerAs".');
        }

        var componentName = routeObjToRouteName(routeCopy, path);

        if (!componentName) {
          throw new Error('Could not determine a name for route "' + path + '".');
        }

        routeDefinition.component = componentName;
        routeDefinition.name = route.name || upperCase(componentName);

        var directiveController = routeCopy.controller;

        var componentDefinition = {
          controller: directiveController,
          controllerAs: routeCopy.controllerAs

        };
        if (routeCopy.templateUrl) componentDefinition.templateUrl = routeCopy.templateUrl;
        if (routeCopy.template) componentDefinition.template = routeCopy.template;


        // if we have route resolve options, prepare a wrapper controller
        if (directiveController && routeCopy.resolve) {
          var originalController = directiveController;
          var resolvedLocals = {};

          componentDefinition.controller = ['$injector', '$scope', function ($injector, $scope) {
            var locals = angular.extend({
              $scope: $scope
            }, resolvedLocals);

            return $injector.instantiate(originalController, locals);
          }];

          // we resolve the locals in a canActivate block
          componentDefinition.controller.$canActivate = function() {
            var locals = angular.extend({}, routeCopy.resolve);

            angular.forEach(locals, function(value, key) {
              locals[key] = angular.isString(value) ?
                $injector.get(value) : $injector.invoke(value, null, null, key);
            });

            return $q.all(locals).then(function (locals) {
              resolvedLocals = locals;
            }).then(function () {
              return true;
            });
          };
        }

        // register the dynamically created directive
        $compileProvider.component(componentName, componentDefinition);
      }
      if (subscriptionFn) {
        subscriptionFn(routeDefinition);
      } else {
        routes.push(routeDefinition);
      }
      successCount++;

      return this;
    };

    this.otherwise = function(params) {
      if (typeof params === 'string') {
        params = {redirectTo: params};
      }
      this.when('/*rest', params);
      return this;
    };


    this.$get = ['$q', '$injector', function (q, injector) {
      $q = q;
      $injector = injector;

      var $route = {
        routes: routeMap,

        /**
         * @ngdoc method
         * @name $route#reload
         *
         * @description
         * Causes `$route` service to reload the current route even if
         * {@link ng.$location $location} hasn't changed.
         */
        reload: function() {
          throw new Error('Not implemented: $route.reload');
        },

        /**
         * @ngdoc method
         * @name $route#updateParams
         */
        updateParams: function(newParams) {
          throw new Error('Not implemented: $route.updateParams');
        },

        /**
         * Runs the given `fn` whenever new configs are added.
         * Only one subscription is allowed.
         * Passed `fn` is called synchronously.
         */
        $$subscribe: function(fn) {
          if (subscriptionFn) {
            throw new Error('only one subscription allowed');
          }
          subscriptionFn = fn;
          subscriptionFn(routes);
          routes = [];
        },

        /**
         * Runs a string with stats about many route configs were adapted, and how many were
         * dropped because they are incompatible.
         */
        $$getStats: consoleMetrics
      };

      return $route;
    }];

  }

  function $routeParamsFactory($rootRouter, $rootScope) {
    // the identity of this object cannot change
    var paramsObj = {};

    $rootScope.$on('$routeChangeSuccess', function () {
      var newParams = $rootRouter.currentInstruction && $rootRouter.currentInstruction.component.params;

      angular.forEach(paramsObj, function (val, name) {
        delete paramsObj[name];
      });
      angular.forEach(newParams, function (val, name) {
        paramsObj[name] = val;
      });
    });

    return paramsObj;
  }

  /**
   * Allows normal anchor links to kick off routing.
   */
  function anchorLinkDirective($rootRouter) {
    return {
      restrict: 'E',
      link: function (scope, element) {
        // If the linked element is not an anchor tag anymore, do nothing
        if (element[0].nodeName.toLowerCase() !== 'a') {
          return;
        }

        // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute.
        var hrefAttrName = Object.prototype.toString.call(element.prop('href')) === '[object SVGAnimatedString]' ?
          'xlink:href' : 'href';

        element.on('click', function (event) {
          if (event.which !== 1) {
            return;
          }

          var href = element.attr(hrefAttrName);
          var target = element.attr('target');
          var isExternal = (['_blank', '_parent', '_self', '_top'].indexOf(target) > -1);

          if (href && $rootRouter.recognize(href) && !isExternal) {
            $rootRouter.navigateByUrl(href);
            event.preventDefault();
          }
        });
      }
    };
  }

  /**
   * Given a route object, attempts to find a unique directive name.
   *
   * @param route – route config object passed to $routeProvider.when
   * @param path – route configuration path
   * @returns {string|name} – a normalized (camelCase) directive name
   */
  function routeObjToRouteName(route, path) {
    var name = route.controllerAs;

    var controller = route.controller;
    if (!name && controller) {
      if (angular.isArray(controller)) {
        controller = controller[controller.length - 1];
      }
      name = controller.name;
    }

    if (!name) {
      var segments = path.split('/');
      name = segments[segments.length - 1];
    }

    if (name) {
      name = name + 'AutoCmp';
    }

    return name;
  }

  function upperCase(str) {
    return str.charAt(0).toUpperCase() + str.substr(1);
  }

  /*
   * Changes "/:foo*" to "/*foo"
   */
  var WILDCARD_PARAM_RE = new RegExp('\\/:([a-z0-9]+)\\*', 'gi');
  function reformatWildcardParams(path) {
    return path.replace(WILDCARD_PARAM_RE, function (m, m1) {
      return '/*' + m1;
    });
  }

}());
