/**
 * @provides javelin-uri
 * @requires javelin-install
 *           javelin-util
 *           javelin-stratcom
 *
 * @javelin-installs JX.$U
 *
 * @javelin
 */

/**
 * Handy convenience function that returns a @{class:JX.URI} instance. This
 * allows you to write things like:
 *
 *   JX.$U('http://zombo.com/').getDomain();
 *
 * @param string            Unparsed URI.
 * @return  @{class:JX.URI} JX.URI instance.
 */
JX.$U = function(uri) {
  return new JX.URI(uri);
};

/**
 * Convert a string URI into a maleable object.
 *
 *   var uri = new JX.URI('http://www.example.com/asdf.php?a=b&c=d#anchor123');
 *   uri.getProtocol();    // http
 *   uri.getDomain();      // www.example.com
 *   uri.getPath();        // /asdf.php
 *   uri.getQueryParams(); // {a: 'b', c: 'd'}
 *   uri.getFragment();    // anchor123
 *
 * ...and back into a string:
 *
 *   uri.setFragment('clowntown');
 *   uri.toString() // http://www.example.com/asdf.php?a=b&c=d#clowntown
 */
JX.install('URI', {
  statics : {
    _uriPattern : /(?:([^:\/?#]+):)?(?:\/\/([^:\/?#]*)(?::(\d*))?)?([^?#]*)(?:\?([^#]*))?(?:#(.*))?/,

    /**
     *  Convert a Javascript object into an HTTP query string.
     *
     *  @param  Object  Map of query keys to values.
     *  @return String  HTTP query string, like 'cow=quack&duck=moo'.
     */
    _defaultQuerySerializer : function(obj) {
      var kv_pairs = [];
      for (var key in obj) {
        if (obj[key] !== null) {
          var value = encodeURIComponent(obj[key]);
          kv_pairs.push(encodeURIComponent(key) + (value ? '=' + value : ''));
        }
      }

      return kv_pairs.join('&');
    },

    _decode : function(str) {
      return decodeURIComponent(str.replace(/\+/g, ' '));
    }
  },

  /**
   * Construct a URI
   *
   * Accepts either absolute or relative URIs. Relative URIs may have protocol
   * and domain properties set to undefined
   *
   * @param string    absolute or relative URI
   */
  construct : function(uri) {
    // need to set the default value here rather than in the properties map,
    // or else we get some crazy global state breakage
    this.setQueryParams({});

    if (uri) {
      // parse the url
      var result = JX.URI._uriPattern.exec(uri);

      // fallback to undefined because IE has weird behavior otherwise
      this.setProtocol(result[1] || undefined);
      this.setDomain(result[2] || undefined);
      this.setPort(result[3] || undefined);
      var path = result[4];
      var query = result[5];
      this.setFragment(result[6] || undefined);

      // parse the path
      this.setPath(path.charAt(0) == '/' ? path : '/' + path);

      // parse the query data
      if (query && query.length) {
        var dict = {};
        var parts = query.split('&');
        for (var ii = 0; ii < parts.length; ii++) {
          var part = parts[ii];
          if (!part.length) {
            continue;
          }
          var pieces = part.split('=');
          var name = pieces[0];
          if (!name.length) {
            continue;
          }
          var value = pieces.slice(1).join('=') || '';
          dict[JX.URI._decode(name)] = JX.URI._decode(value);
        }
        this.setQueryParams(dict);
      }
    }
  },

  properties : {
    protocol: undefined,
    port: undefined,
    path: undefined,
    queryParams: undefined,
    fragment: undefined,
    querySerializer: undefined
  },

  members : {
    _domain: undefined,

    /**
     * Append and override query data values
     * Remove a query key by setting it undefined
     *
     * @param map
     * @return @{JX.URI} self
     */
    addQueryParams : function(map) {
      JX.copy(this.getQueryParams(), map);
      return this;
    },

    /**
     * Set a specific query parameter
     * Remove a query key by setting it undefined
     *
     * @param string
     * @param wild
     * @return @{JX.URI} self
     */
    setQueryParam : function(key, value) {
      var map = {};
      map[key] = value;
      return this.addQueryParams(map);
    },

    /**
     * Set the domain
     *
     * This function checks the domain name to ensure that it is safe for
     * browser consumption.
     */
    setDomain : function(domain) {
      var re = new RegExp(
        // For the bottom 128 code points, we use a strict whitelist of
        // characters that are allowed by all browsers: -.0-9:A-Z[]_a-z
        '[\\x00-\\x2c\\x2f\\x3b-\\x40\\x5c\\x5e\\x60\\x7b-\\x7f' +
        // In IE, these chararacters cause problems when entity-encoded.
        '\\uFDD0-\\uFDEF\\uFFF0-\\uFFFF' +
        // In Safari, these characters terminate the hostname.
        '\\u2047\\u2048\\uFE56\\uFE5F\\uFF03\\uFF0F\\uFF1F]');
      if (re.test(domain)) {
        JX.$E('JX.URI.setDomain(...): invalid domain specified.');
      }
      this._domain = domain;
      return this;
    },

    getDomain : function() {
      return this._domain;
    },

    getRelativeURI: function() {
      return JX.$U(this.toString())
        .setProtocol(null)
        .setPort(null)
        .setDomain(null)
        .toString();
    },

    toString : function() {
      if (__DEV__) {
        if (this.getPath() && this.getPath().charAt(0) != '/') {
          JX.$E(
            'JX.URI.toString(): ' +
            'Path does not begin with a "/" which means this URI will likely' +
            'be malformed. Ensure any string passed to .setPath() leads "/"');
        }
      }
      var str = '';
      if (this.getProtocol()) {
        str += this.getProtocol() + '://';
      }
      str += this.getDomain() || '';

      if (this.getPort()) {
        str += ':' + this.getPort();
      }

      // If there is a domain or a protocol, we need to provide '/' for the
      // path. If we don't have either and also don't have a path, we can omit
      // it to produce a partial URI without path information which begins
      // with "?", "#", or is empty.
      str += this.getPath() || (str ? '/' : '');

      str += this._getQueryString();
      if (this.getFragment()) {
        str += '#' + this.getFragment();
      }
      return str;
    },

    _getQueryString : function() {
      var str = (
        this.getQuerySerializer() || JX.URI._defaultQuerySerializer
      )(this.getQueryParams());
      return str ? '?' + str : '';
    },

    /**
     * Redirect the browser to another page by changing the window location. If
     * the URI is empty, reloads the current page.
     *
     * You can install a Stratcom listener for the 'go' event if you need to log
     * or prevent redirects.
     *
     * @return void
     */
    go : function() {
      var uri = this.toString();
      if (JX.Stratcom.invoke('go', null, {uri: uri}).getPrevented()) {
        return;
      }
      if (!uri) {
        // window.location.reload clears cache in Firefox.
        uri = window.location.pathname + (window.location.query || '');
      }
      window.location = uri;
    }

  }
});
