text: var urlParse = require('url').parse;
  var querystring = require('querystring');
  var isBrowser = (typeof window !== "undefined");
  
  var getDefaults = function(){
    var defaultUrl = "http://localhost:80";
    if (isBrowser){
      defaultUrl = window.location.href.toString();
    }
    var defaults = urlParse(defaultUrl);
    return defaults;
  };
  
  if (!Array.isArray) {
    Array.isArray = function (arg) {
      return Object.prototype.toString.call(arg) === '[object Array]';
    };
  }
  
  var objectEach = function(obj, cb){
    for (var k in obj){
      if (Object.prototype.hasOwnProperty.call(obj, k)) {
        cb(obj[k], k);  
      }
    }
  };
  
  var argsArray = function(obj){
      if (!obj) { return []; }
      if (Array.isArray(obj)) { return obj.slice() ; }
      var args = [];
      objectEach(obj, function(v, k){
        args[k] = v;
      });
      return args;
    };
  
  var arrLast = function(arr){
    return arr[arr.length-1];
  };
  
  var arrFlatten = function(input, output) {
    if (!output) { output = []; }
    for (var i = 0; i < input.length; i++){
      var value = input[i];
      if (Array.isArray(value)) {
          arrFlatten(value, output);
      } else {
        output.push(value);
      }
    }
    return output;
  };
  
  
  var UrlGrey = function(url){
    if (!url && isBrowser){
      url = window.location.href.toString();
    }
    this.url = url;
    this._parsed = null;
  };
  
  UrlGrey.prototype.parsed = function(){
    if (!this._parsed){
      this._parsed = urlParse(this.url);
      var defaults = getDefaults();
      var p = this._parsed;
      p.protocol = p.protocol || defaults.protocol;
      p.protocol = p.protocol.slice(0,-1);
      if (p.hash){
        p.hash = p.hash.substring(1);
      }
      p.username = ''; 
      p.password = '';
      if (p.protocol !== 'file'){
        p.port = parseInt(p.port, 10);
        if (p.auth){
          var auth = p.auth.split(':');
          p.username = auth[0]; 
          p.password = auth[1];
        }
      }
      if (isBrowser){
        p.hostname = p.hostname || defaults.hostname;
      }
    }
  
    // enforce only returning these properties
    this._parsed = {
     protocol : this._parsed.protocol,
     auth:        this._parsed.auth,
     host:        this._parsed.host,
     port:        this._parsed.port,
     hostname:    this._parsed.hostname,
     hash:        this._parsed.hash,
     search:      this._parsed.search,
     query:       this._parsed.query,
     pathname:    this._parsed.pathname,
     path:        this._parsed.path,
     href:        this._parsed.href,
     username:    this._parsed.username,
     password:    this._parsed.password
    };
  
  
    return this._parsed;
  };
  
  
  UrlGrey.prototype.extendedPath = function(url){
    if (url){
      var p = urlParse(url);
      var obj = new UrlGrey(this.toString());
      if (p.hash){
        p.hash = p.hash.substring(1);
      }
      obj.parsed().hash = p.hash;
      obj.parsed().query = p.query;
      obj = obj.path(p.pathname);
      return obj;
    } else {
      var href = this.path();
      href += this.queryString() ? '?' + this.queryString() : '';
      href += this.hash() ? '#' + this.hash() : '';
      return href;
    }
  };
  
  
  UrlGrey.prototype.port = function(num){
    var hostname = this.parsed().hostname;
  
    // setter
    if (num){
      if (this.protocol() === 'file'){
        throw new Error("file urls don't have ports");
      }
      var obj = new UrlGrey(this.toString());
      obj.parsed().port = parseInt(num, 10);
      return obj;
    }
  
    // getter
    var output = this._parsed.port;
    if (!output){
        switch(this.protocol()){
          case 'http' : return 80;
          case 'https' : return 443;
          default : return null;
        }
    }
    return parseInt(output, 10);
  };
  
  UrlGrey.prototype.query = function(mergeObject){
    if (arguments.length === 0) {
      return querystring.parse(this.queryString());
    } else if (mergeObject === null || mergeObject === false){
      return this.queryString('');
    } else {
      // read the object out
      var oldQuery = querystring.parse(this.queryString());
      objectEach(mergeObject, function(v, k){
        if (v === null || v === false){
          delete oldQuery[k];
        } else {
          oldQuery[k] = v;
        }
      });
      var newString = querystring.stringify(oldQuery);
      var ret = this.queryString(newString);
      return ret;
    }
  };
  
    
  UrlGrey.prototype.rawQuery = function(mergeObject){
    if (arguments.length === 0) {
      if (this.queryString().length === 0) { return {}; }
      
      return this.queryString().split("&").reduce(function(obj, pair) {
        pair = pair.split("=");
        var key = pair[0];
        var val = pair[1];
        obj[key] = val;
        return obj;
      }, {});
    } else if (mergeObject === null || mergeObject === false){
      return this.queryString('');
    } else {
      // read the object out
      var oldQuery = querystring.parse(this.queryString());
      objectEach(mergeObject, function(v, k){
        if (v === null){
          delete oldQuery[k];
        } else {
          oldQuery[k] = v;
        }
      });
      var pairs = [];
      objectEach(oldQuery, function(v, k){
        pairs.push(k + '=' + v);
      });
      var newString = pairs.join('&');
  
      return this.queryString(newString);
    }
  };
  
  addPropertyGetterSetter('protocol');
  addPropertyGetterSetter('username');
  addPropertyGetterSetter('password');
  addPropertyGetterSetter('hostname');
  addPropertyGetterSetter('hash');
  // add a method called queryString that manipulates 'query'
  addPropertyGetterSetter('query', 'queryString');  
  addPropertyGetterSetter('pathname', 'path');  
  
  UrlGrey.prototype.path = function(){
    var args = arrFlatten(argsArray(arguments));
    if (args.length !== 0){
      var obj = new UrlGrey(this.toString());
      var str = args.join('/');
      str = str.replace(/\/+/g, '/'); // remove double slashes
      str = str.replace(/\/$/, '');  // remove all trailing slashes
      args = str.split('/');
      for(var i = 0; i < args.length; i++){
        args[i] = this.encode(args[i]);
      }
      str = args.join('/');
      if (str[0] !== '/'){ str = '/' + str; }
      obj.parsed().pathname = str;
      return obj;
    }
    return this.parsed().pathname;
  };
  
  UrlGrey.prototype.rawPath = function(){
    var args = arrFlatten(argsArray(arguments));
    if (args.length !== 0){
      var obj = new UrlGrey(this.toString());
      var str = args.join('/');
      str = str.replace(/\/+/g, '/'); // remove double slashes
      str = str.replace(/\/$/, '');  // remove all trailing slashes
      if (str[0] !== '/'){ str = '/' + str; }
      obj.parsed().pathname = str;
      return obj;
    }
    return this.parsed().pathname;
  };
  
  UrlGrey.prototype.encode = function(str){
    try {
      return encodeURIComponent(str);
    } catch (ex) {
      return querystring.escape(str);
    }
  };
  
  UrlGrey.prototype.decode = function(str){
    return decode(str);
  };
  
  UrlGrey.prototype.parent = function(){
    // read-only.  (can't SET parent)
    var pieces = this.path().split("/");
    var popped = pieces.pop();
    if (popped === ''){  // ignore trailing slash
      popped = pieces.pop();
    }
    if (!popped){
      throw new Error("The current path has no parent path");
    }
    return this.query(false).hash('').path(pieces.join("/"));
  };
  
  UrlGrey.prototype.rawChild = function(suffixes){
    if (suffixes){
      suffixes = argsArray(arguments);
      return this.query(false).hash('').rawPath(this.path(), suffixes);
    } else {
      // if no suffix, return the child
      var pieces = this.path().split("/");
      var last = arrLast(pieces);
      if ((pieces.length > 1) && (last === '')){
        // ignore trailing slashes
        pieces.pop();
        last = arrLast(pieces);
      }
      return last;
    }
  };
  
  UrlGrey.prototype.child = function(suffixes){
    suffixes = argsArray(arguments);
    if (suffixes.length > 0){
      return this.query(false).hash('').path(this.path(), suffixes);
    }
  
    // if no suffix, return the child
    var pieces = pathPieces(this.path());
    var last = arrLast(pieces);
    if ((pieces.length > 1) && (last === '')){
      // ignore trailing slashes
      pieces.pop();
      last = arrLast(pieces);
    }
    return last;
  };
  
  UrlGrey.prototype.toJSON = function(){
    return this.toString();
  };
  
  UrlGrey.prototype.toString = function(){
    var p = this.parsed();
    var retval = this.protocol() + '://';
    if (this.protocol() !== 'file'){
      var userinfo = p.username + ':' + p.password;
      if (userinfo !== ':'){
        retval += userinfo + '@';
      }
      retval += p.hostname;
      var port = portString(this);
      if (port !== ''){
        retval += ':' + port;
      }
    }
    retval += this.path() === '/' ? '' : this.path();
    var qs = this.queryString();
    if (qs){
      retval += '?' + qs;
    }
    if (p.hash){
      retval += '#' + p.hash;
    }
    return retval;
  };
  
  var pathPieces = function(path){
    var pieces = path.split('/');
    for(var i = 0; i < pieces.length; i++){
      pieces[i] = decode(pieces[i]);
    }
    return pieces;
  };
  
  var decode = function(str){
    try {
      return decodeURIComponent(str);
    } catch (ex) {
      return querystring.unescape(str);
    }
  };
  
  var portString = function(o){
    if (o.protocol() === 'https'){
      if (o.port() === 443){
        return '';
      }
    }
    if (o.protocol() === 'http'){
      if (o.port() === 80){
        return '';
      }
    }
    return '' + o.port();
  };
  
  
  /*
  UrlGrey.prototype.absolute = function(path){
    if (path[0] == '/'){
      path = path.substring(1);
    }
    var parsed = urlParse(path);
    if (!!parsed.protocol){  // if it's already absolute, just return it
      return path;
    }
    return this._protocol + "://" + this._host + '/' + path;
  };
  
  // TODO make this interpolate vars into the url.   both sinatra style and url-tempates
  // TODO name this: 
  UrlGrey.prototype.get = function(nameOrPath, varDict){
    if (!!nameOrPath){
      if (!!varDict){
        return this.absolute(this._router.getUrl(nameOrPath, varDict));
      }
      return this.absolute(this._router.getUrl(nameOrPath));
    }
    return this.url;
  };*/
  
  /*
  // TODO needs to take a template as an input
  UrlGrey.prototype.param = function(key, defaultValue){
    var value = this.params()[key];
    if (!!value) { 
      return value; 
    }
    return defaultValue;
  };
  
  // TODO extract params, given a template?
  // TODO needs to take a template as an input
  UrlGrey.prototype.params = function(inUrl){
    if (!!inUrl){
      return this._router.pathVariables(inUrl);
    }
    if (!!this._params){
      return this._params;
    }
    return this._router.pathVariables(this.url);
  };
  */
  
  // TODO relative()  // takes an absolutepath and returns a relative one
  // TODO absolute() // takes a relative path and returns an absolute one.
  
  
  
  module.exports = function(url){ return new UrlGrey(url); };
  
  function addPropertyGetterSetter(propertyName, methodName){
    if (!methodName){
      methodName = propertyName;
    }
    UrlGrey.prototype[methodName] = function(str){
      if (!str && str !== "") {
        return this.parsed()[propertyName];  
      } else {
        var obj = new UrlGrey(this.toString());
        obj.parsed()[propertyName] = str;
        return obj;
      }
    };
  }
  
  
  
  
  