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){
    cb(obj[k], k);
  }
};

var argsArray = function(obj){
    if (!obj) return [];
    if (Array.isArray(obj)) return slice.call(obj);
    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){
  var path;
  if (mergeObject === false){
    // clear the query entirely if the input === false
    return this.queryString('');
  }
  
  var url = this.url;
  if (!mergeObject){
    var parsed = urlParse(url);
    if (!!parsed.search){
      var qstr = parsed.search.substring(1);
      var output = querystring.parse(qstr);

      return output;
    }
    return {};
  } 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 newString = querystring.stringify(oldQuery);
    return this.queryString(newString);
  }
};

  
UrlGrey.prototype.rawQuery = function(mergeObject){
  var path;
  if (mergeObject === false){
    // clear the query entirely if the input === false
    return this.queryString('');
  }
  
  var url = this.url;
  if (!mergeObject){
    var parsed = urlParse(url);
    if (!!parsed.search){
      var qstr = parsed.search.substring(1);
      return querystring.parse(qstr);
    }
    return {};
  } 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 === ''){
      var obj = new UrlGrey(this.toString());
      obj.parsed()[propertyName] = str;
      return obj;
    }
    return this.parsed()[propertyName];  
  };
}




