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

angular.module("aconf").factory(
    "aconfTxn",
    function($http, $q, aconfExclusive, aconfPath, aconfType) {
	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.url = url;
		options.headers = {"X-AConf-Auth-Token": token};
		if (txn) options.headers["X-AConf-Transaction-ID"] = txn;
		options.data = options.data == null ? "" :
		    JSON.stringify(options.data);
		return $http(options);
	    }

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

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

	    function exclusive(task) {
		function attempt() {
		    return $q(function(resolve, reject) {
			function postpone() {
			    attempt().then(resolve, reject);
			}

			var tasks = _.filter(
			    _.pluck(_.values(invalid), 1),
			    function(task) { return task; }
			);

			if (tasks.length) tasks[0].then(postpone, postpone);
			else task().then(resolve, reject);
		    });
		}

		return aconfExclusive(attempt);
	    }

	    txnMgr.query = function(path) {
		return $q(function(resolve, reject) {
		    objRequest(path).success(function(data) {
			data.txnMgr = txnMgr;

			if (aconfType.isTreeNode(data.meta) &&
			    !_.size(data.data))
			    data.data = aconfType.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 aconfPath.isSubordinate(p, path);
			    });
			}

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

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

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

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

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

			    var res = data.data[index(name)];
			    return res === undefined ? null : res;
			};

			data.metaRequest = function(name) {
			    return request("/meta" + aconfPath.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 = aconfPath.join(path, name);
			    function scan(objmap) {
				return _.size(findSubordinateKeys(objmap, p));
			    }

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

			data.isNodeValid = function() {
			    return !(path in invalid);
			};

			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 = $q.defer();
			    function reject(resp) {
				if (mpath in invalid &&
				    invalid[mpath][1] == def.promise)
				    invalid[mpath][1] = null;
				def.reject(resp);
			    }

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

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

			    var empty = newValue === null &&
				aconfType.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] || $q.when();

			    invalid[mpath] = [npv, def.promise];

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

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

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

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

				def.resolve(isValid());
			    }

			    function newTask() {
				if (empty) {
				    reject("Value not set");
				    return;
				}

				var del = newValue == null;

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

				if (!options) {
				    if (data.meta.type == "model" &&
					_.findWhere(
					    data.meta.fields, {name: name}
					).required)
					reject("Required value not set");
				    else resolve();
				    return;
				}

				objRequest(
				    set && !del ? path : mpath, options
				).then(function() {
				    if (!(mpath in changed))
					changed[mpath] = value;
				    if (!tn && newValue == changed[mpath])
					delete changed[mpath];

				    if (del)
					_.each(
					    _.keys(changed),
					    function(p) {
						if (aconfPath.isSubordinate(
						    p, mpath, true
						))
						    delete changed[p];
					    }
					);

				    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(
							aconfPath.join(
							    path, field.name
							)
						    );
					    }
					);

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

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

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

					    resolve();
					}, reject);

				    else resolve();

				}, reject);
			    }

			    prevTask.then(newTask, newTask);

			    return def.promise;
			};

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

			function adjustListIndex(oldIndex, newIndex) {
			    var opath = aconfPath.join(path, oldIndex);
			    var npath = aconfPath.join(path, newIndex);
			    _.each(
				[changed, invalid],
				function(map) {
				    _.each(
					_.keys(map),
					function(p) {
					    if (aconfPath.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) {
			    return $q(function(resolve, reject) {
				var length = data.data.length;

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

					var mpath = aconfPath.join(path, name);
					var key = index(name);

					delete changed[mpath];
					changed[path] = path;

					if (data.meta.type == "list") {
					    if (aconfType.isTreeNode(
						data.meta.members
					    )) {
						data.data[key] = mpath;
						data.data.pop();
					    }
					    else data.data.splice(name - 1, 1);

					    adjustListIndices(name, length);
					}
					else delete data.data[key];
				    }

				    resolve(txnValid);
				}, reject);
			    });
			}

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

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

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

			    return exclusive(function() {
				return $q(function(resolve, reject) {
				    if (oldIndex > newIndex) oldIndex++;
				    else newIndex++;

				    objRequest(path, {method: "POST", data: {
					index: newIndex,
					data: aconfType.isTreeNode(
					    data.meta.members
					) ? aconfPath.join(
					    path, oldIndex
					) : value
				    }}).then(function() {

					if (aconfType.isTreeNode(
					    data.meta.members
					))
					    data.data.push(value);
					else data.data.splice(
					    newIndex - 1, 0, value
					);

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

					_delete(oldIndex).then(resolve, reject);

				    }, reject);
				});
			    });
			};

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

			resolve(data);
		    }).error(reject);
		});
	    };

	    txnMgr.isPristine = function() { return !(_.size(changed)); };

	    txnMgr.start = function() {
		return $q(function(resolve, reject) {
		    if (txn && isValid() && txnMgr.isPristine()) txnMgr.abort();

		    if (txn) resolve();

		    else request("/transaction", {method: "POST"})
			.success(function(data, status, headers) {
			    txn = headers("X-AConf-Transaction-ID");
			    resolve();
			}).error(reject);
		});
	    };

	    txnMgr.commit = function() {
		return $q(function(resolve, reject) {
		    request("/transaction", {method: "PUT"}).then(function() {
			reset();
			if (saveRequired)
			    request("/save", {method: "POST"})
			    .then(resolve, reject);
			else resolve();
		    }, reject);
		});
	    };

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

	    return txnMgr;
	};
    }
);
