text: define([
    'aeris/events',
    'aeris/util',
    'aeris/emptypromise',
    'aeris/errors/invalidargumenterror',
    'aeris/maps/routes/waypoint',
    'aeris/maps/routes/route',
    'aeris/maps/routes/routerenderer',
    'aeris/commands/commandmanager',
    'aeris/maps/routes/commands/addwaypointcommand',
    'aeris/maps/routes/commands/movewaypointcommand',
    'aeris/maps/routes/commands/removewaypointcommand',
    'aeris/maps/routes/commands/resetroutecommand',
    'aeris/maps/routes/commands/appendreverseroutecommand'
  ], function(
    Events,
    _,
    EmptyPromise,
    InvalidArgumentError,
    Waypoint,
    Route,
    RouteRenderer,
    CommandManager,
    AddWaypointCommand,
    MoveWaypointCommand,
    RemoveWaypointCommand,
    ResetRouteCommand,
    AppendReverseRouteCommand
  ) {
  
  
    /**
     * Central Route component interface
     * for an application layer.
     *
     * Provides interface to route commands
     * and binds route events to {aeris.maps.gmaps.route.RouteRenderer}
     *
     * @param {Object=} opt_options
     * @param {aeris.maps.gmaps.route.RouteRenderer=}   opt_options.routeRenderer
     * @param {aeris.commands.CommandManager=}          opt_options.commandManger
     * @param {aeris.maps.gmaps.route.Route}            opt_options.route
     * @param {Object=}                                 opt_options.styles See RouteRenderer for style options.
     *
     * @param {Function=} opt_options.AddWaypointCommand
     * @param {Function=} opt_options.MoveWaypointCommand
     * @param {Function=} opt_options.RemoveWaypointCommand
     * @param {Function=} opt_options.ResetRouteCommand
     * @param {Function=} opt_options.AppendReverseRouteCommand
     *
     * @class aeris.maps.gmaps.route.RouteBuilder
     * @constructor
     */
    var RouteBuilder = function(opt_options) {
      var options = _.defaults(opt_options || {}, {
        followDirections: true,
        travelMode: 'DRIVING',
        commandManager: new CommandManager(),
        AddWaypointCommand: AddWaypointCommand,
        MoveWaypointCommand: MoveWaypointCommand,
        RemoveWaypointCommand: RemoveWaypointCommand,
        ResetRouteCommand: ResetRouteCommand,
        AppendReverseRouteCommand: AppendReverseRouteCommand,
        styles: {}
      });
  
      Events.call(this);
  
  
      /**
       * The Route being built.
       *
       * @type {aeris.maps.gmaps.route.Route}
       * @private
       * @property route_
       */
      this.route_;
  
  
      /**
       * The {aeris.maps.gmaps.route.RouteRenderer} used by the
       * builder to render route view elements.
       *
       * @type {aeris.maps.gmaps.route.RouteRenderer}
       * @private
       * @property routeRenderer_
       */
      this.routeRenderer_ = options.routeRenderer || new RouteRenderer();
  
      /**
       * Travel mode to set on new waypoints.
       * @type {aeris.maps.gmaps.route.Waypoint.travelMode}
       * @property travelMode
       */
      this.travelMode = options.travelMode;
  
  
      /**
       * Whether added waypoints should follow directions.
       * @type {Boolean}
       * @property followDirections
       */
      this.followDirections = options.followDirections;
  
  
  
      /**
       *
       * @type {boolean}
       * @property commandManager_
       */
      this.commandManager_ = options.commandManager;
  
  
      /**
       * @type {Function} Constructor.
       * @private
       * @property AddWaypointCommand_
       */
      this.AddWaypointCommand_ = options.AddWaypointCommand;
  
  
      /**
       * @type {Function} Constructor.
       * @private
       * @property MoveWaypointCommand_
       */
      this.MoveWaypointCommand_ = options.MoveWaypointCommand;
  
  
      /**
       * @type {Function} Constructor.
       * @private
       * @property RemoveWaypointCommand_
       */
      this.RemoveWaypointCommand_ = options.RemoveWaypointCommand;
  
  
      /**
       * @type {Function} Constructor.
       * @private
       * @property ResetRouteCommand_
       */
      this.ResetRouteCommand_ = options.ResetRouteCommand;
  
  
      /**
       * @type {Function} Constructor.
       * @private
       * @property AppendReverseRouteCommand_
       */
      this.AppendReverseRouteCommand_ = options.AppendReverseRouteCommand;
  
  
      /**
       * Hash of event listeners bound to the {aeris.maps.gmaps.route.Route}
       *
       * @type {Object}
       * @private
       * @property routeEvents_
       */
      this.routeEvents_ = {
        add: [this.renderWaypoint_, this.updateWaypointInstructions_],
        remove: this.eraseWaypoint_,
        reset: function(route, options) {
          _.each(options.previousModels, this.eraseWaypoint_, this);
          this.redrawRoute_(route);
        },
  
        'change:position change:position change:path change:selected': function(waypoint) {
          this.renderWaypoint_(waypoint);
        },
  
        'click': function(latLon, waypoint) {
          this.trigger('waypoint:click', latLon, waypoint);
        },
        'dragend': function(latLon, waypoint) {
          this.trigger('waypoint:dragend', latLon, waypoint);
        },
        'path:click': function(latLon, waypoint) {
          this.trigger('path:click', latLon, waypoint);
        }
      };
  
      this.setRoute(options.route || new Route());
  
      this.styleRoute(options.styles);
  
      this.proxyCommandManagerEvents_();
  
      /**
       * @event waypoint:click
       * @param {aeris.maps.LatLon} latLon
       * @param {aeris.maps.gmaps.route.Waypoint} marker
       */
      /**
       * @event waypoint:dragend
       * @param {aeris.maps.LatLon} latLon
       * @param {aeris.maps.gmaps.route.Waypoint} marker
       */
      /**
       * @event path:click
       * @param {aeris.maps.LatLon} latLon
       * @param {aeris.maps.gmaps.route.Waypoint} marker
       *        Waypoint whose path was clicked.
       */
  
      /**
       * A command has been executed, undone, or redone.
       * @event command
       */
      /**
       * @event command:execute
       */
      /**
       * @event command:undo
       */
      /**
       * @event command:redo
       */
    };
  
    _.extend(RouteBuilder.prototype, Events.prototype);
  
  
    RouteBuilder.prototype.proxyCommandManagerEvents_ = function() {
      this.proxyEvents(this.commandManager_, function(topic, arguments) {
        this.trigger('command');
  
        return {
          topic: 'command:' + topic,
          arguments: []               // No need to expose command parameter
        };
      }, this);
    };
  
  
    /**
     * @return {aeris.maps.gmaps.route.Route}
     * @method getRoute
     */
    RouteBuilder.prototype.getRoute = function() {
      return this.route_;
    };
  
  
    /**
     * @param {aeris.maps.Map} map
     * @method setMap
     */
    RouteBuilder.prototype.setMap = function(map) {
      this.routeRenderer_.setMap(map);
    };
  
  
    /**
     * @param {Object} styles
     * @method styleRoute
     */
    RouteBuilder.prototype.styleRoute = function(styles) {
      this.routeRenderer_.setStyles(styles);
    };
  
  
    /**
     * Delegates events to `this.route_`.
     *
     * @param {Object=} opt_events Defaults to this.routeEvents_.
     *        eg { 'click': this.handleClick_ }.
     * @param {Object} opt_ctx Context in which to invoke event handlers.
     * @method delegateRouteEvents
     */
    RouteBuilder.prototype.delegateRouteEvents = function(opt_events, opt_ctx) {
      opt_events || (opt_events = this.routeEvents_);
      opt_ctx || (opt_ctx = this);
  
      this.listenTo(this.getRoute(), opt_events, opt_ctx);
    };
  
  
    /**
     * Undelegate events bound to this.route_
     * @method undelegateRouteEvents
     */
    RouteBuilder.prototype.undelegateRouteEvents = function() {
      this.stopListening(this.getRoute());
    };
  
  
    /**
     * Undelegate all events bound by this
     * RouteBuilder.
     * @method undelegateEvents
     */
    RouteBuilder.prototype.undelegateEvents = function() {
      this.undelegateRouteEvents();
      this.commandManager_.removeProxy();
    };
  
  
    /**
     * @return {Object}
     * @method routeToJSON
     */
    RouteBuilder.prototype.routeToJSON = function() {
      return this.route_.toJSON();
    };
  
  
    /**
     * Undo the last executed command
     *
     * @return {aeris.Promise} Promise to undo command.
     * @method undo
     */
    RouteBuilder.prototype.undo = function() {
      return this.commandManager_.undo();
    };
  
  
    /**
     * @return. {Boolean} Whether undo is an available action.
     * @method canUndo
     */
    RouteBuilder.prototype.canUndo = function() {
      return this.commandManager_.canUndo();
    };
  
    /**
     * Redo the last executed command.
     *
     * @return {aeris.Promise} Promise to redo command.
     * @method redo
     */
    RouteBuilder.prototype.redo = function() {
      return this.commandManager_.redo();
    };
  
  
    /**
     * @return. {Boolean} Whether redo is an available action.
     * @method canRedo
     */
    RouteBuilder.prototype.canRedo = function() {
      return this.commandManager_.canRedo();
    };
  
  
    /**
     * Add a waypoint to the route.
     *
     * @param {aeris.maps.gmaps.route.Waypoint|Array.<number>} waypoint
     *
     * @param {Object=} opt_options
     * @param {number} opt_options.at Index at which to add the waypoint.
     *
     * @return {aeris.Promise} A promise to add the waypoint.
     * @method addWaypoint
     */
    RouteBuilder.prototype.addWaypoint = function(waypoint, opt_options) {
      var options = opt_options || {};
      var command;
  
      this.updateWaypointInstructions_(waypoint);
  
      if (options.at) {
        return this.addWaypointAt(waypoint, options.at);
      }
  
      command = new this.AddWaypointCommand_(this.getRoute(), waypoint);
      return this.executeCommand_(command);
    };
  
  
    /**
     * Insert a waypoint into to the route at a specified index.
     *
     * @param {aeris.maps.gmaps.route.Waypoint|Array.<number>} waypoint
     * @param {Number=} atIndex
     *
     * @return {aeris.Promise} A promise to add the waypoint.
     * @method addWaypointAt
     */
    RouteBuilder.prototype.addWaypointAt = function(waypoint, atIndex) {
      var command;
      var commandOptions = {
        at: atIndex
      };
  
      this.updateWaypointInstructions_(waypoint);
  
      command = new this.AddWaypointCommand_(this.getRoute(), waypoint, commandOptions);
  
      this.executeCommand_(command);
    };
  
  
    /**
     * Change the position of a waypoint.
     *
     * @param {aeris.maps.gmaps.route.Waypoint} waypoint
     * @param {aeris.maps.LatLon} latLon
     *
     * @return {aeris.Promise}
     * @method moveWaypoint
     */
    RouteBuilder.prototype.moveWaypoint = function(waypoint, latLon) {
      var command = new this.MoveWaypointCommand_(this.getRoute(), waypoint, latLon);
      return this.executeCommand_(command);
    };
  
  
    /**
     * @param {aeris.maps.gmaps.route.Waypoint} waypoint
     * @return {aeris.Promise} A promise to remove the waypoint.
     * @method removeWaypoint
     */
    RouteBuilder.prototype.removeWaypoint = function(waypoint) {
      var command = new this.RemoveWaypointCommand_(this.getRoute(), waypoint);
      return this.executeCommand_(command);
    };
  
  
    /**
     * @return {aeris.Promise} Promise to execute command.
     * @method appendReverseRoute
     */
    RouteBuilder.prototype.appendReverseRoute = function() {
      var command;
  
      var doesRouteHaveWaypointsToReverse = this.route_.length > 1;
      if (!doesRouteHaveWaypointsToReverse) { return new EmptyPromise(); }
  
      command = new this.AppendReverseRouteCommand_(this.getRoute());
  
      return this.executeCommand_(command);
    };
  
  
    /**
     * Reset all waypoints in a route.
     *
     * @param {Array.<aeris.maps.gmaps.route.Waypoint>=} opt_waypoints
     *        If not set, will clear all waypoints in the route.
     * @return {aeris.Promise} A promise to reset the route.
     * @method resetRoute
     */
    RouteBuilder.prototype.resetRoute = function(opt_waypoints) {
      var command;
  
      command = new this.ResetRouteCommand_(this.getRoute(), opt_waypoints);
      return this.executeCommand_(command);
    };
  
  
    /**
     * @private
     *
     * @param {aeris.commands.AbstractCommand} command
     * @return {aeris.Promise}
     * @method executeCommand_
     */
    RouteBuilder.prototype.executeCommand_ = function(command) {
      var promiseToExecute = this.commandManager_.executeCommand(command);
      return promiseToExecute;
    };
  
  
    /**
     * @param {aeris.maps.gmaps.route.Route} route
     * @method setRoute
     */
    RouteBuilder.prototype.setRoute = function(route) {
      if (this.route_) {
        this.undelegateRouteEvents();
      }
  
      this.route_ = route;
      this.delegateRouteEvents();
    };
  
  
    /**
     * Render a waypoint.
     *
     * @private
     * @param {aeris.maps.gmaps.route.Waypoint} waypoint
     * @method renderWaypoint_
     */
    RouteBuilder.prototype.renderWaypoint_ = function(waypoint) {
      this.routeRenderer_.renderWaypoint(waypoint);
    };
  
  
    RouteBuilder.prototype.updateWaypointInstructions_ = function(waypoint) {
      waypoint.set({
        followDirections: this.followDirections,
        travelMode: this.travelMode
      }, { validate: true });
    };
  
  
    /**
     * Remove a waypoint view from the map.
     *
     * @param {aeris.maps.gmaps.route.Waypoint} waypoint
     * @param {number} indexOfWaypointInRoute
     * @private
     * @method eraseWaypoint_
     */
    RouteBuilder.prototype.eraseWaypoint_ = function(waypoint, indexOfWaypointInRoute) {
      var nextWaypoint = this.getRoute().at(indexOfWaypointInRoute);
  
      this.routeRenderer_.eraseWaypoint(waypoint);
  
      // Redraw next waypoint
      if (nextWaypoint) {
        this.routeRenderer_.renderWaypoint(nextWaypoint);
      }
    };
  
  
    /**
     * @param {Array.<aeris.maps.gmaps.route.Waypoint>} waypoints
     * @private
     * @method redrawRoute_
     */
    RouteBuilder.prototype.redrawRoute_ = function(waypoints) {
      this.routeRenderer_.eraseRoute(this.getRoute());
  
      if (waypoints.length) {
        this.routeRenderer_.renderRoute(this.getRoute());
      }
    };
  
  
    return _.expose(RouteBuilder, 'aeris.maps.gmaps.route.RouteBuilder');
  });
  