define([
  'async'
], function(async) {
  var root = this;

  /**
   * Google maps AMD loader plugin.
   *
   * Example:
   *  // All configs options are optional.
   *  require.config({
   *      googlemaps: {
   *        url: 'https://maps.googleapis.com/maps/api/js',
   *        params: {
   *          key: 'abcd1234',
   *          libraries: 'geometry',
   *          sensor: true                // Defaults to false
   *        },
   *        async: asyncLoaderPlugin      // Primarly for providing test stubs.
   *      }
   *  });
   *
   *  require(['googlemaps!'], function(gmaps) {
   *    var map = new gmaps.Map('map-canvas);
   *  });
   *
   */
  var googlemapsPlugin = {
    load: function(name, parentRequire, onload, opt_config) {
      var googleMapsLoader;
      var config = opt_config || {};

      if (config.isBuild) {
        onload(null);
        return;
      }

      googleMapsLoader = new GoogleMapsLoader(parentRequire, onload, config.googlemaps || {});
      googleMapsLoader.load();
    }
  };


  /**
   * Helper class for googlemaps loader plugin.
   */
  var GoogleMapsLoader = function(require, onload, config) {
    this.require_ = require;
    this.onload_ = onload || NOOP;
    this.baseUrl_ = config.url || GoogleMapsLoader.DEFAULT_BASE_URL;
    this.async_ = config.async || async;
    this.params_ = config.params;
  };


  GoogleMapsLoader.prototype.load = function() {
    if (this.isGoogleMapsLoaded_()) {
      this.resolveWith_(this.getGlobalGoogleMaps_());
    }
    else {
      this.loadGoogleMaps_();
    }
  };


  GoogleMapsLoader.prototype.loadGoogleMaps_ = function() {
    var self = this;

    var onAsyncLoad = function() {
      // Ensure correct context
      self.resolveWithGoogleMaps_(self);
    };
    onAsyncLoad.onerror = this.onload_.onerror;

    this.async_.load(this.getGoogleUrl_(), this.require_, onAsyncLoad, {});
  };


  GoogleMapsLoader.prototype.getGoogleUrl_ = function() {
    return this.baseUrl_ + '?' + this.serializeParams_();
  };


  GoogleMapsLoader.prototype.resolveWithGoogleMaps_ = function() {
    if (!this.isGoogleMapsLoaded_()) {
      this.reject_();
      return;
    }

    this.resolveWith_(this.getGlobalGoogleMaps_());
  };


  /** Thanks to http://jsfiddle.net/rudiedirkx/U5Tyb/1/ */
  GoogleMapsLoader.prototype.serializeParams_ = function() {
    var encodedParams = [];
    for (var key in this.params_) {
      if (this.params_.hasOwnProperty(key)) {
        var value = this.params_[key];
        var isObject = (typeof value === 'object');
        var encodedParam = encodeURIComponent(key) + "=" + encodeURIComponent(value);
        var serializedValue = isObject ? this.serializeParams_(value, key) : encodedParam;

        encodedParams.push(serializedValue);
      }
    }

    return encodedParams.join("&");
  };


  GoogleMapsLoader.prototype.isGoogleMapsLoaded_ = function() {
    return root.google && root.google.maps;
  };


  GoogleMapsLoader.prototype.getGlobalGoogleMaps_ = function() {
    return root.google ? root.google.maps : undefined;
  };


  GoogleMapsLoader.prototype.resolveWith_ = function(var_args) {
    this.onload_.apply(root, arguments);
  };


  GoogleMapsLoader.prototype.reject_ = function(opt_error) {
    var error = opt_error || new Error('Failed to load Google Maps library.');

    if (this.onload_.onerror) {
      this.onload_.onerror.call(root, error);
    }
    else {
      throw error;
    }
  };


  GoogleMapsLoader.DEFAULT_BASE_URL = 'https://maps.googleapis.com/maps/api/js';


  function NOOP() {
  }


  return googlemapsPlugin;
});