# Source: NodeApiClient.js

    "use strict";

    // Tell JSHint about EventSource
    /*global
    	EventSource
    */

    // Import external names locally
    var Shared = require('./Shared'),
    	Core,
    	CoreInit,
    	Collection,
    	NodeApiClient,
    	Overload;

    /**
     * The NodeApiClient class.
     * @class
     * @constructor
     */
    NodeApiClient = function () {
    	this.init.apply(this, arguments);
    };

    /**
     * The init method that can be overridden or extended.
     * @param {Core} core The ForerunnerDB core instance.
     */
    NodeApiClient.prototype.init = function (core) {
    	var self = this;
    	self._core = core;

    	self.rootPath('/fdb');
    };

    Shared.addModule('NodeApiClient', NodeApiClient);
    Shared.mixin(NodeApiClient.prototype, 'Mixin.Common');
    Shared.mixin(NodeApiClient.prototype, 'Mixin.Events');
    Shared.mixin(NodeApiClient.prototype, 'Mixin.ChainReactor');

    Core = Shared.modules.Core;
    CoreInit = Core.prototype.init;
    Collection = Shared.modules.Collection;
    Overload = Shared.overload;

    /**
     * Gets / sets the rootPath for the client to use in each API call.
     * @name rootPath
     * @method NodeApiClient.rootPath
     * @param {String=} val The path to set.
     * @returns {*}
     */
    Shared.synthesize(NodeApiClient.prototype, 'rootPath');

    /**
     * Set the url of the server to use for API.
     * @param {String} host The server host name including protocol. E.g.
     * "https://0.0.0.0".
     * @param {String} port The server port number e.g. "8080".
     */
    NodeApiClient.prototype.server = function (host, port) {
    	if (host !== undefined) {
    		if (host.substr(host.length - 1, 1) === '/') {
    			// Strip trailing /
    			host = host.substr(0, host.length - 1);
    		}

    		if (port !== undefined) {
    			this._server = host + ":" + port;
    		} else {
    			this._server = host;
    		}

    		this._host = host;
    		this._port = port;

    		return this;
    	}

    	if (port !== undefined) {
    		return {
    			host: this._host,
    			port: this._port,
    			url: this._server
    		};
    	} else {
    		return this._server;
    	}
    };

    NodeApiClient.prototype.http = function (method, url, data, options, callback) {
    	var self = this,
    		finalUrl,
    		sessionData,
    		bodyData,
    		xmlHttp = new XMLHttpRequest();

    	method = method.toUpperCase();

    	xmlHttp.onreadystatechange = function () {
    		if (xmlHttp.readyState === 4) {
    			if (xmlHttp.status === 200) {
    				// Tell the callback about success
    				if (xmlHttp.responseText) {
    					callback(false, self.jParse(xmlHttp.responseText));
    				} else {
    					callback(false, {});
    				}
    			} else if (xmlHttp.status === 204) {
    				callback(false, {});
    			} else {
    				// Tell the callback about the error
    				callback(xmlHttp.status, xmlHttp.responseText);

    				// Emit the error code
    				self.emit('httpError', xmlHttp.status, xmlHttp.responseText);
    			}
    		}
    	};

    	switch (method) {
    		case 'GET':
    		case 'DELETE':
    		case 'HEAD':
    			// Check for global auth
    			if (this._sessionData) {
    				data = data !== undefined ? data : {};

    				// Add the session data to the key specified
    				data[this._sessionData.key] = this._sessionData.obj;
    			}

    			finalUrl = url + (data !== undefined ? '?' + self.jStringify(data) : '');
    			bodyData = null;
    			break;

    		case 'POST':
    		case 'PUT':
    		case 'PATCH':
    			// Check for global auth
    			if (this._sessionData) {
    				sessionData = {};

    				// Add the session data to the key specified
    				sessionData[this._sessionData.key] = this._sessionData.obj;
    			}

    			finalUrl = url + (sessionData !== undefined ? '?' + self.jStringify(sessionData) : '');
    			bodyData = (data !== undefined ? self.jStringify(data) : null);
    			break;

    		default:
    			return false;
    	}

    	xmlHttp.open(method, finalUrl, true);
    	xmlHttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
    	xmlHttp.send(bodyData);

    	return this;
    };

    // Define HTTP helper methods
    NodeApiClient.prototype.head = new Overload({
    	'string, function': function (path, callback) {
    		return this.$main.call(this, path, undefined, {}, callback);
    	},

    	'string, *, function': function (path, data, callback) {
    		return this.$main.call(this, path, data, {}, callback);
    	},

    	'string, *, object, function': function (path, data, options, callback) {
    		return this.$main.call(this, path, data, options, callback);
    	},

    	'$main': function (path, data, options, callback) {
    		return this.http('HEAD', this.server() + this._rootPath + path, data, options, callback);
    	}
    });

    NodeApiClient.prototype.get = new Overload({
    	'string, function': function (path, callback) {
    		return this.$main.call(this, path, undefined, {}, callback);
    	},

    	'string, *, function': function (path, data, callback) {
    		return this.$main.call(this, path, data, {}, callback);
    	},

    	'string, *, object, function': function (path, data, options, callback) {
    		return this.$main.call(this, path, data, options, callback);
    	},

    	'$main': function (path, data, options, callback) {
    		return this.http('GET', this.server() + this._rootPath + path, data, options, callback);
    	}
    });

    NodeApiClient.prototype.put = new Overload({
    	'string, function': function (path, callback) {
    		return this.$main.call(this, path, undefined, {}, callback);
    	},

    	'string, *, function': function (path, data, callback) {
    		return this.$main.call(this, path, data, {}, callback);
    	},

    	'string, *, object, function': function (path, data, options, callback) {
    		return this.$main.call(this, path, data, options, callback);
    	},

    	'$main': function (path, data, options, callback) {
    		return this.http('PUT', this.server() + this._rootPath + path, data, options, callback);
    	}
    });

    NodeApiClient.prototype.post = new Overload({
    	'string, function': function (path, callback) {
    		return this.$main.call(this, path, undefined, {}, callback);
    	},

    	'string, *, function': function (path, data, callback) {
    		return this.$main.call(this, path, data, {}, callback);
    	},

    	'string, *, object, function': function (path, data, options, callback) {
    		return this.$main.call(this, path, data, options, callback);
    	},

    	'$main': function (path, data, options, callback) {
    		return this.http('POST', this.server() + this._rootPath + path, data, options, callback);
    	}
    });

    NodeApiClient.prototype.patch = new Overload({
    	'string, function': function (path, callback) {
    		return this.$main.call(this, path, undefined, {}, callback);
    	},

    	'string, *, function': function (path, data, callback) {
    		return this.$main.call(this, path, data, {}, callback);
    	},

    	'string, *, object, function': function (path, data, options, callback) {
    		return this.$main.call(this, path, data, options, callback);
    	},

    	'$main': function (path, data, options, callback) {
    		return this.http('PATCH', this.server() + this._rootPath + path, data, options, callback);
    	}
    });

    NodeApiClient.prototype.postPatch = function (path, id, data, options, callback) {
    	var self = this;

    	// Determine if the item exists or not
    	this.head(path + '/' + id, undefined, {}, function (err, headData) {
    		if (err) {
    			if (err === 404) {
    				// Item does not exist, run post
    				return self.http('POST', self.server() + self._rootPath + path, data, options, callback);
    			} else {
    				callback(err, data);
    			}
    		} else {
    			// Item already exists, run patch
    			return self.http('PATCH', self.server() + self._rootPath + path + '/' + id, data, options, callback);
    		}
    	});
    };

    NodeApiClient.prototype.delete = new Overload({
    	'string, function': function (path, callback) {
    		return this.$main.call(this, path, undefined, {}, callback);
    	},

    	'string, *, function': function (path, data, callback) {
    		return this.$main.call(this, path, data, {}, callback);
    	},

    	'string, *, object, function': function (path, data, options, callback) {
    		return this.$main.call(this, path, data, options, callback);
    	},

    	'$main': function (path, data, options, callback) {
    		return this.http('DELETE', this.server() + this._rootPath + path, data, options, callback);
    	}
    });

    /**
     * Gets/ sets a global object that will be sent up with client
     * requests to the API or REST server.
     * @param {String} key The key to send the session object up inside.
     * @param {*} obj The object / value to send up with all requests. If
     * a request has its own data to send up, this session data will be
     * mixed in to the request data under the specified key.
     */
    NodeApiClient.prototype.session = function (key, obj) {
    	if (key !== undefined && obj !== undefined) {
    		this._sessionData = {
    			key: key,
    			obj: obj
    		};
    		return this;
    	}

    	return this._sessionData;
    };

    /**
     * Initiates a client connection to the API server.
     * @param collectionInstance
     * @param path
     * @param query
     * @param options
     * @param callback
     */
    NodeApiClient.prototype.sync = function (collectionInstance, path, query, options, callback) {
    	var self = this,
    		source,
    		finalPath,
    		queryParams,
    		queryString = '',
    		connecting = true;

    	if (this.debug()) {
    		console.log(this.logIdentifier() + ' Connecting to API server ' + this.server() + this._rootPath + path);
    	}

    	finalPath = this.server() + this._rootPath + path + '/_sync';

    	// Check for global auth
    	if (this._sessionData) {
    		queryParams = queryParams || {};

    		if (this._sessionData.key) {
    			// Add the session data to the key specified
    			queryParams[this._sessionData.key] = this._sessionData.obj;
    		} else {
    			// Add the session data to the root query object
    			Shared.mixin(queryParams, this._sessionData.obj);
    		}
    	}

    	if (query) {
    		queryParams = queryParams || {};
    		queryParams.$query = query;
    	}

    	if (options) {
    		queryParams = queryParams || {};
    		if (options.$initialData === undefined) {
    			options.$initialData = true;
    		}

    		queryParams.$options = options;
    	}

    	if (queryParams) {
    		queryString = this.jStringify(queryParams);
    		finalPath += '?' + queryString;
    	}

    	source = new EventSource(finalPath);
    	collectionInstance.__apiConnection = source;

    	source.addEventListener('open', function (e) {
    		if (!options || (options && options.$initialData)) {
    			// The connection is open, grab the initial data
    			self.get(path, queryParams, function (err, data) {
    				if (!err) {
    					collectionInstance.upsert(data);
    				}
    			});
    		}
    	}, false);

    	source.addEventListener('error', function (e) {
    		if (source.readyState === 2) {
    			// The connection is dead, remove the connection
    			collectionInstance.unSync();
    		}

    		if (connecting) {
    			connecting = false;
    			callback(e);
    		}
    	}, false);

    	source.addEventListener('insert', function(e) {
    		var data = self.jParse(e.data);
    		collectionInstance.insert(data.dataSet);
    	}, false);

    	source.addEventListener('update', function(e) {
    		var data = self.jParse(e.data);
    		collectionInstance.update(data.query, data.update);
    	}, false);

    	source.addEventListener('remove', function(e) {
    		var data = self.jParse(e.data);
    		collectionInstance.remove(data.query);
    	}, false);

    	if (callback) {
    		source.addEventListener('connected', function (e) {
    			if (connecting) {
    				connecting = false;
    				callback(false);
    			}
    		}, false);
    	}
    };

    Collection.prototype.sync = new Overload({
    	/**
    	 * Sync with this collection on the server-side.
    	 * @name sync
    	 * @method Collection.sync
    	 * @param {Function} callback The callback method to call once
    	 * the connection to the server has been established.
    	 */
    	'function': function (callback) {
    		this.$main.call(this, '/' + this._db.name() + '/collection/' + this.name(), null, null, callback);
    	},

    	/**
    	 * Sync with this collection on the server-side.
    	 * @name sync
    	 * @method Collection.sync
    	 * @param {String} collectionName The collection to sync from.
    	 * @param {Function} callback The callback method to call once
    	 * the connection to the server has been established.
    	 */
    	'string, function': function (collectionName, callback) {
    		this.$main.call(this, '/' + this._db.name() + '/collection/' + collectionName, null, null, callback);
    	},

    	/**
    	 * Sync with this collection on the server-side.
    	 * @name sync
    	 * @method Collection.sync
    	 * @param {Object} query A query object.
    	 * @param {Function} callback The callback method to call once
    	 * the connection to the server has been established.
    	 */
    	'object, function': function (query, callback) {
    		this.$main.call(this, '/' + this._db.name() + '/collection/' + this.name(), query, null, callback);
    	},

    	/**
    	 * Sync with this collection on the server-side.
    	 * @name sync
    	 * @method Collection.sync
    	 * @param {String} objectType The type of object to sync to e.g.
    	 * "collection" or "view".
    	 * @param {String} objectName The name of the object to sync from.
    	 * @param {Function} callback The callback method to call once
    	 * the connection to the server has been established.
    	 */
    	'string, string, function': function (objectType, objectName, callback) {
    		this.$main.call(this, '/' + this._db.name() + '/' + objectType + '/' + objectName, null, null, callback);
    	},

    	/**
    	 * Sync with this collection on the server-side.
    	 * @name sync
    	 * @method Collection.sync
    	 * @param {String} collectionName The collection to sync from.
    	 * @param {Object} query A query object.
    	 * @param {Function} callback The callback method to call once
    	 * the connection to the server has been established.
    	 */
    	'string, object, function': function (collectionName, query, callback) {
    		this.$main.call(this, '/' + this._db.name() + '/collection/' + collectionName, query, null, callback);
    	},

    	/**
    	 * Sync with this collection on the server-side.
    	 * @name sync
    	 * @method Collection.sync
    	 * @param {String} objectType The type of object to sync to e.g.
    	 * "collection" or "view".
    	 * @param {String} objectName The name of the object to sync from.
    	 * @param {Object} query A query object.
    	 * @param {Function} callback The callback method to call once
    	 * the connection to the server has been established.
    	 */
    	'string, string, object, function': function (objectType, objectName, query, callback) {
    		this.$main.call(this, '/' + this._db.name() + '/' + objectType + '/' + objectName, query, null, callback);
    	},

    	/**
    	 * Sync with this collection on the server-side.
    	 * @name sync
    	 * @method Collection.sync
    	 * @param {Object} query A query object.
    	 * @param {Object} options An options object.
    	 * @param {Function} callback The callback method to call once
    	 * the connection to the server has been established.
    	 */
    	'object, object, function': function (query, options, callback) {
    		this.$main.call(this, '/' + this._db.name() + '/collection/' + this.name(), query, options, callback);
    	},

    	/**
    	 * Sync with this collection on the server-side.
    	 * @name sync
    	 * @method Collection.sync
    	 * @param {String} collectionName The collection to sync from.
    	 * @param {Object} query A query object.
    	 * @param {Object} options An options object.
    	 * @param {Function} callback The callback method to call once
    	 * the connection to the server has been established.
    	 */
    	'string, object, object, function': function (collectionName, query, options, callback) {
    		this.$main.call(this, '/' + this._db.name() + '/collection/' + collectionName, query, options, callback);
    	},

    	/**
    	 * Sync with this collection on the server-side.
    	 * @name sync
    	 * @method Collection.sync
    	 * @param {String} objectType The type of object to sync to e.g.
    	 * "collection" or "view".
    	 * @param {String} objectName The name of the object to sync from.
    	 * @param {Object} query A query object.
    	 * @param {Object} options An options object.
    	 * @param {Function} callback The callback method to call once
    	 * the connection to the server has been established.
    	 */
    	'string, string, object, object, function': function (objectType, objectName, query, options, callback) {
    		this.$main.call(this, '/' + this._db.name() + '/' + objectType + '/' + objectName, query, options, callback);
    	},

    	'$main': function (path, query, options, callback) {
    		var self = this;

    		if (this._db && this._db._core) {
    			// Kill any existing sync connection
    			this.unSync();

    			// Create new sync connection
    			this._db._core.api.sync(this, path, query, options, callback);

    			// Hook on drop to call unsync
    			this.on('drop', function () {
    				self.unSync();
    			});
    		} else {
    			throw(this.logIdentifier() + ' Cannot sync for an anonymous collection! (Collection must be attached to a database)');
    		}
    	}
    });

    /**
     * Disconnects an existing connection to a sync server.
     * @returns {boolean} True if a connection existed, false
     * if no connection existed.
     */
    Collection.prototype.unSync = function () {
    	if (this.__apiConnection) {
    		if (this.__apiConnection.readyState !== 2) {
    			this.__apiConnection.close();
    		}

    		delete this.__apiConnection;

    		return true;
    	}

    	return false;
    };

    Collection.prototype.http = new Overload({
    	'string, function': function (method, callback) {
    		this.$main.call(this, method, '/' + this._db.name() + '/collection/' + this.name(), undefined, undefined, {}, callback);
    	},

    	'$main': function (method, path, queryObj, queryOptions, options, callback) {
    		if (this._db && this._db._core) {
    			return this._db._core.api.http('GET', this._db._core.api.server() + this._rootPath + path, {"$query": queryObj, "$options": queryOptions}, options, callback);
    		} else {
    			throw(this.logIdentifier() + ' Cannot do HTTP for an anonymous collection! (Collection must be attached to a database)');
    		}
    	}
    });

    Collection.prototype.autoHttp = new Overload({
    	'string, function': function (method, callback) {
    		this.$main.call(this, method, '/' + this._db.name() + '/collection/' + this.name(), undefined, undefined, {}, callback);
    	},

    	'string, string, function': function (method, collectionName, callback) {
    		this.$main.call(this, method, '/' + this._db.name() + '/collection/' + collectionName, undefined, undefined, {}, callback);
    	},

    	'string, string, string, function': function (method, objType, objName, callback) {
    		this.$main.call(this, method, '/' + this._db.name() + '/' + objType + '/' + objName, undefined, undefined, {}, callback);
    	},

    	'string, string, string, object, function': function (method, objType, objName, queryObj, callback) {
    		this.$main.call(this, method, '/' + this._db.name() + '/' + objType + '/' + objName, queryObj, undefined, {}, callback);
    	},

    	'string, string, string, object, object, function': function (method, objType, objName, queryObj, queryOptions, callback) {
    		this.$main.call(this, method, '/' + this._db.name() + '/' + objType + '/' + objName, queryObj, queryOptions, {}, callback);
    	},

    	'$main': function (method, path, queryObj, queryOptions, options, callback) {
    		var self = this;

    		if (this._db && this._db._core) {
    			return this._db._core.api.http('GET', this._db._core.api.server() + this._db._core.api._rootPath + path, {"$query": queryObj, "$options": queryOptions}, options, function (err, data) {
    				var i;

    				if (!err && data) {
    					// Check the type of method we used and operate on the collection accordingly
    					switch (method) {
    						// Find insert
    						case 'GET':
    							self.insert(data);
    							break;

    						// Insert
    						case 'POST':
    							if (data.inserted && data.inserted.length) {
    								self.insert(data.inserted);
    							}
    							break;

    						// Update overwrite
    						case 'PUT':
    						case 'PATCH':
    							if (data instanceof Array) {
    								// Update each document
    								for (i = 0; i < data.length; i++) {
    									self.updateById(data[i]._id, {$overwrite: data[i]});
    								}
    							} else {
    								// Update single document
    								self.updateById(data._id, {$overwrite: data});
    							}
    							break;

    						// Remove
    						case 'DELETE':
    							self.remove(data);
    							break;

    						default:
    							// Nothing to do with this method
    							break;
    					}
    				}

    				// Send the data back to the callback
    				callback(err, data);
    			});
    		} else {
    			throw(this.logIdentifier() + ' Cannot do HTTP for an anonymous collection! (Collection must be attached to a database)');
    		}
    	}
    });

    // Override the Core init to instantiate the plugin
    Core.prototype.init = function () {
    	CoreInit.apply(this, arguments);
    	this.api = new NodeApiClient(this);
    };

    Shared.finishModule('NodeApiClient');

    module.exports = NodeApiClient;