text: var url = require( "url" ),
  	xmlrpc = require( "xmlrpc" ),
  	fieldMap = require( "./fields" );
  
  // http://codex.wordpress.org/XML-RPC_Support
  // http://codex.wordpress.org/XML-RPC_WordPress_API
  
  function extend( a, b ) {
  	for ( var p in b ) {
  		a[ p ] = b[ p ];
  	}
  
  	return a;
  }
  
  function Client( settings ) {
  	[ "url", "username", "password" ].forEach(function( prop ) {
  		if ( !settings.hasOwnProperty( prop ) ) {
  			throw new Error( "Missing required setting: " + prop );
  		}
  	});
  	var parsedUrl = Client.parseUrl( settings.url );
  	this.rpc = xmlrpc[ parsedUrl.secure ? "createSecureClient" : "createClient" ]({
  		headers: settings.headers,
  		host: parsedUrl.host,
  		port: parsedUrl.port,
  		path: parsedUrl.path,
  		basic_auth: {
  			user: settings.httpAuthUser,
  			pass: settings.httpAuthPassword
  		}
  	});
  
  	this.blogId = settings.blogId || 0;
  	this.username = settings.username;
  	this.password = settings.password;
  }
  
  Client.parseUrl = function( wpUrl ) {
  	var urlParts, secure;
  
  	// allow URLs without a protocol
  	if ( !(/\w+:\/\//.test( wpUrl ) ) ) {
  		wpUrl = "http://" + wpUrl;
  	}
  	urlParts = url.parse( wpUrl );
  	secure = urlParts.protocol === "https:";
  
  	return {
  		host: urlParts.hostname,
  		port: urlParts.port || (secure ? 443 : 80),
  		path: urlParts.path.replace( /\/+$/, "" ) + "/xmlrpc.php",
  		secure: secure
  	};
  };
  
  extend( Client.prototype, {
  	call: function( method ) {
  		var args = [].slice.call( arguments, 1 ),
  			fn = args.pop();
  
  		if ( typeof fn !== "function" ) {
  			args.push( fn );
  			fn = null;
  		}
  
  		this.rpc.methodCall( method, args, function( error ) {
  			if ( error && error.code === "ENOTFOUND" && error.syscall === "getaddrinfo" ) {
  				error.message = "Unable to connect to WordPress.";
  				return fn( error );
  			}
  			fn.apply( this, arguments );
  		});
  	},
  
  	authenticatedCall: function() {
  		var args = [].slice.call( arguments );
  		args.splice( 1, 0, this.blogId, this.username, this.password );
  		this.call.apply( this, args );
  	},
  	
  	credentialsCall: function() {
  		var args = [].slice.call( arguments );
  		args.splice( 1, 0, this.username, this.password );
  		this.call.apply( this, args );
  	},
  
  	listMethods: function( fn ) {
  		this.call( "system.listMethods", fn );
  	}
  });
  
  extend( Client.prototype, {
  	getPost: function( id, fields, fn ) {
  		if ( typeof fields === "function" ) {
  			fn = fields;
  			fields = null;
  		}
  
  		if ( fields ) {
  			fields = fieldMap.array( fields, "post" );
  		}
  
  		this.authenticatedCall( "wp.getPost", id, fields, function( error, post ) {
  			if ( error ) {
  				return fn( error );
  			}
  
  			fn( null, fieldMap.from( post, "post" ) );
  		});
  	},
  
  	getPosts: function( filter, fields, fn ) {
  		if ( typeof filter === "function" ) {
  			fn = filter;
  			fields = null;
  			filter = {};
  		}
  
  		if ( typeof fields === "function" ) {
  			fn = fields;
  			fields = null;
  		}
  
  		if ( filter.type ) {
  			filter.post_type = filter.type;
  			delete filter.type;
  		}
  		if ( filter.status ) {
  			filter.post_status = filter.status;
  			delete filter.status;
  		}
  		if ( filter.orderby ) {
  			filter.orderby = fieldMap.array( [ filter.orderby ], "post" )[ 0 ];
  		}
  
  		if ( fields ) {
  			fields = fieldMap.array( fields, "post" );
  		}
  
  		this.authenticatedCall( "wp.getPosts", filter, fields, function( error, posts ) {
  			if ( error ) {
  				return fn( error );
  			}
  
  			fn( null, posts.map(function( post ) {
  				return fieldMap.from( post, "post" );
  			}));
  		});
  	},
  
  	newPost: function( data, fn ) {
  		this.authenticatedCall( "wp.newPost", fieldMap.to( data, "post" ), fn );
  	},
  
  	// to remove a term, just set the terms and leave out the id that you want to remove
  	// to remove a custom field, pass the id with no key or value
  	editPost: function( id, data, fn ) {
  		this.authenticatedCall( "wp.editPost", id, fieldMap.to( data, "post" ), fn );
  	},
  
  	deletePost: function( id, fn ) {
  		this.authenticatedCall( "wp.deletePost", id, fn );
  	},
  
  	getPostType: function( name, fn ) {
  		// TODO: fields
  		this.authenticatedCall( "wp.getPostType", name, function( error, postType ) {
  			if ( error ) {
  				return fn( error );
  			}
  
  			fn( null, fieldMap.from( postType, "postType" ) );
  		});
  	},
  
  	getPostTypes: function( fn ) {
  		// TODO: filter
  		// TODO: fields
  		this.authenticatedCall( "wp.getPostTypes", function( error, postTypes ) {
  			if ( error ) {
  				return fn( error );
  			}
  
  			Object.keys( postTypes ).forEach(function( postType ) {
  				postTypes[ postType ] = fieldMap.from( postTypes[ postType ], "postType" );
  			});
  			fn( null, postTypes );
  		});
  	}
  });
  
  extend( Client.prototype, {
  	getTaxonomy: function( name, fn ) {
  		this.authenticatedCall( "wp.getTaxonomy", name, function( error, taxonomy ) {
  			if ( error ) {
  				return fn( error );
  			}
  
  			fn( null, fieldMap.from( taxonomy, "taxonomy" ) );
  		});
  	},
  
  	getTaxonomies: function( fn ) {
  		this.authenticatedCall( "wp.getTaxonomies", function( error, taxonomies ) {
  			if ( error ) {
  				return fn( error );
  			}
  
  			fn( null, taxonomies.map(function( taxonomy ) {
  				return fieldMap.from( taxonomy, "taxonomy" );
  			}));
  		});
  	},
  
  	getTerm: function( taxonomy, id, fn ) {
  		this.authenticatedCall( "wp.getTerm", taxonomy, id, function( error, term ) {
  			if ( error ) {
  				return fn( error );
  			}
  
  			fn( null, fieldMap.from( term, "term" ) );
  		});
  	},
  
  	getTerms: function( taxonomy, filter, fn ) {
  		if ( typeof filter === "function" ) {
  			fn = filter;
  			filter = {};
  		}
  
  		if ( filter.hideEmpty ) {
  			filter.hide_empty = filter.hideEmpty;
  			delete filter.hideEmpty;
  		}
  
  		if ( filter.orderby ) {
  			filter.orderby = fieldMap.array( [ filter.orderby ], "term" )[ 0 ];
  		}
  
  		this.authenticatedCall( "wp.getTerms", taxonomy, filter, function( error, terms ) {
  			if ( error ) {
  				return fn( error );
  			}
  
  			fn( null, terms.map(function( term ) {
  				return fieldMap.from( term, "term" );
  			}));
  		});
  	},
  
  	newTerm: function( data, fn ) {
  		this.authenticatedCall( "wp.newTerm", fieldMap.to( data, "term" ), fn );
  	},
  
  	editTerm: function( id, data, fn ) {
  		this.authenticatedCall( "wp.editTerm", id, fieldMap.to( data, "term" ), fn );
  	},
  
  	deleteTerm: function( taxonomy, id, fn ) {
  		this.authenticatedCall( "wp.deleteTerm", taxonomy, id, fn );
  	}
  });
  
  extend( Client.prototype, {
  	getAuthors: function( fn ) {
  		this.authenticatedCall( "wp.getAuthors", function ( error, authors ) {
  			if ( error ) { 
  				return fn( error ); 
  			}
  
  			fn( null, authors.map(function( author ) {
  				return fieldMap.from( author, "author" );
  			}));
  		});
  	},
  
  	uploadFile: function ( data, fn ) {
  		this.authenticatedCall( "wp.uploadFile", fieldMap.to( data, "uploadFile" ), fn );
  	},
  
  	getUsersBlogs: function( fn ) {
  		this.credentialsCall( "wp.getUsersBlogs", fn);
  	},
  
    editImageMeta: function ( id, data, fn ) {
      this.authenticatedCall( "nc.editImageMeta", id, fieldMap.to( data, "editImageMeta" ), fn );
    }
  });
  
  module.exports = {
  	Client: Client,
  
  	createClient: function( settings ) {
  		return new Client( settings );
  	},
  
  	fieldMap: fieldMap
  };
  