var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/,
    HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/,
    DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21};

/**
 * @workInProgress
 * @ngdoc service
 * @name angular.service.$location
 * @requires $browser
 *
 * @property {string} href
 * @property {string} protocol
 * @property {string} host
 * @property {number} port
 * @property {string} path
 * @property {Object.<string|boolean>} search
 * @property {string} hash
 * @property {string} hashPath
 * @property {Object.<string|boolean>} hashSearch
 *
 * @description
 * Parses the browser location url and makes it available to your application.
 * Any changes to the url are reflected into $location service and changes to
 * $location are reflected to url.
 * Notice that using browser's forward/back buttons changes the $location.
 *
 * @example
   <doc:example>
     <doc:source>
       <a href="#">clear hash</a> |
       <a href="#myPath?name=misko">test hash</a><br/>
       <input type='text' name="$location.hash"/>
       <pre>$location = {{$location}}</pre>
     </doc:source>
     <doc:scenario>
     </doc:scenario>
    </doc:example>
 */
angularServiceInject("$location", function($browser) {
  var scope = this,
      location = {update:update, updateHash: updateHash},
      lastLocation = {};

  $browser.onHashChange(function() { //register
    update($browser.getUrl());
    copy(location, lastLocation);
    scope.$eval();
  })(); //initialize

  this.$onEval(PRIORITY_FIRST, sync);
  this.$onEval(PRIORITY_LAST, updateBrowser);

  return location;

  // PUBLIC METHODS

  /**
   * @workInProgress
   * @ngdoc method
   * @name angular.service.$location#update
   * @methodOf angular.service.$location
   *
   * @description
   * Update location object
   * Does not immediately update the browser
   * Browser is updated at the end of $eval()
   *
   * @example
    <doc:example>
      <doc:source>
        scope.$location.update('http://www.angularjs.org/path#hash?search=x');
        scope.$location.update({host: 'www.google.com', protocol: 'https'});
        scope.$location.update({hashPath: '/path', hashSearch: {a: 'b', x: true}});
      </doc:source>
      <doc:scenario>
      </doc:scenario>
    </doc:example>
   *
   * @param {(string|Object)} href Full href as a string or object with properties
   */
  function update(href) {
    if (isString(href)) {
      extend(location, parseHref(href));
    } else {
      if (isDefined(href.hash)) {
        extend(href, isString(href.hash) ? parseHash(href.hash) : href.hash);
      }

      extend(location, href);

      if (isDefined(href.hashPath || href.hashSearch)) {
        location.hash = composeHash(location);
      }

      location.href = composeHref(location);
    }
  }

  /**
   * @workInProgress
   * @ngdoc method
   * @name angular.service.$location#updateHash
   * @methodOf angular.service.$location
   *
   * @description
   * Update location hash part
   * @see update()
   *
   * @example
    <doc:example>
      <doc:source>
        scope.$location.updateHash('/hp')
         ==> update({hashPath: '/hp'})
        scope.$location.updateHash({a: true, b: 'val'})
         ==> update({hashSearch: {a: true, b: 'val'}})
        scope.$location.updateHash('/hp', {a: true})
         ==> update({hashPath: '/hp', hashSearch: {a: true}})
      </doc:source>
      <doc:scenario>
      </doc:scenario>
    </doc:example>
   *
   * @param {(string|Object)} path A hashPath or hashSearch object
   * @param {Object=} search A hashSearch object
   */
  function updateHash(path, search) {
    var hash = {};

    if (isString(path)) {
      hash.hashPath = path;
      hash.hashSearch = search || {};
    } else
      hash.hashSearch = path;

    hash.hash = composeHash(hash);

    update({hash: hash});
  }


  // INNER METHODS

  /**
   * Synchronizes all location object properties.
   *
   * User is allowed to change properties, so after property change,
   * location object is not in consistent state.
   *
   * Properties are synced with the following precedence order:
   *
   * - `$location.href`
   * - `$location.hash`
   * - everything else
   *
   * @example
   * <pre>
   *   scope.$location.href = 'http://www.angularjs.org/path#a/b'
   * </pre>
   * immediately after this call, other properties are still the old ones...
   *
   * This method checks the changes and update location to the consistent state
   */
  function sync() {
    if (!equals(location, lastLocation)) {
      if (location.href != lastLocation.href) {
        update(location.href);
        return;
      }
      if (location.hash != lastLocation.hash) {
        var hash = parseHash(location.hash);
        updateHash(hash.hashPath, hash.hashSearch);
      } else {
        location.hash = composeHash(location);
        location.href = composeHref(location);
      }
      update(location.href);
    }
  }


  /**
   * If location has changed, update the browser
   * This method is called at the end of $eval() phase
   */
  function updateBrowser() {
    sync();

    if ($browser.getUrl() != location.href) {
      $browser.setUrl(location.href);
      copy(location, lastLocation);
    }
  }

  /**
   * Compose href string from a location object
   *
   * @param {Object} loc The location object with all properties
   * @return {string} Composed href
   */
  function composeHref(loc) {
    var url = toKeyValue(loc.search);
    var port = (loc.port == DEFAULT_PORTS[loc.protocol] ? _null : loc.port);

    return loc.protocol  + '://' + loc.host +
          (port ? ':' + port : '') + loc.path +
          (url ? '?' + url : '') + (loc.hash ? '#' + loc.hash : '');
  }

  /**
   * Compose hash string from location object
   *
   * @param {Object} loc Object with hashPath and hashSearch properties
   * @return {string} Hash string
   */
  function composeHash(loc) {
    var hashSearch = toKeyValue(loc.hashSearch);
    //TODO: temporary fix for issue #158
    return escape(loc.hashPath).replace(/%21/gi, '!').replace(/%3A/gi, ':').replace(/%24/gi, '$') +
          (hashSearch ? '?' + hashSearch : '');
  }

  /**
   * Parse href string into location object
   *
   * @param {string} href
   * @return {Object} The location object
   */
  function parseHref(href) {
    var loc = {};
    var match = URL_MATCH.exec(href);

    if (match) {
      loc.href = href.replace(/#$/, '');
      loc.protocol = match[1];
      loc.host = match[3] || '';
      loc.port = match[5] || DEFAULT_PORTS[loc.protocol] || _null;
      loc.path = match[6] || '';
      loc.search = parseKeyValue(match[8]);
      loc.hash = match[10] || '';

      extend(loc, parseHash(loc.hash));
    }

    return loc;
  }

  /**
   * Parse hash string into object
   *
   * @param {string} hash
   */
  function parseHash(hash) {
    var h = {};
    var match = HASH_MATCH.exec(hash);

    if (match) {
      h.hash = hash;
      h.hashPath = unescape(match[1] || '');
      h.hashSearch = parseKeyValue(match[3]);
    }

    return h;
  }
}, ['$browser']);
