/*
 * Copyright (c) 2012-2015 Kaarle Ritvanen
 * See LICENSE file for license details
 */

define(
    ["aconf/blocking", "aconf/path", "aconf/type", "jquery", "underscore"],
    function(blocking, pth, type, $, _) {
	return function(token, saveRequired) {
	    var txnMgr = {};

	    var txn, changed, invalid;

	    function reset() {
		txn = null;
		changed = {};
		invalid = {};
	    }
	    reset();

	    function isValid() { return !(_.size(invalid)); }

	    function request(url, options) {
		options = options || {};
		options.headers = {"X-AConf-Auth-Token": token};
		if (txn) options.headers["X-AConf-Transaction-ID"] = txn;
		if (options.data != undefined)
		    options.data = JSON.stringify(options.data);
		return $.ajax(encodeURI(url), options);
	    }

	    txnMgr.abort = function() {
		request("/transaction", {type: "DELETE"});
		reset();
	    }

	    function objRequest(path, options) {
		return request("/config" + path, options);
	    }

	    function exclusive(task) {
		blocking.enable();

		var def = $.Deferred();
		function resolve(txnValid) { def.resolve(txnValid); }
		function reject() { def.reject(); }

		var tasks = _.filter(
		    _.pluck(_.values(invalid), 1), function(d) {
			return d && d.state() == "pending";
		    }
		);

		if (tasks.length)
		    tasks[0].always(function() {
			exclusive(task).done(resolve).fail(reject);
		    });
		else task().always(blocking.disable).done(resolve).fail(reject);

		return def;
	    }

	    txnMgr.query = function(path) {
		var def = $.Deferred();

		objRequest(path).done(function(data) {
		    data.txnMgr = txnMgr;

		    if (type.isTreeNode(data.meta) && !_.size(data.data))
			data.data = type.isList(data.meta) ? [] : {};

		    if (!_.isArray(data.meta.removable))
			data.meta.removable = [];

		    function index(name) {
			return _.isArray(data.data) ? name - 1 : name;
		    }

		    function ensureKeyPresence(name) {
			var key = index(name);
			if (!(key in data.data)) data.data[key] = null;
			if (data.data[key] == null)
			    data.meta.removable.push(name);
		    }

		    function findSubordinateKeys(objmap, path) {
			return _.filter(_.keys(objmap), function(p) {
			    return pth.isSubordinate(p, path);
			});
		    }

		    function invalidSubordinates() {
			return findSubordinateKeys(invalid, path);
		    }

		    var set = data.meta.type == "set";

		    if (type.isCollection(data.meta) && !set) {
			var level = pth.split(path).length;
			_.each(invalidSubordinates(), function(p) {
			    ensureKeyPresence(pth.split(p)[level]);
			});
		    }

		    data.get = function(name, valid) {
			var p = pth.join(path, name);
			if (!valid && p in invalid) return invalid[p][0];

			if (data.meta.type == "set")
			    return _.contains(data.data, name) ? name : null;

			return data.data[index(name)];
		    };

		    data.metaRequest = function(name) {
			return request("/meta" + pth.join(path, name));
		    };

		    data.match = function(filter) {
			if (!filter) return true;
			return _.every(_.map(filter, function(values, key) {
			    return _.contains(values, data.data[key]);
			}));
		    };

		    data.status = function(name) {
			var p = name ? pth.join(path, name) : path;
			function scan(objmap) {
			    return _.size(findSubordinateKeys(objmap, p));
			}

			if (scan(invalid)) return "invalid";
			if (scan(changed)) return "changed";
			return null;
		    };

		    data.isSubtreeValid = function() {
			return !_.size(invalidSubordinates(invalid, path));
		    };

		    data.validate = function() {
			var valid = true;
			if (data.meta.required) {
			    valid = _.size(data.data);
			    if (valid) delete invalid[path];
			    else invalid[path] = [path];
			}
			return valid && isValid();
		    };

		    data.set = function(name, newValue) {
			var def = $.Deferred();
			function reject(xhr) { def.reject(xhr); }

			var mpath = pth.join(path, name);

			ensureKeyPresence(name);
			var value = data.get(name, true);

			var empty = newValue === null &&
			    type.isCollection(data.meta);
			if (newValue === undefined) newValue = null;
			var tn = _.isObject(newValue);
			var npv = tn ? mpath : newValue;

			var prevTask = mpath in invalid &&
			    invalid[mpath][1] || $.Deferred().resolve();

			invalid[mpath] = [npv, def];

			function ignore(path) {
			    _.each(_.keys(invalid), function(p) {
				if (pth.isSubordinate(p, path))
				    delete invalid[p];
			    });
			}

			function resolve() {
			    if (mpath in invalid &&
				invalid[mpath][1] == def) {

				var del = invalid[mpath][0] == null;

				delete invalid[mpath];
				if (del) ignore(mpath);
			    }

			    def.resolve(isValid());
			}

			prevTask.always(function() {
			    if (empty) {
				def.reject("Value not set");
				return;
			    }

			    var del = newValue == null;

			    var options;
			    if (!del)
				options = {
				    type: set ? "POST" : "PUT", data: newValue
				};
			    else if (data.get(name, true) != null)
				options = {type: "DELETE"};

			    if (!options) {
				if (data.meta.type == "model" &&
				    _.findWhere(
					data.meta.fields, {name: name}
				    ).required)
				    def.reject("Required value not set");
				else resolve();
				return;
			    }
			    
			    objRequest(
				set && !del ? path : mpath, options
			    ).done(function() {
				if (!(mpath in changed))
				    changed[mpath] = value;
				if (!tn && newValue == changed[mpath])
				    delete changed[mpath];

				if (npv == null)
				    _.each(
					_.keys(changed),
					function(p) {
					    if (pth.isSubordinate(
						p, mpath, true
					    ))
						delete changed[p];
					}
				    );

				if (data.meta.type == "list" && del)
				    data.data.splice(name - 1, 1);
				else if (!set) data.data[index(name)] = npv;
				else if (del) data.data = _.without(data.data, name);
				else data.data.push(name);

				data.validate();

				if (data.meta.type == "model")
				    _.each(
					data.meta.fields, function(field) {
					    if (field.condition &&
						field.condition[name] &&
						!_.contains(
						    field.condition[name],
						    newValue
						))
						ignore(
						    pth.join(path, field.name)
						);
					}
				    );

				if (tn && !set)
				    txnMgr.query(mpath).done(function(data) {

					if (mpath in invalid) {
					    if (data.meta.type == "model")
						_.each(
						    data.meta.fields,
						    function(field) {
							var mmpath = pth.join(
							    mpath,
							    field.name
							);
							if (field.required &&
							    data.match(
								field.condition
							    ) &&
							    !(mmpath in invalid) &&
							    (type.isCollection(
								field
							    ) || data.get(
								field.name
							    ) == null)) {
							    invalid[mmpath] = [
								null
							    ];
							}
						    });

					    else if (
						type.isCollection(data.meta) &&
						    data.meta.required
					    )
						invalid[mpath] = [mpath];
					}

					resolve();
				    }).fail(reject);

				else resolve();

			    }).fail(reject);
			});
			
			return def;
		    };

		    data.add = function(name) {
			return data.set(name, name);
		    };

		    function adjustListIndex(oldIndex, newIndex) {
			var opath = pth.join(path, oldIndex);
			var npath = pth.join(path, newIndex);
			_.each(
			    [changed, invalid],
			    function(map) {
				_.each(
				    _.keys(map),
				    function(p) {
					if (pth.isSubordinate(
					    p, opath
					)) {
					    map[npath + p.substring(
						opath.length
					    )] = map[p];
					    delete map[p];
					}
				    });
			    }
			);
		    }

		    function adjustListIndices(start, end) {
			var offset = start < end ? 1 : -1;
			for (var i = start; i != end; i += offset)
			    adjustListIndex(i + offset, i);
		    }

		    function _delete(name) {
			var def = $.Deferred();
			var length = data.data.length;

			data.set(name).done(function(txnValid) {
			    if (type.isTreeNode(data.meta) &&
				data.meta.type != "set") {

				delete changed[pth.join(path, name)];
				changed[path] = path;

				if (data.meta.type == "list")
				    adjustListIndices(name, length);
			    }
			    def.resolve(txnValid);

			}).fail(function() { def.reject(); });

			return def;
		    }

		    data.delete = function(name) {
			return exclusive(function() { return _delete(name); });
		    };

		    data.move = function(oldIndex, newIndex) {
			if (oldIndex == newIndex)
			    return $.Deferred().resolve(isValid());

			var value = data.get(oldIndex);
			var length = data.data.length;

			return exclusive(function() {
			    var def = $.Deferred();
			    function reject() { def.reject(); }

			    if (oldIndex > newIndex) oldIndex++;
			    else newIndex++;

			    objRequest(path, {type: "POST", data: {
				index: newIndex,
				data: type.isTreeNode(data.meta.members) ?
				    pth.join(path, oldIndex) : value
			    }}).done(function() {

				data.data.splice(newIndex - 1, 0, value);

				adjustListIndices(length + 1, newIndex);
				adjustListIndex(oldIndex, newIndex);

				_delete(oldIndex)
				    .done(function(txnValid) {
					def.resolve(txnValid);
				    })
				    .fail(reject);

			    }).fail(reject);

			    return def;
			});
		    };

		    data.invoke = function(name) {
			return objRequest(pth.join(path, name), {type: "POST"});
		    };

		    def.resolve(data);
		}).fail(function() { def.reject(); });

		return def;
	    }

	    txnMgr.start = function() {
		var def = $.Deferred();
		if (txn && isValid() && !(_.size(changed))) txnMgr.abort();

		if (txn) def.resolve();

		else request("/transaction", {type: "POST"})
		    .done(function(data, status, xhr) {
			txn = xhr.getResponseHeader("X-AConf-Transaction-ID");
			def.resolve();
		    })
		    .fail(function() { def.reject(); });

		return def;
	    };

	    txnMgr.commit = function() {
		var def = $.Deferred();
		function reject(xhr) { def.reject(xhr); }
		request("/transaction", {type: "PUT"}).done(function() {
		    reset();
		    if (saveRequired)
			request("/save", {type: "POST"}).done(function() {
			    def.resolve();
			}).fail(reject);
		    else def.resolve();
		}).fail(reject);
		return def;
	    };

	    txnMgr.logout = function() {
		return request("/login", {type: "DELETE"});
	    };

	    return txnMgr;
	}
    }
);
