/* My Mind web app: all source files combined. */
if (!Function.prototype.bind) {
	Function.prototype.bind = function(thisObj) {
		var fn = this;
		var args = Array.prototype.slice.call(arguments, 1);
		return function() {
			return fn.apply(thisObj, args.concat(Array.prototype.slice.call(arguments)));
		}
	}
};

var MM = {
	_subscribers: {},

	publish: function(message, publisher, data) {
		var subscribers = this._subscribers[message] || [];
		subscribers.forEach(function(subscriber) {
			subscriber.handleMessage(message, publisher, data);
		});
	},

	subscribe: function(message, subscriber) {
		if (!(message in this._subscribers)) {
			this._subscribers[message] = [];
		}
		var index = this._subscribers[message].indexOf(subscriber);
		if (index == -1) { this._subscribers[message].push(subscriber); }
	},

	unsubscribe: function(message, subscriber) {
		var index = this._subscribers[message].indexOf(subscriber);
		if (index > -1) { this._subscribers[message].splice(index, 1); }
	},

	generateId: function() {
		var str = "";
		for (var i=0;i<8;i++) {
			var code = Math.floor(Math.random()*26);
			str += String.fromCharCode("a".charCodeAt(0) + code);
		}
		return str;
	},

	isMac: function() {
		return !!navigator.platform.match(/mac/i);
	}
};
/*
	Any copyright is dedicated to the Public Domain.
	http://creativecommons.org/publicdomain/zero/1.0/
*/

/**
 * @class A promise - value to be resolved in the future.
 * Implements the "Promises/A+" specification.
 */
var Promise = function(executor) {
	this._state = 0; /* 0 = pending, 1 = fulfilled, 2 = rejected */
	this._value = null; /* fulfillment / rejection value */

	this._cb = {
		fulfilled: [],
		rejected: []
	}

	this._thenPromises = []; /* promises returned by then() */

	executor && executor(this.fulfill.bind(this), this.reject.bind(this));
}

Promise.resolve = function(value) {
	return new Promise().fulfill(value);
}

Promise.reject = function(value) {
	return new Promise().reject(value);
}

/**
 * @param {function} onFulfilled To be called once this promise gets fulfilled
 * @param {function} onRejected To be called once this promise gets rejected
 * @returns {Promise}
 */
Promise.prototype.then = function(onFulfilled, onRejected) {
	this._cb.fulfilled.push(onFulfilled);
	this._cb.rejected.push(onRejected);

	var thenPromise = new Promise();

	this._thenPromises.push(thenPromise);

	if (this._state > 0) {
		setTimeout(this._processQueue.bind(this), 0);
	}

	/* 3.2.6. then must return a promise. */
	return thenPromise; 
}

/**
 * Fulfill this promise with a given value
 * @param {any} value
 */
Promise.prototype.fulfill = function(value) {
	if (this._state != 0) { return this; }

	this._state = 1;
	this._value = value;

	this._processQueue();

	return this;
}

/**
 * Reject this promise with a given value
 * @param {any} value
 */
Promise.prototype.reject = function(value) {
	if (this._state != 0) { return this; }

	this._state = 2;
	this._value = value;

	this._processQueue();

	return this;
}

/**
 * Pass this promise's resolved value to another promise
 * @param {Promise} promise
 */
Promise.prototype.chain = function(promise) {
	return this.then(promise.fulfill.bind(promise), promise.reject.bind(promise));
}

/**
 * @param {function} onRejected To be called once this promise gets rejected
 * @returns {Promise}
 */
Promise.prototype["catch"] = function(onRejected) {
	return this.then(null, onRejected);
}

Promise.prototype._processQueue = function() {
	while (this._thenPromises.length) {
		var onFulfilled = this._cb.fulfilled.shift();
		var onRejected = this._cb.rejected.shift();
		this._executeCallback(this._state == 1 ? onFulfilled : onRejected);
	}
}

Promise.prototype._executeCallback = function(cb) {
	var thenPromise = this._thenPromises.shift();

	if (typeof(cb) != "function") {
		if (this._state == 1) {
			/* 3.2.6.4. If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value. */
			thenPromise.fulfill(this._value);
		} else {
			/* 3.2.6.5. If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason. */
			thenPromise.reject(this._value);
		}
		return;
	}

	try {
		var returned = cb(this._value);

		if (returned && typeof(returned.then) == "function") {
			/* 3.2.6.3. If either onFulfilled or onRejected returns a promise (call it returnedPromise), promise2 must assume the state of returnedPromise */
			var fulfillThenPromise = function(value) { thenPromise.fulfill(value); }
			var rejectThenPromise = function(value) { thenPromise.reject(value); }
			returned.then(fulfillThenPromise, rejectThenPromise);
		} else {
			/* 3.2.6.1. If either onFulfilled or onRejected returns a value that is not a promise, promise2 must be fulfilled with that value. */ 
			thenPromise.fulfill(returned);
		}

	} catch (e) {

		/* 3.2.6.2. If either onFulfilled or onRejected throws an exception, promise2 must be rejected with the thrown exception as the reason. */
		thenPromise.reject(e); 

	}
}    
/**
 * Wait for all these promises to complete. One failed => this fails too.
 */
Promise.all = Promise.when = function(all) {
	var promise = new this();
	var counter = 0;
	var results = [];

	for (var i=0;i<all.length;i++) {
		counter++;
		all[i].then(function(index, result) {
			results[index] = result;
			counter--;
			if (!counter) { promise.fulfill(results); }
		}.bind(null, i), function(reason) {
			counter = 1/0;
			promise.reject(reason);
		});
	}

	return promise;
}

/**
 * Promise-based version of setTimeout
 */
Promise.setTimeout = function(ms) {
	var promise = new this();
	setTimeout(function() { promise.fulfill(); }, ms);
	return promise;
}

/**
 * Promise-based version of addEventListener
 */
Promise.event = function(element, event, capture) {
	var promise = new this();
	var cb = function(e) {
		element.removeEventListener(event, cb, capture);
		promise.fulfill(e);
	}
	element.addEventListener(event, cb, capture);
	return promise;
}

/**
 * Promise-based wait for CSS transition end
 */
Promise.transition = function(element) {
	if ("transition" in element.style) {
		return this.event(element, "transitionend", false);
	} else if ("webkitTransition" in element.style) {
		return this.event(element, "webkitTransitionEnd", false);
	} else {
		return new this().fulfill();
	}
}

/**
 * Promise-based version of XMLHttpRequest::send
 */
Promise.send = function(xhr, data) {
	var promise = new this();
	xhr.addEventListener("readystatechange", function(e) {
		if (e.target.readyState != 4) { return; }
		if (e.target.status.toString().charAt(0) == "2") {
			promise.fulfill(e.target);
		} else {
			promise.reject(e.target);
		}
	});
	xhr.send(data);
	return promise;
}

Promise.worker = function(url, message) {
	var promise = new this();
	var worker = new Worker(url);
	Promise.event(worker, "message").then(function(e) {
		promise.fulfill(e.data);
	});
	Promise.event(worker, "error").then(function(e) {
		promise.reject(e.message);
	});
	worker.postMessage(message);
	return promise;
}
/**
 * Prototype for all things categorizable: shapes, layouts, commands, formats, backends...
 */
MM.Repo = {
	id: "", /* internal ID */
	label: "", /* human-readable label */
	getAll: function() {
		var all = [];
		for (var p in this) {
			var val = this[p];
			if (this.isPrototypeOf(val)) { all.push(val); }
		}
		return all;
	},
	getByProperty: function(property, value) {
		return this.getAll().filter(function(item) {
			return item[property] == value;
		})[0] || null;
	},
	getById: function(id) {
		return this.getByProperty("id", id);
	},
	buildOption: function() {
		var o = document.createElement("option");
		o.value = this.id;
		o.innerHTML = this.label;
		return o;
	}
}
MM.Item = function() {
	this._parent = null;
	this._children = [];
	this._collapsed = false;

	this._layout = null;
	this._shape = null;
	this._autoShape = true;
	this._color = null;
	this._value = null;
	this._status = null;
	this._side = null; /* side preference */
	this._icon = null;
	this._notes = null;
	this._id = MM.generateId();
	this._oldText = "";

	this._computed = {
		value: 0,
		status: null
	}

	this._dom = {
		node: document.createElement("li"),
		content: document.createElement("div"),
		notes: document.createElement("div"),
		status: document.createElement("span"),
		icon: document.createElement("span"),
		value: document.createElement("span"),
		text: document.createElement("div"),
		children: document.createElement("ul"),
		toggle: document.createElement("div"),
		canvas: document.createElement("canvas")
	}
	this._dom.node.classList.add("item");
	this._dom.content.classList.add("content");
	this._dom.notes.classList.add("notes-indicator");
	this._dom.status.classList.add("status");
	this._dom.icon.classList.add("icon");
	this._dom.value.classList.add("value");
	this._dom.text.classList.add("text");
	this._dom.toggle.classList.add("toggle");
	this._dom.children.classList.add("children");

	this._dom.content.appendChild(this._dom.text); /* status+value are appended in layout */
	this._dom.node.appendChild(this._dom.canvas);
	this._dom.node.appendChild(this._dom.content);
	this._dom.content.appendChild(this._dom.notes);
	/* toggle+children are appended when children exist */

	this._dom.toggle.addEventListener("click", this);
}

MM.Item.COLOR = "#999";

    /* RE explanation:
     *          _________________________________________________________________________ One of the three possible variants
     *           ____________________ scheme://x
     *                                ___________________________ aa.bb.cc
     *                                                            _______________________ aa.bb/
     *                                                                                    ______ path, search
     *                                                                                          __________________________ end with a non-forbidden char
     *                                                                                                                    ______ end of word or end of string
     */                                                                                                                           
MM.Item.RE = /\b(([a-z][\w-]+:\/\/\w)|(([\w-]+\.){2,}[a-z][\w-]+)|([\w-]+\.[a-z][\w-]+\/))[^\s]*([^\s,.;:?!<>\(\)\[\]'"])?($|\b)/i;

MM.Item.fromJSON = function(data) {
	return new this().fromJSON(data);
}

MM.Item.prototype.toJSON = function() {
	var data = {
		id: this._id,
		text: this.getText(),
		notes: this.getNotes()
	}


	if (this._side) { data.side = this._side; }
	if (this._color) { data.color = this._color; }
	if (this._icon) { data.icon = this._icon; }
	if (this._value) { data.value = this._value; }
	if (this._status) { data.status = this._status; }
	if (this._layout) { data.layout = this._layout.id; }
	if (!this._autoShape) { data.shape = this._shape.id; }
	if (this._collapsed) { data.collapsed = 1; }
	if (this._children.length) {
		data.children = this._children.map(function(child) { return child.toJSON(); });
	}

	return data;
}

/**
 * Only when creating a new item. To merge existing items, use .mergeWith().
 */
MM.Item.prototype.fromJSON = function(data) {
	this.setText(data.text);
	if (data.notes) {
		this.setNotes(data.notes);
	}
	if (data.id) { this._id = data.id; }
	if (data.side) { this._side = data.side; }
	if (data.color) { this._color = data.color; }
	if (data.icon) { this._icon = data.icon; }
	if (data.value) { this._value = data.value; }
	if (data.status) {
		this._status = data.status;
		if (this._status == "maybe") { this._status = "computed"; }
	}
	if (data.collapsed) { this.collapse(); }
	if (data.layout) { this._layout = MM.Layout.getById(data.layout); }
	if (data.shape) { this.setShape(MM.Shape.getById(data.shape)); }

	(data.children || []).forEach(function(child) {
		this.insertChild(MM.Item.fromJSON(child));
	}, this);

	return this;
}

MM.Item.prototype.mergeWith = function(data) {
	var dirty = 0;

	if (this.getText() != data.text && !this._dom.text.contentEditable) { this.setText(data.text); }

	if (this._side != data.side) { 
		this._side = data.side;
		dirty = 1;
	}

	if (this._color != data.color) { 
		this._color = data.color;
		dirty = 2;
	}

	if (this._icon != data.icon) {
		this._icon = data.icon;
		dirty = 1;
	}

	if (this._value != data.value) { 
		this._value = data.value;
		dirty = 1;
	}

	if (this._status != data.status) { 
		this._status = data.status;
		dirty = 1;
	}

	if (this._collapsed != !!data.collapsed) { this[this._collapsed ? "expand" : "collapse"](); }

	if (this.getOwnLayout() != data.layout) {
		this._layout = MM.Layout.getById(data.layout);
		dirty = 2;
	}

	var s = (this._autoShape ? null : this._shape.id);
	if (s != data.shape) { this.setShape(MM.Shape.getById(data.shape)); }

	(data.children || []).forEach(function(child, index) {
		if (index >= this._children.length) { /* new child */
			this.insertChild(MM.Item.fromJSON(child));
		} else { /* existing child */
			var myChild = this._children[index];
			if (myChild.getId() == child.id) { /* recursive merge */
				myChild.mergeWith(child);
			} else { /* changed; replace */
				this.removeChild(this._children[index]);
				this.insertChild(MM.Item.fromJSON(child), index);
			}
		}
	}, this);

	/* remove dead children */
	var newLength = (data.children || []).length;
	while (this._children.length > newLength) { this.removeChild(this._children[this._children.length-1]); }

	if (dirty == 1) { this.update(); }
	if (dirty == 2) { this.updateSubtree(); }
}

MM.Item.prototype.clone = function() {
	var data = this.toJSON();

	var removeId = function(obj) {
		delete obj.id;
		obj.children && obj.children.forEach(removeId);
	}
	removeId(data);

	return this.constructor.fromJSON(data);
}

MM.Item.prototype.select = function() {
	this._dom.node.classList.add("current");
	if (window.editor) {
        if (this._notes) {
            window.editor.setContent(this._notes);
        } else {
            window.editor.setContent('');
        }
	}
	this.getMap().ensureItemVisibility(this);
	MM.Clipboard.focus(); /* going to mode 2c */
	MM.publish("item-select", this);
}

MM.Item.prototype.deselect = function() {
	/* we were in 2b; finish that via 3b */
	if (MM.App.editing) { MM.Command.Finish.execute(); }
	this._dom.node.classList.remove("current");
}

MM.Item.prototype.update = function(doNotRecurse) {
	var map = this.getMap();
	if (!map || !map.isVisible()) { return this; }

	MM.publish("item-change", this);

	if (this._autoShape) { /* check for changed auto-shape */
		var autoShape = this._getAutoShape();
		if (autoShape != this._shape) {
			if (this._shape) { this._shape.unset(this); }
			this._shape = autoShape;
			this._shape.set(this);
		}
	}

	this._updateStatus();
	this._updateIcon();
    this._updateNotesIndicator();
	this._updateValue();

	this._dom.node.classList[this._collapsed ? "add" : "remove"]("collapsed");

	this.getLayout().update(this);
	this.getShape().update(this);
	if (!this.isRoot() && !doNotRecurse) { this._parent.update(); }

	return this;
}

MM.Item.prototype.updateSubtree = function(isSubChild) {
	this._children.forEach(function(child) {
		child.updateSubtree(true);
	});
	return this.update(isSubChild);
}

MM.Item.prototype.setText = function(text) {
	this._dom.text.innerHTML = text;
	this._findLinks(this._dom.text);
	return this.update();
}

MM.Item.prototype.setNotes = function(notes) {
	this._notes = notes;
	return this.update();
}

MM.Item.prototype.getId = function() {
	return this._id;
}

MM.Item.prototype.getText = function() {
	return this._dom.text.innerHTML;
}

MM.Item.prototype.getNotes = function() {
	return this._notes;
}

MM.Item.prototype.collapse = function() {
	if (this._collapsed) { return; }
	this._collapsed = true;
	return this.update();
}

MM.Item.prototype.expand = function() {
	if (!this._collapsed) { return; }
	this._collapsed = false;
	this.update();
	return this.updateSubtree();
}

MM.Item.prototype.isCollapsed = function() {
	return this._collapsed;
}

MM.Item.prototype.setValue = function(value) {
	this._value = value;
	return this.update();
}

MM.Item.prototype.getValue = function() {
	return this._value;
}

MM.Item.prototype.getComputedValue = function() {
	return this._computed.value;
}

MM.Item.prototype.setStatus = function(status) {
	this._status = status;
	return this.update();
}

MM.Item.prototype.getStatus = function() {
	return this._status;
}

MM.Item.prototype.setIcon = function(icon) {
	this._icon = icon;
	return this.update();
}

MM.Item.prototype.getIcon = function() {
	return this._icon;
}

MM.Item.prototype.getComputedStatus = function() {
	return this._computed.status;
}

MM.Item.prototype.setSide = function(side) {
	this._side = side;
	return this;
}

MM.Item.prototype.getSide = function() {
	return this._side;
}

MM.Item.prototype.getChildren = function() {
	return this._children;
}

MM.Item.prototype.setColor = function(color) {
	this._color = color;
	return this.updateSubtree();
}

MM.Item.prototype.getColor = function() {
	return this._color || (this.isRoot() ? MM.Item.COLOR : this._parent.getColor());
}

MM.Item.prototype.getOwnColor = function() {
	return this._color;
}

MM.Item.prototype.getLayout = function() {
	return this._layout || this._parent.getLayout();
}

MM.Item.prototype.getOwnLayout = function() {
	return this._layout;
}

MM.Item.prototype.setLayout = function(layout) {
	this._layout = layout;
	return this.updateSubtree();	
}

MM.Item.prototype.getShape = function() {
	return this._shape;
}

MM.Item.prototype.getOwnShape = function() {
	return (this._autoShape ? null : this._shape);
}

MM.Item.prototype.setShape = function(shape) {
	if (this._shape) { this._shape.unset(this); }

	if (shape) {
		this._autoShape = false;
		this._shape = shape;
	} else {
		this._autoShape = true;
		this._shape = this._getAutoShape();
	}

	this._shape.set(this);
	return this.update();
}

MM.Item.prototype.getDOM = function() {
	return this._dom;
}

MM.Item.prototype.getMap = function() {
	var item = this._parent;
	while (item) {
		if (item instanceof MM.Map) { return item; }
		item = item.getParent();
	}
	return null;
}

MM.Item.prototype.getParent = function() {
	return this._parent;
}

MM.Item.prototype.isRoot = function() {
	return (this._parent instanceof MM.Map);
}

MM.Item.prototype.setParent = function(parent) {
	this._parent = parent;
	return this.updateSubtree();
}

MM.Item.prototype.insertChild = function(child, index) {
	/* Create or remove child as necessary. This must be done before computing the index (inserting own child) */
	var newChild = false;
	if (!child) { 
		child = new MM.Item();
		newChild = true;
	} else if (child.getParent() && child.getParent().removeChild) { /* only when the child has non-map parent */
		child.getParent().removeChild(child);
	}

	if (!this._children.length) {
		this._dom.node.appendChild(this._dom.toggle);
		this._dom.node.appendChild(this._dom.children);
	}

	if (arguments.length < 2) { index = this._children.length; }
	
	var next = null;
	if (index < this._children.length) { next = this._children[index].getDOM().node; }
	this._dom.children.insertBefore(child.getDOM().node, next);
	this._children.splice(index, 0, child);
	
	return child.setParent(this);
}

MM.Item.prototype.removeChild = function(child) {
	var index = this._children.indexOf(child);
	this._children.splice(index, 1);
	var node = child.getDOM().node;
	node.parentNode.removeChild(node);
	
	child.setParent(null);
	
	if (!this._children.length) {
		this._dom.toggle.parentNode.removeChild(this._dom.toggle);
		this._dom.children.parentNode.removeChild(this._dom.children);
	}
	
	return this.update();
}

MM.Item.prototype.startEditing = function() {
	this._oldText = this.getText();
	this._dom.text.contentEditable = true;
	this._dom.text.focus(); /* switch to 2b */
	document.execCommand("styleWithCSS", null, false);

	this._dom.text.addEventListener("input", this);
	this._dom.text.addEventListener("keydown", this);
	this._dom.text.addEventListener("blur", this);
	return this;
}

MM.Item.prototype.stopEditing = function() {
	this._dom.text.removeEventListener("input", this);
	this._dom.text.removeEventListener("keydown", this);
	this._dom.text.removeEventListener("blur", this);

	this._dom.text.blur();
	this._dom.text.contentEditable = false;
	var result = this._dom.text.innerHTML;
	this._dom.text.innerHTML = this._oldText;
	this._oldText = "";

	this.update(); /* text changed */

	MM.Clipboard.focus();

	return result;
}

MM.Item.prototype.handleEvent = function(e) {
	switch (e.type) {
		case "input":
			this.update();
			this.getMap().ensureItemVisibility(this);
		break;

		case "keydown":
			if (e.keyCode == 9) { e.preventDefault(); } /* TAB has a special meaning in this app, do not use it to change focus */
		break;

		case "blur": /* 3d */
			MM.Command.Finish.execute();
		break;

		case "click":
			if (this._collapsed) { this.expand(); } else { this.collapse(); }
			MM.App.select(this);
		break;
	}
}

MM.Item.prototype._getAutoShape = function() {
	var depth = 0;
	var node = this;
	while (!node.isRoot()) {
		depth++;
		node = node.getParent();
	}
	switch (depth) {
		case 0: return MM.Shape.Ellipse;
		case 1: return MM.Shape.Box;
		default: return MM.Shape.Underline;
	}
}

MM.Item.prototype._updateStatus = function() {
	this._dom.status.className = "status";
	this._dom.status.style.display = "";

	var status = this._status;
	if (this._status == "computed") {
		var childrenStatus = this._children.every(function(child) {
			return (child.getComputedStatus() !== false);
		});
		status = (childrenStatus ? "yes" : "no");
	}

	switch (status) {
		case "yes":
			this._dom.status.classList.add("yes");
			this._computed.status = true;
		break;

		case "no":
			this._dom.status.classList.add("no");
			this._computed.status = false;
		break;

		default:
			this._computed.status = null;
			this._dom.status.style.display = "none";
		break;
	}
}
MM.Item.prototype._updateIcon = function() {
    this._dom.icon.className = "icon";
    this._dom.icon.style.display = "";

    var icon = this._icon;
    if (icon)
	{
        this._dom.icon.classList.add('fa');
        this._dom.icon.classList.add(icon);
        this._computed.icon = true;
	} else {
        this._computed.icon = null;
        this._dom.icon.style.display = "none";
	}
}

MM.Item.prototype._updateNotesIndicator = function() {
    if (this._notes)
    {
        this._dom.notes.classList.add("notes-indicator-visible");
    } else {
        this._dom.notes.classList.remove("notes-indicator-visible");
    }
}

MM.Item.prototype._updateValue = function() {
	this._dom.value.style.display = "";

	if (typeof(this._value) == "number") {
		this._computed.value = this._value;
		this._dom.value.innerHTML = this._value;
		return;
	}
	
	var childValues = this._children.map(function(child) {
		return child.getComputedValue();
	});
	
	var result = 0;
	switch (this._value) {
		case "sum":
			result = childValues.reduce(function(prev, cur) {
				return prev+cur;
			}, 0);
		break;
		
		case "avg":
			var sum = childValues.reduce(function(prev, cur) {
				return prev+cur;
			}, 0);
			result = (childValues.length ? sum/childValues.length : 0);
		break;
		
		case "max":
			result = Math.max.apply(Math, childValues);
		break;
		
		case "min":
			result = Math.min.apply(Math, childValues);
		break;
		
		default:
			this._computed.value = 0;
			this._dom.value.innerHTML = "";
			this._dom.value.style.display = "none";
			return;
		break;
	}
	
	this._computed.value = result;
	this._dom.value.innerHTML = (Math.round(result) == result ? result : result.toFixed(3));
}

MM.Item.prototype._findLinks = function(node) {

	var children = [].slice.call(node.childNodes);
	for (var i=0;i<children.length;i++) {
		var child = children[i];
		switch (child.nodeType) {
			case 1: /* element */
				if (child.nodeName.toLowerCase() == "a") { continue; }
				this._findLinks(child);
			break;
			
			case 3: /* text */
				var result = child.nodeValue.match(this.constructor.RE);
				if (result) {
					var before = child.nodeValue.substring(0, result.index);
					var after = child.nodeValue.substring(result.index + result[0].length);
					var link = document.createElement("a");
					link.innerHTML = link.href = result[0];
					
					if (before) {
						node.insertBefore(document.createTextNode(before), child);
					}

					node.insertBefore(link, child);
					
					if (after) {
						child.nodeValue = after;
						i--; /* re-try with the aftertext */
					} else {
						node.removeChild(child);
					}
				}
			break;
		}
	}
}
MM.Map = function(options) {
	var o = {
		root: "My Mind Map",
		layout: MM.Layout.Map
	}
	for (var p in options) { o[p] = options[p]; }
	this._root = null;
	this._visible = false;
	this._position = [0, 0];

	this._setRoot(new MM.Item().setText(o.root).setLayout(o.layout));
}

MM.Map.fromJSON = function(data) {
	return new this().fromJSON(data);
}

MM.Map.prototype.toJSON = function() {
	var data = {
		root: this._root.toJSON()
	};
	return data;
}

MM.Map.prototype.fromJSON = function(data) {
	this._setRoot(MM.Item.fromJSON(data.root));
	return this;
}

MM.Map.prototype.mergeWith = function(data) {
	/* store a sequence of nodes to be selected when merge is over */
	var ids = [];
	var current = MM.App.current;
	var node = current;
	while (node != this) {
		ids.push(node.getId());
		node = node.getParent();
	}

	this._root.mergeWith(data.root);

	if (current.getMap()) { /* selected node still in tree, cool */
		/* if one of the parents got collapsed, act as if the node got removed */
		var node = current.getParent();
		var hidden = false;
		while (node != this) {
			if (node.isCollapsed()) { hidden = true; }
			node = node.getParent();
		}
		if (!hidden) { return; } /* nothing bad happened, continue */
	} 

	/* previously selected node is no longer in the tree OR it is folded */

	/* what if the node was being edited? */
	if (MM.App.editing) { current.stopEditing(); } 

	/* get all items by their id */
	var idMap = {};
	var scan = function(item) {
		idMap[item.getId()] = item;
		item.getChildren().forEach(scan);
	}
	scan(this._root);

	/* select the nearest existing parent */
	while (ids.length) {
		var id = ids.shift();
		if (id in idMap) {
			MM.App.select(idMap[id]);
			return;
		}
	}
}

MM.Map.prototype.isVisible = function() {
	return this._visible;
}

MM.Map.prototype.update = function() {
	this._root.updateSubtree();
	return this;
}

MM.Map.prototype.show = function(where) {
	var node = this._root.getDOM().node;
	where.appendChild(node);
	this._visible = true;
	this._root.updateSubtree();
	this.center();
	MM.App.select(this._root);
	return this;
}

MM.Map.prototype.hide = function() {
	var node = this._root.getDOM().node;
	node.parentNode.removeChild(node);
	this._visible = false;
	return this;
}

MM.Map.prototype.center = function() {
	var node = this._root.getDOM().node;
	var port = MM.App.portSize;
	var left = (port[0] - node.offsetWidth)/2;
	var top = (port[1] - node.offsetHeight)/2;
	
	this._moveTo(Math.round(left), Math.round(top));

	return this;
}

MM.Map.prototype.moveBy = function(dx, dy) {
	return this._moveTo(this._position[0]+dx, this._position[1]+dy);
}

MM.Map.prototype.getClosestItem = function(x, y) {
	var all = [];

	var scan = function(item) {
		var rect = item.getDOM().content.getBoundingClientRect();
		var dx = rect.left + rect.width/2 - x;
		var dy = rect.top + rect.height/2 - y;
		all.push({
			item: item,
			dx: dx,
			dy: dy
		});
		if (!item.isCollapsed()) { item.getChildren().forEach(scan); }
	}
	
	scan(this._root);
	
	all.sort(function(a, b) {
		var da = a.dx*a.dx + a.dy*a.dy;
		var db = b.dx*b.dx + b.dy*b.dy;
		return da-db;
	});
	
	return all[0];
}

MM.Map.prototype.getItemFor = function(node) {
	var port = this._root.getDOM().node.parentNode;
	while (node != port && !node.classList.contains("content")) {
		node = node.parentNode;
	}	
	if (node == port) { return null; }

	var scan = function(item, node) {
		if (item.getDOM().content == node) { return item; }
		var children = item.getChildren();
		for (var i=0;i<children.length;i++) {
			var result = scan(children[i], node);
			if (result) { return result; }
		}
		return null;
	}

	return scan(this._root, node);
}

MM.Map.prototype.ensureItemVisibility = function(item) {
	var padding = 10;

	var node = item.getDOM().content;
	var itemRect = node.getBoundingClientRect();
	var root = this._root.getDOM().node;
	var parentRect = root.parentNode.getBoundingClientRect();

	var delta = [0, 0];

	var dx = parentRect.left-itemRect.left+padding;
	if (dx > 0) { delta[0] = dx; }
	var dx = parentRect.right-itemRect.right-padding;
	if (dx < 0) { delta[0] = dx; }

	var dy = parentRect.top-itemRect.top+padding;
	if (dy > 0) { delta[1] = dy; }
	var dy = parentRect.bottom-itemRect.bottom-padding;
	if (dy < 0) { delta[1] = dy; }

	if (delta[0] || delta[1]) {
		this.moveBy(delta[0], delta[1]);
	}
}

MM.Map.prototype.getParent = function() {
	return null;
}

MM.Map.prototype.getRoot = function() {
	return this._root;
}

MM.Map.prototype.getName = function() {
	var name = this._root.getText();
	return MM.Format.br2nl(name).replace(/\n/g, " ").replace(/<.*?>/g, "").trim();
}

MM.Map.prototype.getId = function() {
	return this._root.getId();
}

MM.Map.prototype.pick = function(item, direction) {
	var candidates = [];
	var currentRect = item.getDOM().content.getBoundingClientRect();

	this._getPickCandidates(currentRect, this._root, direction, candidates);
	if (!candidates.length) { return item; }

	candidates.sort(function(a, b) {
		return a.dist - b.dist;
	});

	return candidates[0].item;
}

MM.Map.prototype._getPickCandidates = function(currentRect, item, direction, candidates) {
	if (!item.isCollapsed()) {
		item.getChildren().forEach(function(child) {
			this._getPickCandidates(currentRect, child, direction, candidates);
		}, this);
	}

	var node = item.getDOM().content;
	var rect = node.getBoundingClientRect();

	if (direction == "left" || direction == "right") {
		var x1 = currentRect.left + currentRect.width/2;
		var x2 = rect.left + rect.width/2;
		if (direction == "left" && x2 > x1) { return; }
		if (direction == "right" && x2 < x1) { return; }

		var diff1 = currentRect.top - rect.bottom;
		var diff2 = rect.top - currentRect.bottom;
		var dist = Math.abs(x2-x1);
	} else {
		var y1 = currentRect.top + currentRect.height/2;
		var y2 = rect.top + rect.height/2;
		if (direction == "top" && y2 > y1) { return; }
		if (direction == "bottom" && y2 < y1) { return; }

		var diff1 = currentRect.left - rect.right;
		var diff2 = rect.left - currentRect.right;
		var dist = Math.abs(y2-y1);
	}

	var diff = Math.max(diff1, diff2);
	if (diff > 0) { return; }
	if (!dist || dist < diff) { return; }

	candidates.push({item:item, dist:dist});
}

MM.Map.prototype._moveTo = function(left, top) {
	this._position = [left, top];

	var node = this._root.getDOM().node;
	node.style.left = left + "px";
	node.style.top = top + "px";
}

MM.Map.prototype._setRoot = function(item) {
	this._root = item;
	this._root.setParent(this);
}
MM.Keyboard = {};
MM.Keyboard.init = function() {
	window.addEventListener("keydown", this);
	window.addEventListener("keypress", this);
}

MM.Keyboard.handleEvent = function(e) {
	/* mode 2a: ignore keyboard when the activeElement resides somewhere inside of the UI pane */
	var node = document.activeElement;
	while (node && node != document) {
		if (node.classList.contains("ui")) { return; }
		node = node.parentNode;
	}
	
	var commands = MM.Command.getAll();
	for (var i=0;i<commands.length;i++) {
		var command = commands[i];
		if (!command.isValid()) { continue; }
		var keys = command.keys;
		for (var j=0;j<keys.length;j++) {
			if (this._keyOK(keys[j], e)) {
				command.prevent && e.preventDefault();
				command.execute(e);
				return;
			}
		}
	}
}

MM.Keyboard._keyOK = function(key, e) {
	if ("keyCode" in key && e.type != "keydown") { return false; }
	if ("charCode" in key && e.type != "keypress") { return false; }
	for (var p in key) {
		if (key[p] != e[p]) { return false; }
	}
	return true;
}
MM.Tip = {
	_node: null,

	handleEvent: function() {
		this._hide();
	},

	handleMessage: function() {
		this._hide();
	},

	init: function() {
		this._node = document.querySelector("#tip");
		this._node.addEventListener("click", this);

		MM.subscribe("command-child", this);
		MM.subscribe("command-sibling", this);
	},

	_hide: function() {
		MM.unsubscribe("command-child", this);
		MM.unsubscribe("command-sibling", this);

		this._node.removeEventListener("click", this);
		this._node.classList.add("hidden");
		this._node = null;
	}
}
MM.Action = function() {}
MM.Action.prototype.perform = function() {}
MM.Action.prototype.undo = function() {}

MM.Action.Multi = function(actions) {
	this._actions = actions;
}
MM.Action.Multi.prototype = Object.create(MM.Action.prototype);
MM.Action.Multi.prototype.perform = function() {
	this._actions.forEach(function(action) {
		action.perform();
	});
}
MM.Action.Multi.prototype.undo = function() {
	this._actions.slice().reverse().forEach(function(action) {
		action.undo();
	});
}

MM.Action.InsertNewItem = function(parent, index) {
	this._parent = parent;
	this._index = index;
	this._item = new MM.Item();
}
MM.Action.InsertNewItem.prototype = Object.create(MM.Action.prototype);
MM.Action.InsertNewItem.prototype.perform = function() {
	this._parent.expand(); /* FIXME remember? */
	this._item = this._parent.insertChild(this._item, this._index);
	MM.App.select(this._item);
}
MM.Action.InsertNewItem.prototype.undo = function() {
	this._parent.removeChild(this._item);
	MM.App.select(this._parent);
}

MM.Action.AppendItem = function(parent, item) {
	this._parent = parent;
	this._item = item;
}
MM.Action.AppendItem.prototype = Object.create(MM.Action.prototype);
MM.Action.AppendItem.prototype.perform = function() {
	this._parent.insertChild(this._item);
	MM.App.select(this._item);
}
MM.Action.AppendItem.prototype.undo = function() {
	this._parent.removeChild(this._item);
	MM.App.select(this._parent);
}

MM.Action.RemoveItem = function(item) {
	this._item = item;
	this._parent = item.getParent();
	this._index = this._parent.getChildren().indexOf(this._item);
}
MM.Action.RemoveItem.prototype = Object.create(MM.Action.prototype);
MM.Action.RemoveItem.prototype.perform = function() {
	this._parent.removeChild(this._item);
	MM.App.select(this._parent);
}
MM.Action.RemoveItem.prototype.undo = function() {
	this._parent.insertChild(this._item, this._index);
	MM.App.select(this._item);
}

MM.Action.MoveItem = function(item, newParent, newIndex, newSide) {
	this._item = item;
	this._newParent = newParent;
	this._newIndex = (arguments.length < 3 ? null : newIndex);
	this._newSide = newSide || "";
	this._oldParent = item.getParent();
	this._oldIndex = this._oldParent.getChildren().indexOf(item);
	this._oldSide = item.getSide();
}
MM.Action.MoveItem.prototype = Object.create(MM.Action.prototype);
MM.Action.MoveItem.prototype.perform = function() {
	this._item.setSide(this._newSide);
	if (this._newIndex === null) {
		this._newParent.insertChild(this._item);
	} else {
		this._newParent.insertChild(this._item, this._newIndex);
	}
	MM.App.select(this._item);
}
MM.Action.MoveItem.prototype.undo = function() {
	this._item.setSide(this._oldSide);
	this._oldParent.insertChild(this._item, this._oldIndex);
	MM.App.select(this._newParent);
}

MM.Action.Swap = function(item, diff) {
	this._item = item;
	this._parent = item.getParent();

	var children = this._parent.getChildren();
	var sibling = this._parent.getLayout().pickSibling(this._item, diff);
	
	this._sourceIndex = children.indexOf(this._item);
	this._targetIndex = children.indexOf(sibling);
}
MM.Action.Swap.prototype = Object.create(MM.Action.prototype);
MM.Action.Swap.prototype.perform = function() {
	this._parent.insertChild(this._item, this._targetIndex);
}
MM.Action.Swap.prototype.undo = function() {
	this._parent.insertChild(this._item, this._sourceIndex);
}

MM.Action.SetLayout = function(item, layout) {
	this._item = item;
	this._layout = layout;
	this._oldLayout = item.getOwnLayout();
}
MM.Action.SetLayout.prototype = Object.create(MM.Action.prototype);
MM.Action.SetLayout.prototype.perform = function() {
	this._item.setLayout(this._layout);
}
MM.Action.SetLayout.prototype.undo = function() {
	this._item.setLayout(this._oldLayout);
}

MM.Action.SetShape = function(item, shape) {
	this._item = item;
	this._shape = shape;
	this._oldShape = item.getOwnShape();
}
MM.Action.SetShape.prototype = Object.create(MM.Action.prototype);
MM.Action.SetShape.prototype.perform = function() {
	this._item.setShape(this._shape);
}
MM.Action.SetShape.prototype.undo = function() {
	this._item.setShape(this._oldShape);
}

MM.Action.SetColor = function(item, color) {
	this._item = item;
	this._color = color;
	this._oldColor = item.getOwnColor();
}
MM.Action.SetColor.prototype = Object.create(MM.Action.prototype);
MM.Action.SetColor.prototype.perform = function() {
	this._item.setColor(this._color);
}
MM.Action.SetColor.prototype.undo = function() {
	this._item.setColor(this._oldColor);
}

MM.Action.SetText = function(item, text) {
	this._item = item;
	this._text = text;
	this._oldText = item.getText();
	this._oldValue = item.getValue(); /* adjusting text can also modify value! */
}
MM.Action.SetText.prototype = Object.create(MM.Action.prototype);
MM.Action.SetText.prototype.perform = function() {
	this._item.setText(this._text);
	var numText = Number(this._text);
	if (numText == this._text) { this._item.setValue(numText); }
}
MM.Action.SetText.prototype.undo = function() {
	this._item.setText(this._oldText);
	this._item.setValue(this._oldValue);
}

MM.Action.SetValue = function(item, value) {
	this._item = item;
	this._value = value;
	this._oldValue = item.getValue();
}
MM.Action.SetValue.prototype = Object.create(MM.Action.prototype);
MM.Action.SetValue.prototype.perform = function() {
	this._item.setValue(this._value);
}
MM.Action.SetValue.prototype.undo = function() {
	this._item.setValue(this._oldValue);
}

MM.Action.SetStatus = function(item, status) {
	this._item = item;
	this._status = status;
	this._oldStatus = item.getStatus();
}
MM.Action.SetStatus.prototype = Object.create(MM.Action.prototype);
MM.Action.SetStatus.prototype.perform = function() {
	this._item.setStatus(this._status);
}
MM.Action.SetStatus.prototype.undo = function() {
	this._item.setStatus(this._oldStatus);
}

MM.Action.SetIcon = function(item, icon) {
	this._item = item;
	this._icon = icon;
	this._oldIcon = item.getIcon();
}
MM.Action.SetIcon.prototype = Object.create(MM.Action.prototype);
MM.Action.SetIcon.prototype.perform = function() {
	this._item.setIcon(this._icon);
}
MM.Action.SetIcon.prototype.undo = function() {
	this._item.setIcon(this._oldIcon);
}

MM.Action.SetSide = function(item, side) {
	this._item = item;
	this._side = side;
	this._oldSide = item.getSide();
}
MM.Action.SetSide.prototype = Object.create(MM.Action.prototype);
MM.Action.SetSide.prototype.perform = function() {
	this._item.setSide(this._side);
	this._item.getMap().update();
}
MM.Action.SetSide.prototype.undo = function() {
	this._item.setSide(this._oldSide);
	this._item.getMap().update();
}
MM.Clipboard = {
	_item: null,
	_mode: "",
	_delay: 50,
	_node: document.createElement("textarea")
};

MM.Clipboard.init = function() {
	this._node.style.position = "absolute";
	this._node.style.width = 0;
	this._node.style.height = 0;
	this._node.style.left = "-100px";
	this._node.style.top = "-100px";
	document.body.appendChild(this._node);
}

MM.Clipboard.focus = function() {
	this._node.focus();
	this._empty();
}

MM.Clipboard.copy = function(sourceItem) {
	this._endCut();
	this._item = sourceItem.clone();
	this._mode = "copy";

	this._expose();
}

MM.Clipboard.paste = function(targetItem) {
	setTimeout(function() {
		var pasted = this._node.value;
		this._empty();
		if (!pasted) { return; } /* nothing */

		if (this._item && pasted == MM.Format.Plaintext.to(this._item.toJSON())) { /* pasted a previously copied/cut item */
			this._pasteItem(this._item, targetItem);
		} else { /* pasted some external data */
			this._pastePlaintext(pasted, targetItem);
		}

	}.bind(this), this._delay);
}

MM.Clipboard._pasteItem = function(sourceItem, targetItem) {
	switch (this._mode) {
		case "cut":
			if (sourceItem == targetItem || sourceItem.getParent() == targetItem) { /* abort by pasting on the same node or the parent */
				this._endCut();
				return;
			}

			var item = targetItem;
			while (!item.isRoot()) {
				if (item == sourceItem) { return; } /* moving to a child => forbidden */
				item = item.getParent();
			}

			var action = new MM.Action.MoveItem(sourceItem, targetItem);
			MM.App.action(action);

			this._endCut();
		break;

		case "copy":
			var action = new MM.Action.AppendItem(targetItem, sourceItem.clone());
			MM.App.action(action);
		break;
	}
}

MM.Clipboard._pastePlaintext = function(plaintext, targetItem) {
	if (this._mode == "cut") { this._endCut(); } /* external paste => abort cutting */

	var json = MM.Format.Plaintext.from(plaintext);
	var map = MM.Map.fromJSON(json);
	var root = map.getRoot();

	if (root.getText()) {
		var action = new MM.Action.AppendItem(targetItem, root);
		MM.App.action(action);
	} else {
		var actions = root.getChildren().map(function(item) {
			return new MM.Action.AppendItem(targetItem, item);
		});
		var action = new MM.Action.Multi(actions);
		MM.App.action(action);
	}
}

MM.Clipboard.cut = function(sourceItem) {
	this._endCut();

	this._item = sourceItem;
	this._item.getDOM().node.classList.add("cut");
	this._mode = "cut";

	this._expose();
}

/**
 * Expose plaintext data to the textarea to be copied to system clipboard. Clear afterwards.
 */
MM.Clipboard._expose = function() {
	var json = this._item.toJSON();
	var plaintext = MM.Format.Plaintext.to(json);
	this._node.value = plaintext;
	this._node.selectionStart = 0;
	this._node.selectionEnd = this._node.value.length;
	setTimeout(this._empty.bind(this), this._delay);
}

MM.Clipboard._empty = function() {
	/* safari needs a non-empty selection in order to actually perfrom a real copy on cmd+c */
	this._node.value = "\n";
	this._node.selectionStart = 0;
	this._node.selectionEnd = this._node.value.length;
}

MM.Clipboard._endCut = function() {
	if (this._mode != "cut") { return; }

	this._item.getDOM().node.classList.remove("cut");
	this._item = null;
	this._mode = "";
}
MM.Menu = {
	_dom: {},
	_port: null,
	
	open: function(x, y) {
		this._dom.node.style.display = "";
		var w = this._dom.node.offsetWidth;
		var h = this._dom.node.offsetHeight;

		var left = x;
		var top = y;

		if (left > this._port.offsetWidth / 2) { left -= w; }
		if (top > this._port.offsetHeight / 2) { top -= h; }

		this._dom.node.style.left = left+"px";
		this._dom.node.style.top = top+"px";
	},
	
	close: function() {
		this._dom.node.style.display = "none";
	},
	
	handleEvent: function(e) {
		if (e.currentTarget != this._dom.node) {
			this.close();
			return;
		}
		
		e.stopPropagation(); /* no dragdrop, no blur of activeElement */
		e.preventDefault(); /* we do not want to focus the button */
		
		var command = e.target.getAttribute("data-command");
		if (!command) { return; }

		command = MM.Command[command];
		if (!command.isValid()) { return; }

		command.execute();
		this.close();
	},
	
	init: function(port) {
		this._port = port;
		this._dom.node = document.querySelector("#menu");
		var buttons = this._dom.node.querySelectorAll("[data-command]");
		[].slice.call(buttons).forEach(function(button) {
			button.innerHTML = MM.Command[button.getAttribute("data-command")].label;
		});
		
		this._port.addEventListener("mousedown", this);
		this._dom.node.addEventListener("mousedown", this);
		
		this.close();
	}
}

MM.Command = Object.create(MM.Repo, {
	keys: {value: []},
	editMode: {value: false},
	prevent: {value: true}, /* prevent default keyboard action? */
	label: {value: ""}
});

MM.Command.isValid = function() {
	return (this.editMode === null || this.editMode == MM.App.editing);
}
MM.Command.execute = function() {}

MM.Command.Notes = Object.create(MM.Command, {
	label: {value: "Notes"},
	keys: {value: [{keyCode: "M".charCodeAt(0), ctrlKey: true}]}
});

MM.Command.Notes.isValid = function() {
	return MM.Command.isValid.call(this);
}

MM.Command.Notes.execute = function() {
	MM.App.notes.toggle();
}

MM.Command.Undo = Object.create(MM.Command, {
	label: {value: "Undo"},
	keys: {value: [{keyCode: "Z".charCodeAt(0), ctrlKey: true}]}
});
MM.Command.Undo.isValid = function() {
	return MM.Command.isValid.call(this) && !!MM.App.historyIndex;
}
MM.Command.Undo.execute = function() {
	MM.App.history[MM.App.historyIndex-1].undo();
	MM.App.historyIndex--;
}

MM.Command.Redo = Object.create(MM.Command, {
	label: {value: "Redo"},
	keys: {value: [{keyCode: "Y".charCodeAt(0), ctrlKey: true}]},
});
MM.Command.Redo.isValid = function() {
	return (MM.Command.isValid.call(this) && MM.App.historyIndex != MM.App.history.length);
}
MM.Command.Redo.execute = function() {
	MM.App.history[MM.App.historyIndex].perform();
	MM.App.historyIndex++;
}

MM.Command.InsertSibling = Object.create(MM.Command, {
	label: {value: "Insert a sibling"},
	keys: {value: [{keyCode: 13}]}
});
MM.Command.InsertSibling.execute = function() {
	var item = MM.App.current;
	if (item.isRoot()) {
		var action = new MM.Action.InsertNewItem(item, item.getChildren().length);
	} else {
		var parent = item.getParent();
		var index = parent.getChildren().indexOf(item);
		var action = new MM.Action.InsertNewItem(parent, index+1);
	}
	MM.App.action(action);

	MM.Command.Edit.execute();

	MM.publish("command-sibling");
}

MM.Command.InsertChild = Object.create(MM.Command, {
	label: {value: "Insert a child"},
	keys: {value: [
		{keyCode: 9, ctrlKey:false},
		{keyCode: 45}
	]}
});
MM.Command.InsertChild.execute = function() {
	var item = MM.App.current;
	var action = new MM.Action.InsertNewItem(item, item.getChildren().length);
	MM.App.action(action);

	MM.Command.Edit.execute();

	MM.publish("command-child");
}

MM.Command.Delete = Object.create(MM.Command, {
	label: {value: "Delete an item"},
	keys: {value: [{keyCode: MM.isMac() ? 8 : 46}]} // Mac keyboards' "delete" button generates 8 (backspace)
});
MM.Command.Delete.isValid = function() {
	return MM.Command.isValid.call(this) && !MM.App.current.isRoot();
}
MM.Command.Delete.execute = function() {
	var action = new MM.Action.RemoveItem(MM.App.current);
	MM.App.action(action);
}

MM.Command.Swap = Object.create(MM.Command, {
	label: {value: "Swap sibling"},
	keys: {value: [
		{keyCode: 38, ctrlKey:true},
		{keyCode: 40, ctrlKey:true},
	]}
});
MM.Command.Swap.execute = function(e) {
	var current = MM.App.current;
	if (current.isRoot() || current.getParent().getChildren().length < 2) { return; }

	var diff = (e.keyCode == 38 ? -1 : 1);
	var action = new MM.Action.Swap(MM.App.current, diff);
	MM.App.action(action);
}

MM.Command.Side = Object.create(MM.Command, {
	label: {value: "Change side"},
	keys: {value: [
		{keyCode: 37, ctrlKey:true},
		{keyCode: 39, ctrlKey:true},
	]}
});
MM.Command.Side.execute = function(e) {
	var current = MM.App.current;
	if (current.isRoot() || !current.getParent().isRoot()) { return; }

	var side = (e.keyCode == 37 ? "left" : "right");
	var action = new MM.Action.SetSide(MM.App.current, side);
	MM.App.action(action);
}

MM.Command.Save = Object.create(MM.Command, {
	label: {value: "Save map"},
	keys: {value: [{keyCode: "S".charCodeAt(0), ctrlKey:true, shiftKey:false}]}
});
MM.Command.Save.execute = function() {
	MM.App.io.quickSave();
}

MM.Command.SaveAs = Object.create(MM.Command, {
	label: {value: "Save as&hellip;"},
	keys: {value: [{keyCode: "S".charCodeAt(0), ctrlKey:true, shiftKey:true}]}
});
MM.Command.SaveAs.execute = function() {
	MM.App.io.show("save");
}

MM.Command.Load = Object.create(MM.Command, {
	label: {value: "Load map"},
	keys: {value: [{keyCode: "O".charCodeAt(0), ctrlKey:true}]}
});
MM.Command.Load.execute = function() {
	MM.App.io.show("load");
}

MM.Command.Center = Object.create(MM.Command, {
	label: {value: "Center map"},
	keys: {value: [{keyCode: 35}]}
});
MM.Command.Center.execute = function() {
	MM.App.map.center();
}

MM.Command.New = Object.create(MM.Command, {
	label: {value: "New map"},
	keys: {value: [{keyCode: "N".charCodeAt(0), ctrlKey:true}]}
});
MM.Command.New.execute = function() {
	if (!confirm("Throw away your current map and start a new one?")) { return; }
	var map = new MM.Map();
	MM.App.setMap(map);
	MM.publish("map-new", this);
}

MM.Command.ZoomIn = Object.create(MM.Command, {
	label: {value: "Zoom in"},
	keys: {value: [{charCode:"+".charCodeAt(0)}]}
});
MM.Command.ZoomIn.execute = function() {
	MM.App.adjustFontSize(1);
}

MM.Command.ZoomOut = Object.create(MM.Command, {
	label: {value: "Zoom out"},
	keys: {value: [{charCode:"-".charCodeAt(0)}]}
});
MM.Command.ZoomOut.execute = function() {
	MM.App.adjustFontSize(-1);
}

MM.Command.Help = Object.create(MM.Command, {
	label: {value: "Show/hide help"},
	keys: {value: [{charCode: "?".charCodeAt(0)}]}
});
MM.Command.Help.execute = function() {
	MM.App.help.toggle();
}

MM.Command.UI = Object.create(MM.Command, {
	label: {value: "Show/hide UI"},
	keys: {value: [{charCode: "*".charCodeAt(0)}]}
});
MM.Command.UI.execute = function() {
	MM.App.ui.toggle();
}

MM.Command.Pan = Object.create(MM.Command, {
	label: {value: "Pan the map"},
	keys: {value: [
		{keyCode: "W".charCodeAt(0), ctrlKey:false, altKey:false, metaKey:false},
		{keyCode: "A".charCodeAt(0), ctrlKey:false, altKey:false, metaKey:false},
		{keyCode: "S".charCodeAt(0), ctrlKey:false, altKey:false, metaKey:false},
		{keyCode: "D".charCodeAt(0), ctrlKey:false, altKey:false, metaKey:false}
	]},
	chars: {value: []}
});
MM.Command.Pan.execute = function(e) {
	var ch = String.fromCharCode(e.keyCode);
	var index = this.chars.indexOf(ch);
	if (index > -1) { return; }

	if (!this.chars.length) {
		window.addEventListener("keyup", this);
		this.interval = setInterval(this._step.bind(this), 50);
	}

	this.chars.push(ch);
	this._step();
}

MM.Command.Pan._step = function() {
	var dirs = {
		"W": [0, 1],
		"A": [1, 0],
		"S": [0, -1],
		"D": [-1, 0]
	}
	var offset = [0, 0];

	this.chars.forEach(function(ch) {
		offset[0] += dirs[ch][0];
		offset[1] += dirs[ch][1];
	});

	MM.App.map.moveBy(15*offset[0], 15*offset[1]);
}

MM.Command.Pan.handleEvent = function(e) {
	var ch = String.fromCharCode(e.keyCode);
	var index = this.chars.indexOf(ch);
	if (index > -1) {
		this.chars.splice(index, 1);
		if (!this.chars.length) {
			window.removeEventListener("keyup", this);
			clearInterval(this.interval);
		}
	}
}

MM.Command.Copy = Object.create(MM.Command, {
	label: {value: "Copy"},
	prevent: {value: false},
	keys: {value: [
		{keyCode: "C".charCodeAt(0), ctrlKey:true},
		{keyCode: "C".charCodeAt(0), metaKey:true}
	]}
});
MM.Command.Copy.execute = function() {
	MM.Clipboard.copy(MM.App.current);
}

MM.Command.Cut = Object.create(MM.Command, {
	label: {value: "Cut"},
	prevent: {value: false},
	keys: {value: [
		{keyCode: "X".charCodeAt(0), ctrlKey:true},
		{keyCode: "X".charCodeAt(0), metaKey:true}
	]}
});
MM.Command.Cut.execute = function() {
	MM.Clipboard.cut(MM.App.current);
}

MM.Command.Paste = Object.create(MM.Command, {
	label: {value: "Paste"},
	prevent: {value: false},
	keys: {value: [
		{keyCode: "V".charCodeAt(0), ctrlKey:true},
		{keyCode: "V".charCodeAt(0), metaKey:true}
	]}
});
MM.Command.Paste.execute = function() {
	MM.Clipboard.paste(MM.App.current);
}

MM.Command.Fold = Object.create(MM.Command, {
	label: {value: "Fold/Unfold"},
	keys: {value: [{charCode: "f".charCodeAt(0), ctrlKey:false}]}
});
MM.Command.Fold.execute = function() {
	var item = MM.App.current;
	if (item.isCollapsed()) { item.expand(); } else { item.collapse(); }
	MM.App.map.ensureItemVisibility(item);
}
MM.Command.Edit = Object.create(MM.Command, {
	label: {value: "Edit item"},
	keys: {value: [
		{keyCode: 32},
		{keyCode: 113}
	]}
});
MM.Command.Edit.execute = function() {
	MM.App.current.startEditing();
	MM.App.editing = true;
}

MM.Command.Finish = Object.create(MM.Command, {
	keys: {value: [{keyCode: 13, altKey:false, ctrlKey:false, shiftKey:false}]},
	editMode: {value: true}
});
MM.Command.Finish.execute = function() {
	MM.App.editing = false;
	var text = MM.App.current.stopEditing();
	if (text) {
		var action = new MM.Action.SetText(MM.App.current, text);
	} else {
		var action = new MM.Action.RemoveItem(MM.App.current);
	}
	MM.App.action(action);
}

MM.Command.Newline = Object.create(MM.Command, {
	label: {value: "Line break"},
	keys: {value: [
		{keyCode: 13, shiftKey:true},
		{keyCode: 13, ctrlKey:true}
	]},
	editMode: {value: true}
});
MM.Command.Newline.execute = function() {
	var range = getSelection().getRangeAt(0);
	var br = document.createElement("br");
	range.insertNode(br);
	range.setStartAfter(br);
	MM.App.current.updateSubtree();
}

MM.Command.Cancel = Object.create(MM.Command, {
	editMode: {value: true},
	keys: {value: [{keyCode: 27}]}
});
MM.Command.Cancel.execute = function() {
	MM.App.editing = false;
	MM.App.current.stopEditing();
	var oldText = MM.App.current.getText();
	if (!oldText) { /* newly added node */
		var action = new MM.Action.RemoveItem(MM.App.current);
		MM.App.action(action);
	}
}

MM.Command.Style = Object.create(MM.Command, {
	editMode: {value: null},
	command: {value: ""}
});

MM.Command.Style.execute = function() {
	if (MM.App.editing) {
		document.execCommand(this.command, null, null);
	} else {
		MM.Command.Edit.execute();
		var selection = getSelection();
		var range = selection.getRangeAt(0);
		range.selectNodeContents(MM.App.current.getDOM().text);
		selection.removeAllRanges();
		selection.addRange(range);
		this.execute();
		MM.Command.Finish.execute();
	}
}

MM.Command.Bold = Object.create(MM.Command.Style, {
	command: {value: "bold"},
	label: {value: "Bold"},
	keys: {value: [{keyCode: "B".charCodeAt(0), ctrlKey:true}]}
});

MM.Command.Underline = Object.create(MM.Command.Style, {
	command: {value: "underline"},
	label: {value: "Underline"},
	keys: {value: [{keyCode: "U".charCodeAt(0), ctrlKey:true}]}
});

MM.Command.Italic = Object.create(MM.Command.Style, {
	command: {value: "italic"},
	label: {value: "Italic"},
	keys: {value: [{keyCode: "I".charCodeAt(0), ctrlKey:true}]}
});

MM.Command.Strikethrough = Object.create(MM.Command.Style, {
	command: {value: "strikeThrough"},
	label: {value: "Strike-through"},
	keys: {value: [{keyCode: "S".charCodeAt(0), ctrlKey:true}]}
});

MM.Command.Value = Object.create(MM.Command, {
	label: {value: "Set value"},
	keys: {value: [{charCode: "v".charCodeAt(0), ctrlKey:false, metaKey:false}]}
});
MM.Command.Value.execute = function() {
	var item = MM.App.current;
	var oldValue = item.getValue();
	var newValue = prompt("Set item value", oldValue);
	if (newValue == null) { return; }

	if (!newValue.length) { newValue = null; }

	var numValue = parseFloat(newValue);
	var action = new MM.Action.SetValue(item, isNaN(numValue) ? newValue : numValue);
	MM.App.action(action);
}

MM.Command.Yes = Object.create(MM.Command, {
	label: {value: "Yes"},
	keys: {value: [{charCode: "y".charCodeAt(0), ctrlKey:false}]}
});
MM.Command.Yes.execute = function() {
	var item = MM.App.current;
	var status = (item.getStatus() == "yes" ? null : "yes");
	var action = new MM.Action.SetStatus(item, status);
	MM.App.action(action);
}

MM.Command.No = Object.create(MM.Command, {
	label: {value: "No"},
	keys: {value: [{charCode: "n".charCodeAt(0), ctrlKey:false}]}
});
MM.Command.No.execute = function() {
	var item = MM.App.current;
	var status = (item.getStatus() == "no" ? null : "no");
	var action = new MM.Action.SetStatus(item, status);
	MM.App.action(action);
}

MM.Command.Computed = Object.create(MM.Command, {
	label: {value: "Computed"},
	keys: {value: [{charCode: "c".charCodeAt(0), ctrlKey:false, metaKey:false}]}
});
MM.Command.Computed.execute = function() {
	var item = MM.App.current;
	var status = (item.getStatus() == "computed" ? null : "computed");
	var action = new MM.Action.SetStatus(item, status);
	MM.App.action(action);
}
MM.Command.Select = Object.create(MM.Command, {
	label: {value: "Move selection"},
	keys: {value: [
		{keyCode: 38, ctrlKey:false},
		{keyCode: 37, ctrlKey:false},
		{keyCode: 40, ctrlKey:false},
		{keyCode: 39, ctrlKey:false}
	]}
});
MM.Command.Select.execute = function(e) {
	var dirs = {
		37: "left",
		38: "top",
		39: "right",
		40: "bottom"
	}
	var dir = dirs[e.keyCode];

	var layout = MM.App.current.getLayout();
	var item = /*MM.App.map*/layout.pick(MM.App.current, dir);
	MM.App.select(item);
}

MM.Command.SelectRoot = Object.create(MM.Command, {
	label: {value: "Select root"},
	keys: {value: [{keyCode: 36}]}
});
MM.Command.SelectRoot.execute = function() {
	var item = MM.App.current;
	while (!item.isRoot()) { item = item.getParent(); }
	MM.App.select(item);
}

// Macs use keyCode 8 to delete instead
if (!MM.isMac()) {
	MM.Command.SelectParent = Object.create(MM.Command, {
		label: {value: "Select parent"},
		keys: {value: [{keyCode: 8}]}
	});
	MM.Command.SelectParent.execute = function() {
		if (MM.App.current.isRoot()) { return; }
		MM.App.select(MM.App.current.getParent());
	}
}
MM.Layout = Object.create(MM.Repo, {
	ALL: {value: []},
	SPACING_RANK: {value: 4},
	SPACING_CHILD: {value: 4},
});

MM.Layout.getAll = function() {
	return this.ALL;
}

/**
 * Re-draw an item and its children
 */
MM.Layout.update = function(item) {
	return this;
}

/**
 * @param {MM.Item} child Child node (its parent uses this layout)
 */
MM.Layout.getChildDirection = function(child) {
	return "";
}

MM.Layout.pick = function(item, dir) {
	var opposite = {
		left: "right",
		right: "left",
		top: "bottom",
		bottom: "top"
	}
	
	/* direction for a child */
	if (!item.isCollapsed()) {
		var children = item.getChildren();
		for (var i=0;i<children.length;i++) {
			var child = children[i];
			if (this.getChildDirection(child) == dir) { return child; }
		}
	}

	if (item.isRoot()) { return item; }
	
	var parentLayout = item.getParent().getLayout();
	var thisChildDirection = parentLayout.getChildDirection(item);
	if (thisChildDirection == dir) {
		return item;
	} else if (thisChildDirection == opposite[dir]) {
		return item.getParent();
	} else {
		return parentLayout.pickSibling(item, (dir == "left" || dir == "top" ? -1 : +1));
	}
}

MM.Layout.pickSibling = function(item, dir) {
	if (item.isRoot()) { return item; }

	var children = item.getParent().getChildren();
	var index = children.indexOf(item);
	index += dir;
	index = (index+children.length) % children.length;
	return children[index];
}

/**
 * Adjust canvas size and position
 */
MM.Layout._anchorCanvas = function(item) {
	var dom = item.getDOM();
	dom.canvas.width = dom.node.offsetWidth;
	dom.canvas.height = dom.node.offsetHeight;
}

MM.Layout._anchorToggle = function(item, x, y, side) {
	var node = item.getDOM().toggle;
	var w = node.offsetWidth;
	var h = node.offsetHeight;
	var l = x;
	var t = y;

	switch (side) {
		case "left":
			t -= h/2;
			l -= w;
		break;

		case "right":
			t -= h/2;
		break;
		
		case "top":
			l -= w/2;
			t -= h;
		break;

		case "bottom":
			l -= w/2;
		break;
	}
	
	node.style.left = Math.round(l) + "px";
	node.style.top = Math.round(t) + "px";
}

MM.Layout._getChildAnchor = function(item, side) {
	var dom = item.getDOM();
	if (side == "left" || side == "right") {
		var pos = dom.node.offsetLeft + dom.content.offsetLeft;
		if (side == "left") { pos += dom.content.offsetWidth; }
	} else {
		var pos = dom.node.offsetTop + dom.content.offsetTop;
		if (side == "top") { pos += dom.content.offsetHeight; }
	}
	return pos;
}

MM.Layout._computeChildrenBBox = function(children, childIndex) {
	var bbox = [0, 0];
	var rankIndex = (childIndex+1) % 2;

	children.forEach(function(child, index) {
		var node = child.getDOM().node;
		var childSize = [node.offsetWidth, node.offsetHeight];

		bbox[rankIndex] = Math.max(bbox[rankIndex], childSize[rankIndex]); /* adjust cardinal size */
		bbox[childIndex] += childSize[childIndex]; /* adjust orthogonal size */
	}, this);

	if (children.length > 1) { bbox[childIndex] += this.SPACING_CHILD * (children.length-1); } /* child separation */

	return bbox;
}

MM.Layout._alignItem = function(item, side) {
	var dom = item.getDOM();

	switch (side) {
		case "left":
			dom.content.insertBefore(dom.icon, dom.content.firstChild);
			dom.content.appendChild(dom.value);
			dom.content.appendChild(dom.status);
		break;
		case "right":
			dom.content.insertBefore(dom.icon, dom.content.firstChild);
			dom.content.insertBefore(dom.value, dom.content.firstChild);
			dom.content.insertBefore(dom.status, dom.content.firstChild);
		break;
	}
}
MM.Layout.Graph = Object.create(MM.Layout, {
	SPACING_RANK: {value: 16},
	childDirection: {value: ""}
});

MM.Layout.Graph.getChildDirection = function(child) {
	return this.childDirection;
}

MM.Layout.Graph.create = function(direction, id, label) {
	var layout = Object.create(this, {
		childDirection: {value:direction},
		id: {value:id},
		label: {value:label}
	});
	MM.Layout.ALL.push(layout);
	return layout;
}

MM.Layout.Graph.update = function(item) {
	var side = this.childDirection;
	if (!item.isRoot()) {
		side = item.getParent().getLayout().getChildDirection(item);
	}
	this._alignItem(item, side);

	this._layoutItem(item, this.childDirection);

	if (this.childDirection == "left" || this.childDirection == "right") {
		this._drawLinesHorizontal(item, this.childDirection);
	} else {
		this._drawLinesVertical(item, this.childDirection);
	}

	return this;
}


/**
 * Generic graph child layout routine. Updates item's orthogonal size according to the sum of its children.
 */
MM.Layout.Graph._layoutItem = function(item, rankDirection) {
	var sizeProps = ["width", "height"];
	var posProps = ["left", "top"];
	var rankIndex = (rankDirection == "left" || rankDirection == "right" ? 0 : 1);
	var childIndex = (rankIndex+1) % 2;

	var rankPosProp = posProps[rankIndex];
	var childPosProp = posProps[childIndex];
	var rankSizeProp = sizeProps[rankIndex];
	var childSizeProp = sizeProps[childIndex];

	var dom = item.getDOM();

	/* content size */
	var contentSize = [dom.content.offsetWidth, dom.content.offsetHeight];

	/* children size */
	var bbox = this._computeChildrenBBox(item.getChildren(), childIndex);

	/* node size */
	var rankSize = contentSize[rankIndex];
	if (bbox[rankIndex]) { rankSize += bbox[rankIndex] + this.SPACING_RANK; }
	var childSize = Math.max(bbox[childIndex], contentSize[childIndex]);
	dom.node.style[rankSizeProp] = rankSize + "px";
	dom.node.style[childSizeProp] = childSize + "px";

	var offset = [0, 0];
	if (rankDirection == "right") { offset[0] = contentSize[0] + this.SPACING_RANK; }
	if (rankDirection == "bottom") { offset[1] = contentSize[1] + this.SPACING_RANK; }
	offset[childIndex] = Math.round((childSize - bbox[childIndex])/2);
	this._layoutChildren(item.getChildren(), rankDirection, offset, bbox);

	/* label position */
	var labelPos = 0;
	if (rankDirection == "left") { labelPos = rankSize - contentSize[0]; }
	if (rankDirection == "top") { labelPos = rankSize - contentSize[1]; }
	dom.content.style[childPosProp] = Math.round((childSize - contentSize[childIndex])/2) + "px";
	dom.content.style[rankPosProp] = labelPos + "px";

	return this;
}

MM.Layout.Graph._layoutChildren = function(children, rankDirection, offset, bbox) {
	var posProps = ["left", "top"];

	var rankIndex = (rankDirection == "left" || rankDirection == "right" ? 0 : 1);
	var childIndex = (rankIndex+1) % 2;
	var rankPosProp = posProps[rankIndex];
	var childPosProp = posProps[childIndex];

	children.forEach(function(child, index) {
		var node = child.getDOM().node;
		var childSize = [node.offsetWidth, node.offsetHeight];

		if (rankDirection == "left") { offset[0] = bbox[0] - childSize[0]; }
		if (rankDirection == "top") { offset[1] = bbox[1] - childSize[1]; }

		node.style[childPosProp] = offset[childIndex] + "px";
		node.style[rankPosProp] = offset[rankIndex] + "px";

		offset[childIndex] += childSize[childIndex] + this.SPACING_CHILD; /* offset for next child */
	}, this);

	return bbox;
}

MM.Layout.Graph._drawLinesHorizontal = function(item, side) {
	this._anchorCanvas(item);
	this._drawHorizontalConnectors(item, side, item.getChildren());
}

MM.Layout.Graph._drawLinesVertical = function(item, side) {
	this._anchorCanvas(item);
	this._drawVerticalConnectors(item, side, item.getChildren());
}

MM.Layout.Graph._drawHorizontalConnectors = function(item, side, children) {
	if (children.length == 0) { return; }

	var dom = item.getDOM();
	var canvas = dom.canvas;
	var ctx = canvas.getContext("2d");
	ctx.strokeStyle = item.getColor();
	var R = this.SPACING_RANK/2;

	/* first part */
	var y1 = item.getShape().getVerticalAnchor(item);
	if (side == "left") {
		var x1 = dom.content.offsetLeft - 0.5;
	} else {
		var x1 = dom.content.offsetWidth + dom.content.offsetLeft + 0.5;
	}
	
	this._anchorToggle(item, x1, y1, side);
	if (item.isCollapsed()) { return; }

	if (children.length == 1) {
		var child = children[0];
		var y2 = child.getShape().getVerticalAnchor(child) + child.getDOM().node.offsetTop;
		var x2 = this._getChildAnchor(child, side);
		ctx.beginPath();
		ctx.moveTo(x1, y1);
		ctx.bezierCurveTo((x1+x2)/2, y1, (x1+x2)/2, y2, x2, y2);
		ctx.stroke();
		return;
	}

	if (side == "left") {
		var x2 = x1 - R;
	} else {
		var x2 = x1 + R;
	}

	ctx.beginPath();
	ctx.moveTo(x1, y1);
	ctx.lineTo(x2, y1);
	ctx.stroke();

	/* rounded connectors */
	var c1 = children[0];
	var c2 = children[children.length-1];
 	var x = x2;
 	var xx = x + (side == "left" ? -R : R);

	var y1 = c1.getShape().getVerticalAnchor(c1) + c1.getDOM().node.offsetTop;
	var y2 = c2.getShape().getVerticalAnchor(c2) + c2.getDOM().node.offsetTop;
	var x1 = this._getChildAnchor(c1, side);
	var x2 = this._getChildAnchor(c2, side);

	ctx.beginPath();
	ctx.moveTo(x1, y1);
	ctx.lineTo(xx, y1)
	ctx.arcTo(x, y1, x, y1+R, R);
	ctx.lineTo(x, y2-R);
	ctx.arcTo(x, y2, xx, y2, R);
	ctx.lineTo(x2, y2);

	for (var i=1; i<children.length-1; i++) {
		var c = children[i];
		var y = c.getShape().getVerticalAnchor(c) + c.getDOM().node.offsetTop;
		ctx.moveTo(x, y);
		ctx.lineTo(this._getChildAnchor(c, side), y);
	}
	ctx.stroke();
}

MM.Layout.Graph._drawVerticalConnectors = function(item, side, children) {
	if (children.length == 0) { return; }

	var dom = item.getDOM();
	var canvas = dom.canvas;
	var ctx = canvas.getContext("2d");
	ctx.strokeStyle = item.getColor();

	/* first part */
	var R = this.SPACING_RANK/2;
	
	var x = item.getShape().getHorizontalAnchor(item);
	var height = (children.length == 1 ? 2*R : R);

	if (side == "top") {
		var y1 = canvas.height - dom.content.offsetHeight;
		var y2 = y1 - height;
		this._anchorToggle(item, x, y1, side);
	} else {
		var y1 = item.getShape().getVerticalAnchor(item);
		var y2 = dom.content.offsetHeight + height;
		this._anchorToggle(item, x, dom.content.offsetHeight, side);
	}

	ctx.beginPath();
	ctx.moveTo(x, y1);
	ctx.lineTo(x, y2);
	ctx.stroke();


	if (children.length == 1) { return; }

	/* rounded connectors */
	var c1 = children[0];
	var c2 = children[children.length-1];
	var offset = dom.content.offsetHeight + height;
	var y = Math.round(side == "top" ? canvas.height - offset : offset) + 0.5;

	var x1 = c1.getShape().getHorizontalAnchor(c1) + c1.getDOM().node.offsetLeft;
	var x2 = c2.getShape().getHorizontalAnchor(c2) + c2.getDOM().node.offsetLeft;
	var y1 = this._getChildAnchor(c1, side);
	var y2 = this._getChildAnchor(c2, side);

	ctx.beginPath();
	ctx.moveTo(x1, y1);
	ctx.arcTo(x1, y, x1+R, y, R);
	ctx.lineTo(x2-R, y);
	ctx.arcTo(x2, y, x2, y2, R);

	for (var i=1; i<children.length-1; i++) {
		var c = children[i];
		var x = c.getShape().getHorizontalAnchor(c) + c.getDOM().node.offsetLeft;
		ctx.moveTo(x, y);
		ctx.lineTo(x, this._getChildAnchor(c, side));
	}
	ctx.stroke();
}


MM.Layout.Graph.Down = MM.Layout.Graph.create("bottom", "graph-bottom", "Bottom");
MM.Layout.Graph.Up = MM.Layout.Graph.create("top", "graph-top", "Top");
MM.Layout.Graph.Left = MM.Layout.Graph.create("left", "graph-left", "Left");
MM.Layout.Graph.Right = MM.Layout.Graph.create("right", "graph-right", "Right");
MM.Layout.Tree = Object.create(MM.Layout, {
	SPACING_RANK: {value: 32},
	childDirection: {value: ""}
});

MM.Layout.Tree.getChildDirection = function(child) {
	return this.childDirection;
}

MM.Layout.Tree.create = function(direction, id, label) {
	var layout = Object.create(this, {
		childDirection: {value:direction},
		id: {value:id},
		label: {value:label}
	});
	MM.Layout.ALL.push(layout);
	return layout;
}

MM.Layout.Tree.update = function(item) {
	var side = this.childDirection;
	if (!item.isRoot()) {
		side = item.getParent().getLayout().getChildDirection(item);
	}
	this._alignItem(item, side);

	this._layoutItem(item, this.childDirection);
	this._anchorCanvas(item);
	this._drawLines(item, this.childDirection);
	return this;
}

/**
 * Generic graph child layout routine. Updates item's orthogonal size according to the sum of its children.
 */
MM.Layout.Tree._layoutItem = function(item, rankDirection) {
	var dom = item.getDOM();

	/* content size */
	var contentSize = [dom.content.offsetWidth, dom.content.offsetHeight];

	/* children size */
	var bbox = this._computeChildrenBBox(item.getChildren(), 1);

	/* node size */
	var rankSize = contentSize[0];
	var childSize = bbox[1] + contentSize[1];
	if (bbox[0]) { 
		rankSize = Math.max(rankSize, bbox[0] + this.SPACING_RANK); 
		childSize += this.SPACING_CHILD;
	}
	dom.node.style.width = rankSize + "px";
	dom.node.style.height = childSize + "px";

	var offset = [this.SPACING_RANK, contentSize[1]+this.SPACING_CHILD];
	if (rankDirection == "left") { offset[0] = rankSize - bbox[0] - this.SPACING_RANK; }
	this._layoutChildren(item.getChildren(), rankDirection, offset, bbox);

	/* label position */
	var labelPos = 0;
	if (rankDirection == "left") { labelPos = rankSize - contentSize[0]; }
	dom.content.style.left = labelPos + "px";
	dom.content.style.top = 0;

	return this;
}

MM.Layout.Tree._layoutChildren = function(children, rankDirection, offset, bbox) {
	children.forEach(function(child, index) {
		var node = child.getDOM().node;
		var childSize = [node.offsetWidth, node.offsetHeight];
		var left = offset[0];
		if (rankDirection == "left") { left += (bbox[0] - childSize[0]); }

		node.style.left = left + "px";
		node.style.top = offset[1] + "px";

		offset[1] += childSize[1] + this.SPACING_CHILD; /* offset for next child */
	}, this);

	return bbox;
}

MM.Layout.Tree._drawLines = function(item, side) {
	var dom = item.getDOM();
	var canvas = dom.canvas;

	var R = this.SPACING_RANK/4;
	var x = (side == "left" ? canvas.width - 2*R : 2*R) + 0.5;
	this._anchorToggle(item, x, dom.content.offsetHeight, "bottom");

	var children = item.getChildren();
	if (children.length == 0 || item.isCollapsed()) { return; }

	var ctx = canvas.getContext("2d");
	ctx.strokeStyle = item.getColor();

	var y1 = item.getShape().getVerticalAnchor(item);
	var last = children[children.length-1];
	var y2 = last.getShape().getVerticalAnchor(last) + last.getDOM().node.offsetTop;

	ctx.beginPath();
	ctx.moveTo(x, y1);
	ctx.lineTo(x, y2 - R);

	/* rounded connectors */
	for (var i=0; i<children.length; i++) {
		var c = children[i];
		var y = c.getShape().getVerticalAnchor(c) + c.getDOM().node.offsetTop;
		var anchor = this._getChildAnchor(c, side);

		ctx.moveTo(x, y - R);
		ctx.arcTo(x, y, anchor, y, R);
		ctx.lineTo(anchor, y);
	}
	ctx.stroke();
}

MM.Layout.Tree.Left = MM.Layout.Tree.create("left", "tree-left", "Left");
MM.Layout.Tree.Right = MM.Layout.Tree.create("right", "tree-right", "Right");
MM.Layout.Map = Object.create(MM.Layout.Graph, {
	id: {value:"map"},
	label: {value:"Map"},
	LINE_THICKNESS: {value:8}
});
MM.Layout.ALL.push(MM.Layout.Map);

MM.Layout.Map.update = function(item) {
	if (item.isRoot()) {
		this._layoutRoot(item);
	} else {
		var side = this.getChildDirection(item);
		var name = side.charAt(0).toUpperCase() + side.substring(1);
		MM.Layout.Graph[name].update(item);
	}
}

/**
 * @param {MM.Item} child Child node
 */
MM.Layout.Map.getChildDirection = function(child) {
	while (!child.getParent().isRoot()) {
		child = child.getParent();
	}
	/* child is now the sub-root node */

	var side = child.getSide();
	if (side) { return side; }

	var counts = {left:0, right:0};
	var children = child.getParent().getChildren();
	for (var i=0;i<children.length;i++) {
		var side = children[i].getSide();
		if (!side) {
			side = (counts.right > counts.left ? "left" : "right");
			children[i].setSide(side);
		}
		counts[side]++;
	}

	return child.getSide();
}

MM.Layout.Map.pickSibling = function(item, dir) {
	if (item.isRoot()) { return item; }

	var parent = item.getParent();
	var children = parent.getChildren();
	if (parent.isRoot()) {
		var side = this.getChildDirection(item);
		children = children.filter(function(child) {
			return (this.getChildDirection(child) == side);
		}, this);
	}
	
	var index = children.indexOf(item);
	index += dir;
	index = (index+children.length) % children.length;
	return children[index];
}

MM.Layout.Map._layoutRoot = function(item) {
	this._alignItem(item, "right");

	var dom = item.getDOM();

	var children = item.getChildren();
	var childrenLeft = [];
	var childrenRight = [];

	children.forEach(function(child, index) {
		var node = child.getDOM().node;
		var side = this.getChildDirection(child);
		
		if (side == "left") {
			childrenLeft.push(child);
		} else {
			childrenRight.push(child);
		}
	}, this);

	var bboxLeft = this._computeChildrenBBox(childrenLeft, 1);
	var bboxRight = this._computeChildrenBBox(childrenRight, 1);
	var height = Math.max(bboxLeft[1], bboxRight[1], dom.content.offsetHeight);

	var left = 0;
	this._layoutChildren(childrenLeft, "left", [left, Math.round((height-bboxLeft[1])/2)], bboxLeft);
	left += bboxLeft[0];

	if (childrenLeft.length) { left += this.SPACING_RANK; }
	dom.content.style.left = left + "px";
	left += dom.content.offsetWidth;

	if (childrenRight.length) { left += this.SPACING_RANK; }
	this._layoutChildren(childrenRight, "right", [left, Math.round((height-bboxRight[1])/2)], bboxRight);
	left += bboxRight[0];

	dom.content.style.top = Math.round((height - dom.content.offsetHeight)/2) + "px";
	dom.node.style.height = height + "px";
	dom.node.style.width = left + "px";

	this._anchorCanvas(item);
	this._drawRootConnectors(item, "left", childrenLeft);
	this._drawRootConnectors(item, "right", childrenRight);
}

MM.Layout.Map._drawRootConnectors = function(item, side, children) {
	if (children.length == 0 || item.isCollapsed()) { return; }

	var dom = item.getDOM();
	var canvas = dom.canvas;
	var ctx = canvas.getContext("2d");
	var R = this.SPACING_RANK/2;

	var x1 = dom.content.offsetLeft + dom.content.offsetWidth/2;
	var y1 = item.getShape().getVerticalAnchor(item);
	var half = this.LINE_THICKNESS/2;

	for (var i=0;i<children.length;i++) {
		var child = children[i];

		var x2 = this._getChildAnchor(child, side);
		var y2 = child.getShape().getVerticalAnchor(child) + child.getDOM().node.offsetTop;
		var angle = Math.atan2(y2-y1, x2-x1) + Math.PI/2;
		var dx = Math.cos(angle) * half;
		var dy = Math.sin(angle) * half;

		ctx.fillStyle = ctx.strokeStyle = child.getColor();
		ctx.beginPath();
		ctx.moveTo(x1-dx, y1-dy);
		ctx.quadraticCurveTo((x2+x1)/2, y2, x2, y2);
		ctx.quadraticCurveTo((x2+x1)/2, y2, x1+dx, y1+dy);
		ctx.fill();
		ctx.stroke();
	}

}
MM.Shape = Object.create(MM.Repo, {
	VERTICAL_OFFSET: {value: 0.5},
});

MM.Shape.set = function(item) {
	item.getDOM().node.classList.add("shape-"+this.id);
	return this;
}

MM.Shape.unset = function(item) {
	item.getDOM().node.classList.remove("shape-"+this.id);
	return this;
}

MM.Shape.update = function(item) {
	item.getDOM().content.style.borderColor = item.getColor();
	return this;
}

MM.Shape.getHorizontalAnchor = function(item) {
	var node = item.getDOM().content;
	return Math.round(node.offsetLeft + node.offsetWidth/2) + 0.5;
}

MM.Shape.getVerticalAnchor = function(item) {
	var node = item.getDOM().content;
	return node.offsetTop + Math.round(node.offsetHeight * this.VERTICAL_OFFSET) + 0.5;
}
MM.Shape.Underline = Object.create(MM.Shape, {
	id: {value: "underline"},
	label: {value: "Underline"},
	VERTICAL_OFFSET: {value: -3}
});

MM.Shape.Underline.update = function(item) {
	var dom = item.getDOM();

	var ctx = dom.canvas.getContext("2d");
	ctx.strokeStyle = item.getColor();

	var left = dom.content.offsetLeft;
	var right = left + dom.content.offsetWidth;

	var top = this.getVerticalAnchor(item);

	ctx.beginPath();
	ctx.moveTo(left, top);
	ctx.lineTo(right, top);
	ctx.stroke();
}

MM.Shape.Underline.getVerticalAnchor = function(item) {
	var node = item.getDOM().content;
	return node.offsetTop + node.offsetHeight + this.VERTICAL_OFFSET + 0.5;
}
MM.Shape.Box = Object.create(MM.Shape, {
	id: {value: "box"},
	label: {value: "Box"}
});
MM.Shape.Ellipse = Object.create(MM.Shape, {
	id: {value: "ellipse"},
	label: {value: "Ellipse"}
});
MM.Format = Object.create(MM.Repo, {
	extension: {value:""},
	mime: {value:""}
});

MM.Format.getByName = function(name) {
	var index = name.lastIndexOf(".");
	if (index == -1) { return null; }
	var extension = name.substring(index+1).toLowerCase(); 
	return this.getByProperty("extension", extension);
}

MM.Format.getByMime = function(mime) {
	return this.getByProperty("mime", mime);
}

MM.Format.to = function(data) {}
MM.Format.from = function(data) {}

MM.Format.nl2br = function(str) {
	return str.replace(/\n/g, "<br/>");
}

MM.Format.br2nl = function(str) {
	return str.replace(/<br\s*\/?>/g, "\n");
}
MM.Format.JSON = Object.create(MM.Format, {
	id: {value: "json"},
	label: {value: "Native (JSON)"},
	extension: {value: "mymind"},
	mime: {value: "application/vnd.mymind+json"}
});

MM.Format.JSON.to = function(data) { 
	return JSON.stringify(data, null, "\t") + "\n";
}

MM.Format.JSON.from = function(data) {
	return JSON.parse(data);
}
MM.Format.FreeMind = Object.create(MM.Format, {
	id: {value: "freemind"},
	label: {value: "FreeMind"},
	extension: {value: "mm"},
	mime: {value: "application/x-freemind"}
});

MM.Format.FreeMind.to = function(data) {
	var doc = document.implementation.createDocument(null, null, null);
	var map = doc.createElement("map");

	map.setAttribute("version", "1.0.1");
	map.appendChild(this._serializeItem(doc, data.root));

	doc.appendChild(map);
	var serializer = new XMLSerializer();
	return serializer.serializeToString(doc);
}

MM.Format.FreeMind.from = function(data) {
	var parser = new DOMParser();
	var doc = parser.parseFromString(data, "application/xml");
	if (doc.documentElement.nodeName.toLowerCase() == "parsererror") { throw new Error(doc.documentElement.textContent); }

	var root = doc.documentElement.getElementsByTagName("node")[0];
	if (!root) { throw new Error("No root node found"); }

	var json = {
		root: this._parseNode(root, {shape:"underline"})
	};
	json.root.layout = "map";
	json.root.shape = "ellipse";

	return json;
}

MM.Format.FreeMind._serializeItem = function(doc, json) {
	var elm = this._serializeAttributes(doc, json);

	(json.children || []).forEach(function(child) {
		elm.appendChild(this._serializeItem(doc, child));
	}, this);

	return elm;
}

MM.Format.FreeMind._serializeAttributes = function(doc, json) {
	var elm = doc.createElement("node");
	elm.setAttribute("TEXT", MM.Format.br2nl(json.text));
	elm.setAttribute("ID", json.id);

	if (json.side) { elm.setAttribute("POSITION", json.side); }
	if (json.shape == "box") { elm.setAttribute("STYLE", "bubble"); }
	if (json.collapsed) { elm.setAttribute("FOLDED", "true"); }

	if (json.notes) {
	    var notesElm = doc.createElement("richcontent");
	    notesElm.setAttribute("TYPE", "NOTE");
	    // note: the freemind file format isn't very good.
	    notesElm.appendChild(doc.createCDATASection('<html><head></head><body>' + json.notes + '</body></html>'));
	    elm.appendChild(notesElm);
    }

	return elm;
}

MM.Format.FreeMind._parseNode = function(node, parent) {
	var json = this._parseAttributes(node, parent);

	for (var i=0;i<node.childNodes.length;i++) {
		var child = node.childNodes[i];
		if (child.nodeName.toLowerCase() == "node") {
			json.children.push(this._parseNode(child, json));
		}
	}

	return json;
}

MM.Format.FreeMind._parseAttributes = function(node, parent) {
	var json = {
		children: [],
		text: MM.Format.nl2br(node.getAttribute("TEXT") || ""),
		id: node.getAttribute("ID")
	};

	var position = node.getAttribute("POSITION");
	if (position) { json.side = position; }

	var style = node.getAttribute("STYLE");
	if (style == "bubble") {
		json.shape = "box";
	} else {
		json.shape = parent.shape;
	}

	if (node.getAttribute("FOLDED") == "true") { json.collapsed = 1; }

	var children = node.children;
	for (var i=0;i<children.length;i++) {
		var child = children[i];
		switch (child.nodeName.toLowerCase()) {
			case "richcontent":
				if (child.getAttribute("TYPE") == "NOTE") {
					var body = child.querySelector("body > *");
					if (body) {
						var serializer = new XMLSerializer();
						json.notes = serializer.serializeToString(body).trim();
					}
				}
			break;

			case "font":
				if (child.getAttribute("ITALIC") == "true") { json.text = "<i>" + json.text + "</i>"; }
				if (child.getAttribute("BOLD") == "true") { json.text = "<b>" + json.text + "</b>"; }
			break;
		}
	}

	return json;
}
MM.Format.MMA = Object.create(MM.Format.FreeMind, {
	id: {value: "mma"},
	label: {value: "Mind Map Architect"},
	extension: {value: "mma"}
});

MM.Format.MMA._parseAttributes = function(node, parent) {
	var json = {
		children: [],
		text: MM.Format.nl2br(node.getAttribute("title") || ""),
		shape: "box"
	};

	if (node.getAttribute("expand") == "false") { json.collapsed = 1; }

	var direction = node.getAttribute("direction");
	if (direction == "0") { json.side = "left"; }
	if (direction == "1") { json.side = "right"; }

	var color = node.getAttribute("color");
	if (color) {
		var re = color.match(/^#(....)(....)(....)$/);
		if (re) {
			var r = parseInt(re[1], 16) >> 8;
			var g = parseInt(re[2], 16) >> 8;
			var b = parseInt(re[3], 16) >> 8;
			r = Math.round(r/17).toString(16);
			g = Math.round(g/17).toString(16);
			b = Math.round(b/17).toString(16);
		}
		json.color = "#" + [r,g,b].join("");
	}

	json.icon = node.getAttribute("icon");

	return json;
}

MM.Format.MMA._serializeAttributes = function(doc, json) {
	var elm = doc.createElement("node");
	elm.setAttribute("title", MM.Format.br2nl(json.text));
	elm.setAttribute("expand", json.collapsed ? "false" : "true");

	if (json.side) { elm.setAttribute("direction", json.side == "left" ? "0" : "1"); }
	if (json.color) {
		var parts = json.color.match(/^#(.)(.)(.)$/);
		var r = new Array(5).join(parts[1]);
		var g = new Array(5).join(parts[2]);
		var b = new Array(5).join(parts[3]);
		elm.setAttribute("color", "#" + [r,g,b].join(""));
	}
	if (json.icon) {
		elm.setAttribute("icon", json.icon);
	}

	return elm;
}
MM.Format.Mup = Object.create(MM.Format, {
	id: {value: "mup"},
	label: {value: "MindMup"},
	extension: {value: "mup"}
});

MM.Format.Mup.to = function(data) {
	var root = this._MMtoMup(data.root);
	return JSON.stringify(root, null, 2);
}

MM.Format.Mup.from = function(data) {
	var source = JSON.parse(data);
	var root = this._MupToMM(source);
	root.layout = "map";

	var map = {
		root: root
	}

	return map;
}

MM.Format.Mup._MupToMM = function(item) {
	var json = {
		text: MM.Format.nl2br(item.title),
		id: item.id,
		shape: "box",
		icon: item.icon
	}

	if (item.attr && item.attr.style && item.attr.style.background) {
		json.color = item.attr.style.background;
	}

	if (item.attr && item.attr.collapsed) {
		json.collapsed = 1;
	}

	if (item.ideas) {
		var data = [];
		for (var key in item.ideas) {
			var child = this._MupToMM(item.ideas[key]);
			var num = parseFloat(key);
			child.side = (num < 0 ? "left" : "right");
			data.push({
				child: child,
				num: num
			});
		}

		data.sort(function(a, b) {
			return a.num-b.num;
		});

		json.children = data.map(function(item) { return item.child; });
	}

	return json;
}

MM.Format.Mup._MMtoMup = function(item, side) {
	var result = {
		id: item.id,
		title: MM.Format.br2nl(item.text),
		icon: item.icon,
		attr: {}
	}
	if (item.color) {
		result.attr.style = {background:item.color};
	}
	if (item.collapsed) {
		result.attr.collapsed = true;
	}

	if (item.children) {
		result.ideas = {};

		for (var i=0;i<item.children.length;i++) {
			var child = item.children[i];
			var childSide = side || child.side;

			var key = i+1;
			if (childSide == "left") { key *= -1; }

			result.ideas[key] = this._MMtoMup(child, childSide);
		}
	}

	return result;
}
MM.Format.Plaintext = Object.create(MM.Format, {
	id: {value: "plaintext"},
	label: {value: "Plain text"},
	extension: {value: "txt"},
	mime: {value: "application/vnd.mymind+txt"}
});

/**
 * Can serialize also a sub-tree
 */
MM.Format.Plaintext.to = function(data) {
	return this._serializeItem(data.root || data);
}

MM.Format.Plaintext.from = function(data) {
	var lines = data.split("\n").filter(function(line) {
		return line.match(/\S/);
	});

	var items = this._parseItems(lines);

	if (items.length == 1) {
		var result = {
			root: items[0]
		}
	} else {
		var result = {
			root: {
				text: "",
				children: items
			}
		}
	}
	result.root.layout = "map";

	return result;
}

MM.Format.Plaintext._serializeItem = function(item, depth) {
	depth = depth || 0;

	var lines = (item.children || []) .map(function(child) {
		return this._serializeItem(child, depth+1);
	}, this);

	var prefix = new Array(depth+1).join("\t");
	lines.unshift(prefix + item.text.replace(/\n/g, ""));

	return lines.join("\n") + (depth ? "" : "\n");
}


MM.Format.Plaintext._parseItems = function(lines) {
	var items = [];
	if (!lines.length) { return items; }
	var firstPrefix = this._parsePrefix(lines[0]);

	var currentItem = null;
	var childLines = [];

	/* finalize a block of sub-children by converting them to items and appending */
	var convertChildLinesToChildren = function() { 
		if (!currentItem || !childLines.length) { return; }
		var children = this._parseItems(childLines);
		if (children.length) { currentItem.children = children; }
		childLines = [];
	}

	lines.forEach(function(line, index) {
		if (this._parsePrefix(line) == firstPrefix) { /* new top-level item! */
			convertChildLinesToChildren.call(this); /* finalize previous item */
			currentItem = {text:line.match(/^\s*(.*)/)[1]};
			items.push(currentItem);
		} else { /* prepare as a future child */
			childLines.push(line);
		}
	}, this);

	convertChildLinesToChildren.call(this);

	return items;
}

MM.Format.Plaintext._parsePrefix = function(line) {
	return line.match(/^\s*/)[0];
}
MM.Backend = Object.create(MM.Repo);

/**
 * Backends are allowed to have some internal state. 
 * This method notifies them that "their" map is no longer used 
 * (was either replaced by a new one or saved using other backend).
 */ 
MM.Backend.reset = function() {
}

MM.Backend.save = function(data, name) {
}

MM.Backend.load = function(name) {
}
MM.Backend.Local = Object.create(MM.Backend, {
	label: {value: "Browser storage"},
	id: {value: "local"},
	prefix: {value: "mm.map."}
});

MM.Backend.Local.save = function(data, id, name) {
	localStorage.setItem(this.prefix + id, data);

	var names = this.list();
	names[id] = name;
	localStorage.setItem(this.prefix + "names", JSON.stringify(names));
}

MM.Backend.Local.load = function(id) {
	var data = localStorage.getItem(this.prefix + id);
	if (!data) { throw new Error("There is no such saved map"); }
	return data;
}

MM.Backend.Local.remove = function(id) {
	localStorage.removeItem(this.prefix + id);

	var names = this.list();
	delete names[id];
	localStorage.setItem(this.prefix + "names", JSON.stringify(names));
}

MM.Backend.Local.list = function() {
	try {
		var data = localStorage.getItem(this.prefix + "names") || "{}";
		return JSON.parse(data);
	} catch (e) {
		return {};
	}
}
MM.Backend.WebDAV = Object.create(MM.Backend, {
	id: {value: "webdav"},
	label: {value: "Generic WebDAV"}
});

MM.Backend.WebDAV.save = function(data, url) {
	return this._request("put", url, data);
}

MM.Backend.WebDAV.load = function(url) {
	return this._request("get", url);
}

MM.Backend.WebDAV._request = function(method, url, data) {
	var xhr = new XMLHttpRequest();
	xhr.open(method, url, true);
	xhr.withCredentials = true;

	var promise = new Promise();
	
	Promise.send(xhr, data).then(
		function(xhr) { promise.fulfill(xhr.responseText); },
		function(xhr) { promise.reject(new Error("HTTP/" + xhr.status + "\n\n" + xhr.responseText)); }
	);

	return promise;
}
MM.Backend.Image = Object.create(MM.Backend, {
	id: {value: "image"},
	label: {value: "Image"},
	url: {value:"", writable:true}
});

MM.Backend.Image.save = function(data, name) {
	var form = document.createElement("form");
	form.action = this.url;
	form.method = "post";
	form.target = "_blank";

	var input = document.createElement("input");
	input.type = "hidden";
	input.name = "data";
	input.value = data;
	form.appendChild(input);

	var input = document.createElement("input");
	input.type = "hidden";
	input.name = "name";
	input.value = name;
	form.appendChild(input);

	document.body.appendChild(form);
	form.submit();
	form.parentNode.removeChild(form);
}
MM.Backend.File = Object.create(MM.Backend, {
	id: {value: "file"},
	label: {value: "File"},
	input: {value:document.createElement("input")}
});

MM.Backend.File.save = function(data, name) {
	var link = document.createElement("a");
	link.download = name;
	link.href = "data:text/plain;base64," + btoa(unescape(encodeURIComponent(data)));
	document.body.appendChild(link);
	link.click();
	link.parentNode.removeChild(link);

	var promise = new Promise().fulfill();
	return promise;
}

MM.Backend.File.load = function() {
	var promise = new Promise();

	this.input.type = "file";

	this.input.onchange = function(e) {
		var file = e.target.files[0];
		if (!file) { return; }

		var reader = new FileReader();
		reader.onload = function() { promise.fulfill({data:reader.result, name:file.name}); }
		reader.onerror = function() { promise.reject(reader.error); }
		reader.readAsText(file);
	}.bind(this);

	this.input.click();
	return promise;
}
MM.Backend.Firebase = Object.create(MM.Backend, {
	label: {value: "Firebase"},
	id: {value: "firebase"},
	ref: {value:null, writable:true},
	_current: {value: {
		id: null,
		name: null,
		data: null
	}}
});

MM.Backend.Firebase.connect = function(server, auth) {
	// Initialize Firebase
	var config = {
		apiKey: "AIzaSyBO_6uCK8pHjoz1c9htVwZi6Skpm8o4LtQ",
		authDomain: "my-mind.firebaseapp.com",
		databaseURL: "https://" + server + ".firebaseio.com",
		projectId: "firebase-my-mind",
		storageBucket: "firebase-my-mind.appspot.com",
		messagingSenderId: "666556281676"
	};
	firebase.initializeApp(config);

	this.ref = firebase.database().ref();
	
	this.ref.child("names").on("value", function(snap) {
		MM.publish("firebase-list", this, snap.val() || {});
	}, this);

	if (auth) {
		return this._login(auth);
	} else {
		return new Promise().fulfill();
	}
}

MM.Backend.Firebase.save = function(data, id, name) {
	var promise = new Promise();

	try {
		this.ref.child("names/" + id).set(name);
		this.ref.child("data/" + id).set(data, function(result) {
			if (result) {
				promise.reject(result);
			} else {
				promise.fulfill();
				this._listenStart(data, id);
			}
		}.bind(this));
	} catch (e) {
		promise.reject(e);
	}
	return promise;
}

MM.Backend.Firebase.load = function(id) {
	var promise = new Promise();
	
	this.ref.child("data/" + id).once("value", function(snap) {
		var data = snap.val();
		if (data) {
			promise.fulfill(data);
			this._listenStart(data, id);
		} else {
			promise.reject(new Error("There is no such saved map"));
		}
	}, this);
	return promise;
}

MM.Backend.Firebase.remove = function(id) {
	var promise = new Promise();

	try {
		this.ref.child("names/" + id).remove();
		this.ref.child("data/" + id).remove(function(result) {
			if (result) {
				promise.reject(result);
			} else {
				promise.fulfill();
			}
		});
	} catch (e) {
		promise.reject(e);
	}

	return promise;
}

MM.Backend.Firebase.reset = function() {
	this._listenStop(); /* do not monitor current firebase ref for changes */
}

/**
 * Merge current (remote) data with updated map
 */
MM.Backend.Firebase.mergeWith = function(data, name) {
	var id = this._current.id;

	if (name != this._current.name) {
		this._current.name = name;
		this.ref.child("names/" + id).set(name);
	}


	var dataRef = this.ref.child("data/" + id);
	var oldData = this._current.data;

	this._listenStop();
	this._recursiveRefMerge(dataRef, oldData, data);
	this._listenStart(data, id);
}

/**
 * @param {Firebase} ref
 * @param {object} oldData
 * @param {object} newData
 */
MM.Backend.Firebase._recursiveRefMerge = function(ref, oldData, newData) {
	var updateObject = {};

	if (newData instanceof Array) { /* merge arrays */

		for (var i=0; i<newData.length; i++) {
			var newValue = newData[i];

			if (!(i in oldData)) { /* new key */
				updateObject[i] = newValue;
			} else if (typeof(newValue) == "object") { /* recurse */
				this._recursiveRefMerge(ref.child(i), oldData[i], newValue);
			} else if (newValue !== oldData[i]) { /* changed key */
				updateObject[i] = newValue;
			}
		}

		for (var i=newData.length; i<oldData.length; i++) { updateObject[i] = null; } /* removed array items */

	} else { /* merge objects */

		for (var p in newData) { /* new/changed keys */
			var newValue = newData[p];

			if (!(p in oldData)) { /* new key */
				updateObject[p] = newValue;
			} else if (typeof(newValue) == "object") { /* recurse */
				this._recursiveRefMerge(ref.child(p), oldData[p], newValue);
			} else if (newValue !== oldData[p]) { /* changed key */
				updateObject[p] = newValue;
			}

		}

		for (var p in oldData) { /* removed keys */
			if (!(p in newData)) { updateObject[p] = null; }
		}

	}

	if (Object.keys(updateObject).length) { ref.update(updateObject); }
}

MM.Backend.Firebase._listenStart = function(data, id) {
	if (this._current.id && this._current.id == id) { return; }

	this._listenStop();
	this._current.id = id;
	this._current.data = data;

	this.ref.child("data/" + id).on("value", this._valueChange, this);
}

MM.Backend.Firebase._listenStop = function() {
	if (!this._current.id) { return; }

	this.ref.child("data/" + this._current.id).off("value");
	this._current.id = null;
	this._current.name = null;
	this._current.data = null;
}


/**
 * Monitored remote ref changed.
 * FIXME move timeout logic to ui.backend.firebase?
 */
MM.Backend.Firebase._valueChange = function(snap) {
	this._current.data = snap.val();
	if (this._changeTimeout) { clearTimeout(this._changeTimeout); }
	this._changeTimeout = setTimeout(function() {
		MM.publish("firebase-change", this, this._current.data);
	}.bind(this), 200);
}

MM.Backend.Firebase._login = function(type) {
	var provider;
	switch (type) {
		case "github":
			provider = new firebase.auth.GithubAuthProvider();
		break;
		case "facebook":
			provider = new firebase.auth.FacebookAuthProvider();
		break;
		case "twitter":
			provider = new firebase.auth.TwitterAuthProvider();
		break;
		case "google":
			provider = new firebase.auth.GoogleAuthProvider();
		break;
	}

	return firebase.auth().signInWithPopup(provider).then(function(result) {
		return result.user;
	});
}
MM.Backend.GDrive = Object.create(MM.Backend, {
	id: {value: "gdrive"},
	label: {value: "Google Drive"},
	scope: {value: "https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.install"},
	clientId: {value: "767837575056-h87qmlhmhb3djhaaqta5gv2v3koa9hii.apps.googleusercontent.com"},
	apiKey: {value: "AIzaSyCzu1qVxlgufneOYpBgDJXN6Z9SNVcHYWM"},
	fileId: {value: null, writable: true}
});

MM.Backend.GDrive.reset = function() {
	this.fileId = null;
}

MM.Backend.GDrive.save = function(data, name, mime) {
	return this._connect().then(
		function() {
			return this._send(data, name, mime);
		}.bind(this)
	);
}

MM.Backend.GDrive._send = function(data, name, mime) {
	var promise = new Promise();

	var path = "/upload/drive/v2/files";
	var method = "POST";
	if (this.fileId) {
		path += "/" + this.fileId;
		method = "PUT";
	}

	var boundary = "b" + Math.random();
	var delimiter = "--" + boundary;
	var body = [
		delimiter,
		"Content-Type: application/json", "",
		JSON.stringify({title:name}),
		delimiter,
		"Content-Type: " + mime, "",
		data,
		delimiter + "--"
	].join("\r\n");

	var request = gapi.client.request({
		path: path,
		method: method,
		headers: {
			"Content-Type": "multipart/mixed; boundary='" + boundary + "'"
		},
		body: body
	});

	request.execute(function(response) {
		if (!response) {
			promise.reject(new Error("Failed to upload to Google Drive"));
		} else if (response.error) {
			promise.reject(response.error);
		} else {
			this.fileId = response.id;
			promise.fulfill();
		}
	}.bind(this));
	
	return promise;
}

MM.Backend.GDrive.load = function(id) {
	return this._connect().then(
		this._load.bind(this, id)
	);
}

MM.Backend.GDrive._load = function(id) {
	this.fileId = id;

	var promise = new Promise();

	var request = gapi.client.request({
		path: "/drive/v2/files/" + this.fileId,
		method: "GET"
	});

	request.execute(function(response) {
		if (response && response.downloadUrl) {
			var xhr = new XMLHttpRequest();
			xhr.open("get", response.downloadUrl, true);
			xhr.setRequestHeader("Authorization", "Bearer " + gapi.auth.getToken().access_token);
			Promise.send(xhr).then(
				function(xhr) { promise.fulfill({data:xhr.responseText, name:response.title, mime:response.mimeType}); },
				function(xhr) { promise.reject(xhr.responseText); }
			);
		} else {
			promise.reject(response && response.error || new Error("Failed to download file"));
		}
	}.bind(this));

	return promise;
}

MM.Backend.GDrive.pick = function() {
	return this._connect().then(
		this._pick.bind(this)
	);
}

MM.Backend.GDrive._pick = function() {
	var promise = new Promise();

	var token = gapi.auth.getToken();
	var formats = MM.Format.getAll();
	var mimeTypes = ["application/json; charset=UTF-8", "application/json"];
	formats.forEach(function(format) {
		if (format.mime) { mimeTypes.unshift(format.mime); }
	});

	var view = new google.picker.DocsView(google.picker.ViewId.DOCS)
		.setMimeTypes(mimeTypes.join(","))
		.setMode(google.picker.DocsViewMode.LIST);

	var picker = new google.picker.PickerBuilder()
		.enableFeature(google.picker.Feature.NAV_HIDDEN)
		.addView(view)
		.setOAuthToken(token.access_token)
		.setDeveloperKey(this.apiKey)
		.setCallback(function(data) {
			switch (data[google.picker.Response.ACTION]) {
				case google.picker.Action.PICKED:
			 		var doc = data[google.picker.Response.DOCUMENTS][0];
			 		promise.fulfill(doc.id);
				break;

				case google.picker.Action.CANCEL:
					promise.fulfill(null);
				break;
			}
		})
		.build();
	picker.setVisible(true);

	return promise;
}

MM.Backend.GDrive._connect = function() {
	if (window.gapi && window.gapi.auth.getToken()) {
		return new Promise().fulfill();
	} else {
		return this._loadGapi().then(this._auth.bind(this));
	}
}

MM.Backend.GDrive._loadGapi = function() {
	var promise = new Promise();
	if (window.gapi) { return promise.fulfill(); }
	
	var script = document.createElement("script");
	var name = ("cb"+Math.random()).replace(".", "");
	window[name] = promise.fulfill.bind(promise);
	script.src = "https://apis.google.com/js/client:picker.js?onload=" + name;
	document.body.appendChild(script);

	return promise;
}

MM.Backend.GDrive._auth = function(forceUI) {
	var promise = new Promise();

	gapi.auth.authorize({
		"client_id": this.clientId,
		"scope": this.scope,
		"immediate": !forceUI
	}, function(token) {
		if (token && !token.error) { /* done */
			promise.fulfill();
		} else if (!forceUI) { /* try again with ui */
			this._auth(true).then(
				promise.fulfill.bind(promise),
				promise.reject.bind(promise)
			);
		} else { /* bad luck */
			promise.reject(token && token.error || new Error("Failed to authorize with Google"));
		}
	}.bind(this));

	return promise;
}
MM.UI = function() {
	this._node = document.querySelector(".ui");
	
	this._toggle = this._node.querySelector("#toggle");

	this._layout = new MM.UI.Layout();
	this._shape = new MM.UI.Shape();
	this._icon = new MM.UI.Icon();
	this._color = new MM.UI.Color();
	this._value = new MM.UI.Value();
	this._status = new MM.UI.Status();
		
	MM.subscribe("item-select", this);
	MM.subscribe("item-change", this);

	this._node.addEventListener("click", this);
	this._node.addEventListener("change", this);

	this.toggle();
}

MM.UI.prototype.handleMessage = function(message, publisher) {
	switch (message) {
		case "item-select":
			this._update();
		break;

		case "item-change":
			if (publisher == MM.App.current) { this._update(); }
		break;
	}
}

MM.UI.prototype.handleEvent = function(e) {
	switch (e.type) {
		case "click":
			if (e.target.nodeName.toLowerCase() != "select") { MM.Clipboard.focus(); } /* focus the clipboard (2c) */

			if (e.target == this._toggle) {
				this.toggle();
				return;
			}
			
			var node = e.target;
			while (node != document) {
				var command = node.getAttribute("data-command");
				if (command) {
					MM.Command[command].execute();
					return;
				}
				node = node.parentNode;
			}
		break;

		case "change":
			MM.Clipboard.focus(); /* focus the clipboard (2c) */
		break;
	}
}

MM.UI.prototype.toggle = function() {
	this._node.classList.toggle("visible");
	MM.publish("ui-change", this);
}


MM.UI.prototype.getWidth = function() {
	return (this._node.classList.contains("visible") ? this._node.offsetWidth : 0);
}

MM.UI.prototype._update = function() {
	this._layout.update();
	this._shape.update();
	this._icon.update();
	this._value.update();
	this._status.update();
}
MM.UI.Layout = function() {
	this._select = document.querySelector("#layout");

	this._select.appendChild(MM.Layout.Map.buildOption());

	var label = this._buildGroup("Graph");
	label.appendChild(MM.Layout.Graph.Right.buildOption());
	label.appendChild(MM.Layout.Graph.Left.buildOption());
	label.appendChild(MM.Layout.Graph.Down.buildOption());
	label.appendChild(MM.Layout.Graph.Up.buildOption());

	var label = this._buildGroup("Tree");
	label.appendChild(MM.Layout.Tree.Right.buildOption());
	label.appendChild(MM.Layout.Tree.Left.buildOption());
	
	this._select.addEventListener("change", this);
}

MM.UI.Layout.prototype.update = function() {
	var value = "";
	var layout = MM.App.current.getOwnLayout();
	if (layout) { value = layout.id; }
	this._select.value = value;
	
	this._getOption("").disabled = MM.App.current.isRoot();
	this._getOption(MM.Layout.Map.id).disabled = !MM.App.current.isRoot();
}

MM.UI.Layout.prototype.handleEvent = function(e) {
	var layout = MM.Layout.getById(this._select.value);

	var action = new MM.Action.SetLayout(MM.App.current, layout);
	MM.App.action(action);
}

MM.UI.Layout.prototype._getOption = function(value) {
	return this._select.querySelector("option[value='" + value + "']");
}

MM.UI.Layout.prototype._buildGroup = function(label) {
	var node = document.createElement("optgroup");
	node.label = label;
	this._select.appendChild(node);
	return node;
}
MM.UI.Shape = function() {
	this._select = document.querySelector("#shape");
	
	this._select.appendChild(MM.Shape.Box.buildOption());
	this._select.appendChild(MM.Shape.Ellipse.buildOption());
	this._select.appendChild(MM.Shape.Underline.buildOption());
	
	this._select.addEventListener("change", this);
}

MM.UI.Shape.prototype.update = function() {
	var value = "";
	var shape = MM.App.current.getOwnShape();
	if (shape) { value = shape.id; }

	this._select.value = value;
}

MM.UI.Shape.prototype.handleEvent = function(e) {
	var shape = MM.Shape.getById(this._select.value);

	var action = new MM.Action.SetShape(MM.App.current, shape);
	MM.App.action(action);
}
MM.UI.Value = function() {
	this._select = document.querySelector("#value");
	this._select.addEventListener("change", this);
}

MM.UI.Value.prototype.update = function() {
	var value = MM.App.current.getValue();
	if (value === null) { value = ""; }
	if (typeof(value) == "number") { value = "num" }

	this._select.value = value;
}

MM.UI.Value.prototype.handleEvent = function(e) {
	var value = this._select.value;
	if (value == "num") {
		MM.Command.Value.execute();
	} else {
		var action = new MM.Action.SetValue(MM.App.current, value || null);
		MM.App.action(action);
	}
}
MM.UI.Status = function() {
	this._select = document.querySelector("#status");
	this._select.addEventListener("change", this);
}

MM.UI.Status.prototype.update = function() {
	this._select.value = MM.App.current.getStatus() || "";
}

MM.UI.Status.prototype.handleEvent = function(e) {
	var action = new MM.Action.SetStatus(MM.App.current, this._select.value || null);
	MM.App.action(action);
}
MM.UI.Color = function() {
	this._node = document.querySelector("#color");
	this._node.addEventListener("click", this);

	var items = this._node.querySelectorAll("[data-color]");
	
	for (var i=0;i<items.length;i++) {
		var item = items[i];
		item.style.backgroundColor = item.getAttribute("data-color");
	}
}

MM.UI.Color.prototype.handleEvent = function(e) {
	e.preventDefault();
	if (!e.target.hasAttribute("data-color")) { return; }
	
	var color = e.target.getAttribute("data-color") || null;
	var action = new MM.Action.SetColor(MM.App.current, color);
	MM.App.action(action);
}
MM.UI.Icon = function() {
    this._select = document.querySelector("#icons");
    this._select.addEventListener("change", this);
}

MM.UI.Icon.prototype.update = function() {
    this._select.value = MM.App.current.getIcon() || "";
}

MM.UI.Icon.prototype.handleEvent = function(e) {
    var action = new MM.Action.SetIcon(MM.App.current, this._select.value || null);
    MM.App.action(action);
}
MM.UI.Help = function() {
	this._node = document.querySelector("#help");
	this._map = {
		8: "Backspace",
		9: "Tab",
		13: "↩",
		32: "Spacebar",
		33: "PgUp",
		34: "PgDown",
		35: "End",
		36: "Home",
		37: "←",
		38: "↑",
		39: "→",
		40: "↓",
		45: "Insert",
		46: "Delete",
		65: "A",
		68: "D",
		83: "S",
		87: "W",
		112: "F1",
		113: "F2",
		114: "F3",
		115: "F4",
		116: "F5",
		117: "F6",
		118: "F7",
		119: "F8",
		120: "F9",
		121: "F10",
		"-": "&minus;"
	};

	this._build();
}

MM.UI.Help.prototype.toggle = function() {
	this._node.classList.toggle("visible");
}

MM.UI.Help.prototype._build = function() {
	var t = this._node.querySelector(".navigation");
	this._buildRow(t, "Pan");
	this._buildRow(t, "Select");
	this._buildRow(t, "SelectRoot");
	this._buildRow(t, "SelectParent");
	this._buildRow(t, "Center");
	this._buildRow(t, "ZoomIn", "ZoomOut");
	this._buildRow(t, "Fold");

	var t = this._node.querySelector(".manipulation");
	this._buildRow(t, "InsertSibling");
	this._buildRow(t, "InsertChild");
	this._buildRow(t, "Swap");
	this._buildRow(t, "Side");
	this._buildRow(t, "Delete");

	this._buildRow(t, "Copy");
	this._buildRow(t, "Cut");
	this._buildRow(t, "Paste");

	var t = this._node.querySelector(".editing");
	this._buildRow(t, "Value");
	this._buildRow(t, "Yes", "No", "Computed");
	this._buildRow(t, "Edit");
	this._buildRow(t, "Newline");
	this._buildRow(t, "Bold");
	this._buildRow(t, "Italic");
	this._buildRow(t, "Underline");
	this._buildRow(t, "Strikethrough");

	var t = this._node.querySelector(".other");
	this._buildRow(t, "Undo", "Redo");
	this._buildRow(t, "Save");
	this._buildRow(t, "SaveAs");
	this._buildRow(t, "Load");
	this._buildRow(t, "Help");
	this._buildRow(t, "Notes");
	this._buildRow(t, "UI");
}

MM.UI.Help.prototype._buildRow = function(table, commandName) {
	var row = table.insertRow(-1);

	var labels = [];
	var keys = [];

	for (var i=1;i<arguments.length;i++) {
		var command = MM.Command[arguments[i]];
		if (!command) { continue; }
		labels.push(command.label);
		keys = keys.concat(command.keys.map(this._formatKey, this));
	}

	row.insertCell(-1).innerHTML = labels.join("/");
	row.insertCell(-1).innerHTML = keys.join("/");

}

MM.UI.Help.prototype._formatKey = function(key) {
	var str = "";
	if (key.ctrlKey) { str += "Ctrl+"; }
	if (key.altKey) { str += "Alt+"; }
	if (key.shiftKey) { str += "Shift+"; }
	if (key.charCode) {
		var ch = String.fromCharCode(key.charCode);
		str += this._map[ch] || ch.toUpperCase();
	}
	if (key.keyCode) { str += this._map[key.keyCode] || String.fromCharCode(key.keyCode); }
	return str;
}

MM.UI.Help.prototype.close = function() {
	if (this._node.classList.contains("visible")) {
		this._node.classList.toggle("visible");
	}
}
MM.UI.Notes = function() {
	this._node = document.querySelector("#notes");
}

MM.UI.Notes.prototype.toggle = function() {
	this._node.classList.toggle("visible");
}

MM.UI.Notes.prototype.close = function() {
	if (this._node.classList.contains("visible")) {
		this._node.classList.toggle("visible");
		MM.Clipboard.focus();
	}
}

MM.UI.Notes.prototype.update = function(html) {
	if (html.trim().length === 0) {
		MM.App.current._notes = null;
	} else {
		MM.App.current._notes = html;
	}
	MM.App.current.update();
}
MM.UI.IO = function() {
	this._prefix = "mm.app.";
	this._mode = "";
	this._node = document.querySelector("#io");
	this._heading = this._node.querySelector("h3");

	this._backend = this._node.querySelector("#backend");
	this._currentBackend = null;
	this._backends = {};
	var ids = ["local", "firebase", "gdrive", "file", "webdav", "image"];
	ids.forEach(function(id) {
		var ui = MM.UI.Backend.getById(id);
		ui.init(this._backend);
		this._backends[id] = ui;
	}, this);

	this._backend.value = localStorage.getItem(this._prefix + "backend") || MM.Backend.File.id;
	this._backend.addEventListener("change", this);
	
	MM.subscribe("map-new", this);
	MM.subscribe("save-done", this);
	MM.subscribe("load-done", this);
}

MM.UI.IO.prototype.restore = function() {
	var parts = {};
	location.search.substring(1).split("&").forEach(function(item) {
		var keyvalue = item.split("=");
		parts[decodeURIComponent(keyvalue[0])] = decodeURIComponent(keyvalue[1]);
	});
	
	/* backwards compatibility */
	if ("map" in parts) { parts.url = parts.map; }

	/* just URL means webdav backend */
	if ("url" in parts && !("b" in parts)) { parts.b = "webdav"; }

	var backend = MM.UI.Backend.getById(parts.b);
	if (backend) { /* saved backend info */
		backend.setState(parts); 
		return;
	}

	if (parts.state) { /* opened from gdrive */
		try {
			var state = JSON.parse(parts.state);
			if (state.action == "open") {
				state = {
					b: "gdrive",
					id: state.ids[0]
				};
				MM.UI.Backend.GDrive.setState(state);
			} else {
				history.replaceState(null, "", ".");
			}
			return;
		} catch (e) { }
	}
}

MM.UI.IO.prototype.handleMessage = function(message, publisher) {
	switch (message) {
		case "map-new":
			this._setCurrentBackend(null);
		break;
		
		case "save-done":
		case "load-done":
			this.hide();
			this._setCurrentBackend(publisher);
		break;
	}
}

MM.UI.IO.prototype.show = function(mode) {
	this._mode = mode;
	this._node.classList.add("visible");
	this._heading.innerHTML = mode;
	
	this._syncBackend();
	window.addEventListener("keydown", this);
}

MM.UI.IO.prototype.hide = function() {
	if (!this._node.classList.contains("visible")) { return; }
	this._node.classList.remove("visible");
	MM.Clipboard.focus();
	window.removeEventListener("keydown", this);
}

MM.UI.IO.prototype.quickSave = function() {
	if (this._currentBackend) { 
		this._currentBackend.save();
	} else {
		this.show("save");
	}
}

MM.UI.IO.prototype.handleEvent = function(e) {
	switch (e.type) {
		case "keydown":
			if (e.keyCode == 27) { this.hide(); }
		break;
		
		case "change":
			this._syncBackend();
		break;
	}
}

MM.UI.IO.prototype._syncBackend = function() {
	var all = this._node.querySelectorAll("div[id]");
	[].slice.apply(all).forEach(function(node) { node.style.display = "none"; });
	
	this._node.querySelector("#" + this._backend.value).style.display = "";
	
	this._backends[this._backend.value].show(this._mode);
}

/**
 * @param {MM.UI.Backend} backend
 */
MM.UI.IO.prototype._setCurrentBackend = function(backend) {
	if (this._currentBackend && this._currentBackend != backend) { this._currentBackend.reset(); }
	
	if (backend) { localStorage.setItem(this._prefix + "backend", backend.id); }
	this._currentBackend = backend;
	try {
		this._updateURL(); /* fails when on file:/// */
	} catch (e) {}
}

MM.UI.IO.prototype._updateURL = function() {
	var data = this._currentBackend && this._currentBackend.getState();
	if (!data) {
		history.replaceState(null, "", ".");
	} else {
		var arr = Object.keys(data).map(function(key) {
			return encodeURIComponent(key)+"="+encodeURIComponent(data[key]);
		});
		history.replaceState(null, "", "?" + arr.join("&"));
	}
}
MM.UI.Backend = Object.create(MM.Repo);

MM.UI.Backend.init = function(select) {
	this._backend = MM.Backend.getById(this.id);
	this._mode = "";
	this._prefix = "mm.app." + this.id + ".";

	this._node = document.querySelector("#" + this.id);
	
	this._cancel = this._node.querySelector(".cancel");
	this._cancel.addEventListener("click", this);

	this._go = this._node.querySelector(".go");
	this._go.addEventListener("click", this);
	
	select.appendChild(this._backend.buildOption());
}

MM.UI.Backend.reset = function() {
	this._backend.reset();
}

MM.UI.Backend.setState = function(data) {
}

MM.UI.Backend.getState = function() {
	return null;
}

MM.UI.Backend.handleEvent = function(e) {
	switch (e.target) {
		case this._cancel:
			MM.App.io.hide();
		break;

		case this._go:
			this._action();
		break;
	}
}

MM.UI.Backend.save = function() {
}

MM.UI.Backend.load = function() {
}

MM.UI.Backend.show = function(mode) {
	this._mode = mode;

	this._go.innerHTML = mode.charAt(0).toUpperCase() + mode.substring(1);

	var all = this._node.querySelectorAll("[data-for]");
	[].concat.apply([], all).forEach(function(node) { node.style.display = "none"; });

	var visible = this._node.querySelectorAll("[data-for~=" + mode + "]");
	[].concat.apply([], visible).forEach(function(node) { node.style.display = ""; });

	/* switch to 2a: steal focus from the current item */
	this._go.focus();
}

MM.UI.Backend._action = function() {
	switch (this._mode) {
		case "save":
			this.save();
		break;
		
		case "load":
			this.load();
		break;
	}
}

MM.UI.Backend._saveDone = function() {
	MM.App.setThrobber(false);
	MM.publish("save-done", this);
}

MM.UI.Backend._loadDone = function(json) {
	MM.App.setThrobber(false);
	try {
		MM.App.setMap(MM.Map.fromJSON(json));
		MM.publish("load-done", this);
	} catch (e) { 
		this._error(e);
	}
}

MM.UI.Backend._error = function(e) {
	MM.App.setThrobber(false);
	alert("IO error: " + e.message);
}

MM.UI.Backend._buildList = function(list, select) {
	var data = [];
	
	for (var id in list) {
		data.push({id:id, name:list[id]});
	}
	
	data.sort(function(a, b) {
		return a.name.localeCompare(b.name);
	});
	
	data.forEach(function(item) {
		var o = document.createElement("option");
		o.value = item.id;
		o.innerHTML = item.name;
		select.appendChild(o);
	});
}
MM.UI.Backend.File = Object.create(MM.UI.Backend, {
	id: {value: "file"}
});

MM.UI.Backend.File.init = function(select) {
	MM.UI.Backend.init.call(this, select);

	this._format = this._node.querySelector(".format");
	this._format.appendChild(MM.Format.JSON.buildOption());
	this._format.appendChild(MM.Format.FreeMind.buildOption());
	this._format.appendChild(MM.Format.MMA.buildOption());
	this._format.appendChild(MM.Format.Mup.buildOption());
	this._format.appendChild(MM.Format.Plaintext.buildOption());
	this._format.value = localStorage.getItem(this._prefix + "format") || MM.Format.JSON.id;
}

MM.UI.Backend.File.show = function(mode) {
	MM.UI.Backend.show.call(this, mode);
	
	this._go.innerHTML = (mode == "save" ? "Save" : "Browse");
}

MM.UI.Backend.File._action = function() {
	localStorage.setItem(this._prefix + "format", this._format.value);
	
	MM.UI.Backend._action.call(this);
}

MM.UI.Backend.File.save = function() {
	var format = MM.Format.getById(this._format.value);
	var json = MM.App.map.toJSON();
	var data = format.to(json);

	var name = MM.App.map.getName() + "." + format.extension;
	this._backend.save(data, name).then(
		this._saveDone.bind(this),
		this._error.bind(this)
	);
}

MM.UI.Backend.File.load = function() {
	this._backend.load().then(
		this._loadDone.bind(this),
		this._error.bind(this)
	);
}

MM.UI.Backend.File._loadDone = function(data) {
	try {
		var format = MM.Format.getByName(data.name) || MM.Format.JSON;
		var json = format.from(data.data);
	} catch (e) { 
		this._error(e);
	}

	MM.UI.Backend._loadDone.call(this, json);
}
MM.UI.Backend.WebDAV = Object.create(MM.UI.Backend, {
	id: {value: "webdav"}
});

MM.UI.Backend.WebDAV.init = function(select) {
	MM.UI.Backend.init.call(this, select);

	this._url = this._node.querySelector(".url");
	this._url.value = localStorage.getItem(this._prefix + "url") || "";
	
	this._current = "";
}

MM.UI.Backend.WebDAV.getState = function() {
	var data = {
		url: this._current
	};
	return data;
}

MM.UI.Backend.WebDAV.setState = function(data) {
	this._load(data.url);
}

MM.UI.Backend.WebDAV.save = function() {
	MM.App.setThrobber(true);

	var map = MM.App.map;
	var url = this._url.value;
	localStorage.setItem(this._prefix + "url", url);

	if (url.match(/\.mymind$/)) { /* complete file name */
	} else { /* just a path */
		if (url.charAt(url.length-1) != "/") { url += "/"; }
		url += map.getName() + "." + MM.Format.JSON.extension;
	}

	this._current = url;
	var json = map.toJSON();
	var data = MM.Format.JSON.to(json);

	this._backend.save(data, url).then(
		this._saveDone.bind(this),
		this._error.bind(this)
	);
}

MM.UI.Backend.WebDAV.load = function() {
	this._load(this._url.value);
}

MM.UI.Backend.WebDAV._load = function(url) {
	this._current = url;
	MM.App.setThrobber(true);

	var lastIndex = url.lastIndexOf("/");
	this._url.value = url.substring(0, lastIndex);
	localStorage.setItem(this._prefix + "url", this._url.value);

	this._backend.load(url).then(
		this._loadDone.bind(this),
		this._error.bind(this)
	);
}

MM.UI.Backend.WebDAV._loadDone = function(data) {
	try {
		var json = MM.Format.JSON.from(data);
	} catch (e) { 
		this._error(e);
	}

	MM.UI.Backend._loadDone.call(this, json);
}
MM.UI.Backend.Image = Object.create(MM.UI.Backend, {
	id: {value: "image"}
});

MM.UI.Backend.Image.save = function() {
	var name = MM.App.map.getName();
	var json = MM.App.map.toJSON();
	var data = MM.Format.JSON.to(json);

	this._backend.save(data, name);
}

MM.UI.Backend.Image.load = null;
MM.UI.Backend.Local = Object.create(MM.UI.Backend, {
	id: {value: "local"}
});

MM.UI.Backend.Local.init = function(select) {
	MM.UI.Backend.init.call(this, select);
	
	this._list = this._node.querySelector(".list");
	this._remove = this._node.querySelector(".remove");
	this._remove.addEventListener("click", this);
}

MM.UI.Backend.Local.handleEvent = function(e) {
	MM.UI.Backend.handleEvent.call(this, e);

	switch (e.target) {
		case this._remove:
			var id = this._list.value;
			if (!id) { break; } 
			this._backend.remove(id);
			this.show(this._mode);
		break;
	}
}

MM.UI.Backend.Local.show = function(mode) {
	MM.UI.Backend.show.call(this, mode);
	
	this._go.disabled = false;

	if (mode == "load") { 
		var list = this._backend.list();
		this._list.innerHTML = "";
		if (Object.keys(list).length) {
			this._go.disabled = false;
			this._remove.disabled = false;
			this._buildList(list, this._list);
		} else {
			this._go.disabled = true;
			this._remove.disabled = true;
			var o = document.createElement("option");
			o.innerHTML = "(no maps saved)";
			this._list.appendChild(o);
		}
	}
}

MM.UI.Backend.Local.setState = function(data) {
	this._load(data.id);
}

MM.UI.Backend.Local.getState = function() {
	var data = {
		b: this.id,
		id: MM.App.map.getId()
	};
	return data;
}

MM.UI.Backend.Local.save = function() {
	var json = MM.App.map.toJSON();
	var data = MM.Format.JSON.to(json);

	try {
		this._backend.save(data, MM.App.map.getId(), MM.App.map.getName());
		this._saveDone();
	} catch (e) {
		this._error(e);
	}
}

MM.UI.Backend.Local.load = function() {
	this._load(this._list.value);
}

MM.UI.Backend.Local._load = function(id) {
	try {
		var data = this._backend.load(id);
		var json = MM.Format.JSON.from(data);
		this._loadDone(json);
	} catch (e) {
		this._error(e);
	}
}
MM.UI.Backend.Firebase = Object.create(MM.UI.Backend, {
	id: {value: "firebase"}
});

MM.UI.Backend.Firebase.init = function(select) {
	MM.UI.Backend.init.call(this, select);
	
	this._online = false;
	this._itemChangeTimeout = null;
	this._list = this._node.querySelector(".list");
	this._server = this._node.querySelector(".server");
	this._server.value = localStorage.getItem(this._prefix + "server") || "my-mind";

	this._auth = this._node.querySelector(".auth");
	this._auth.value = localStorage.getItem(this._prefix + "auth") || "";

	this._remove = this._node.querySelector(".remove");
	this._remove.addEventListener("click", this);

	this._go.disabled = false;
	MM.subscribe("firebase-list", this);
	MM.subscribe("firebase-change", this);
}

MM.UI.Backend.Firebase.setState = function(data) {
	this._connect(data.s, data.a).then(
		this._load.bind(this, data.id),
		this._error.bind(this)
	);
}

MM.UI.Backend.Firebase.getState = function() {
	var data = {
		id: MM.App.map.getId(),
		b: this.id,
		s: this._server.value
	};
	if (this._auth.value) { data.a = this._auth.value; }
	return data;
}

MM.UI.Backend.Firebase.show = function(mode) {
	MM.UI.Backend.show.call(this, mode);
	this._sync();
}

MM.UI.Backend.Firebase.handleEvent = function(e) {
	MM.UI.Backend.handleEvent.call(this, e);

	switch (e.target) {
		case this._remove:
			var id = this._list.value;
			if (!id) { break; }
			MM.App.setThrobber(true);
			this._backend.remove(id).then(
				function() { MM.App.setThrobber(false); },
				this._error.bind(this)
			);
		break;
	}
}

MM.UI.Backend.Firebase.handleMessage = function(message, publisher, data) {
	switch (message) {
		case "firebase-list":
			this._list.innerHTML = "";
			if (Object.keys(data).length) {
				this._buildList(data, this._list);
			} else {
				var o = document.createElement("option");
				o.innerHTML = "(no maps saved)";
				this._list.appendChild(o);
			}
			this._sync();
		break;

		case "firebase-change":
			if (data) {
				MM.unsubscribe("item-change", this);
				MM.App.map.mergeWith(data);
				MM.subscribe("item-change", this);
			} else { /* FIXME */
				console.log("remote data disappeared");
			}
		break;

		case "item-change":
			if (this._itemChangeTimeout) { clearTimeout(this._itemChangeTimeout); }
			this._itemChangeTimeout = setTimeout(this._itemChange.bind(this), 200);
		break;
	}
}

MM.UI.Backend.Firebase.reset = function() {
	this._backend.reset();
	MM.unsubscribe("item-change", this);
}

MM.UI.Backend.Firebase._itemChange = function() {
	var map = MM.App.map;
	this._backend.mergeWith(map.toJSON(), map.getName());
}

MM.UI.Backend.Firebase._action = function() {
	if (!this._online) {
		this._connect(this._server.value, this._auth.value);
		return;
	}
	
	MM.UI.Backend._action.call(this);
}

MM.UI.Backend.Firebase.save = function() {
	MM.App.setThrobber(true);

	var map = MM.App.map;
	this._backend.save(map.toJSON(), map.getId(), map.getName()).then(
		this._saveDone.bind(this),
		this._error.bind(this)
	);
}

MM.UI.Backend.Firebase.load = function() {
	this._load(this._list.value);
}

MM.UI.Backend.Firebase._load = function(id) {
	MM.App.setThrobber(true);
	/* FIXME posere se kdyz zmenim jeden firebase na jiny, mozna */
	this._backend.load(id).then(
		this._loadDone.bind(this),
		this._error.bind(this)
	);
}

MM.UI.Backend.Firebase._connect = function(server, auth) {
	var promise = new Promise();

	this._server.value = server;
	this._auth.value = auth;
	this._server.disabled = true;
	this._auth.disabled = true;

	localStorage.setItem(this._prefix + "server", server);
	localStorage.setItem(this._prefix + "auth", auth || "");

	this._go.disabled = true;
	MM.App.setThrobber(true);

	this._backend.connect(server, auth).then(
		function() {
			this._connected();
			promise.fulfill();
		}.bind(this),
		promise.reject.bind(promise)
	);

	return promise;
}

MM.UI.Backend.Firebase._connected = function() {
	MM.App.setThrobber(false);
	this._online = true;
	this._sync();
}

MM.UI.Backend.Firebase._sync = function() {
	if (!this._online) {
		this._go.innerHTML = "Connect";
		return;
	}

	this._go.disabled = false;
	if (this._mode == "load" && !this._list.value) { this._go.disabled = true; }
	this._go.innerHTML = this._mode.charAt(0).toUpperCase() + this._mode.substring(1);
}

MM.UI.Backend.Firebase._loadDone = function() {
	MM.subscribe("item-change", this);
	MM.UI.Backend._loadDone.apply(this, arguments);
}

MM.UI.Backend.Firebase._saveDone = function() {
	MM.subscribe("item-change", this);
	MM.UI.Backend._saveDone.apply(this, arguments);
}
MM.UI.Backend.GDrive = Object.create(MM.UI.Backend, {
	id: {value: "gdrive"}
});

MM.UI.Backend.GDrive.init = function(select) {
	MM.UI.Backend.init.call(this, select);

	this._format = this._node.querySelector(".format");
	this._format.appendChild(MM.Format.JSON.buildOption());
	this._format.appendChild(MM.Format.FreeMind.buildOption());
	this._format.appendChild(MM.Format.MMA.buildOption());
	this._format.appendChild(MM.Format.Mup.buildOption());
	this._format.appendChild(MM.Format.Plaintext.buildOption());
	this._format.value = localStorage.getItem(this._prefix + "format") || MM.Format.JSON.id;
}

MM.UI.Backend.GDrive.save = function() {
	MM.App.setThrobber(true);

	var format = MM.Format.getById(this._format.value);
	var json = MM.App.map.toJSON();
	var data = format.to(json);
	var name = MM.App.map.getName();
	var mime = "text/plain";
	
	if (format.mime) {
		mime = format.mime;
	} else {
		name += "." + format.extension;
	}

	this._backend.save(data, name, mime).then(
		this._saveDone.bind(this),
		this._error.bind(this)
	);
}

MM.UI.Backend.GDrive.load = function() {
	MM.App.setThrobber(true);

	this._backend.pick().then(
		this._picked.bind(this),
		this._error.bind(this)
	);
}

MM.UI.Backend.GDrive._picked = function(id) {
	MM.App.setThrobber(false);
	if (!id) { return;  }

	MM.App.setThrobber(true);

	this._backend.load(id).then(
		this._loadDone.bind(this),
		this._error.bind(this)
	)
}

MM.UI.Backend.GDrive.setState = function(data) {
	this._picked(data.id);
}

MM.UI.Backend.GDrive.getState = function() {
	var data = {
		b: this.id,
		id: this._backend.fileId
	};
	return data;
}

MM.UI.Backend.GDrive._loadDone = function(data) {
	try {
		var format = MM.Format.getByMime(data.mime) || MM.Format.getByName(data.name) || MM.Format.JSON;
		var json = format.from(data.data);
	} catch (e) { 
		this._error(e);
	}

	MM.UI.Backend._loadDone.call(this, json);
}
MM.Mouse = {
	TOUCH_DELAY: 500,
	_port: null,
	_cursor: [0, 0],
	_pos: [0, 0], /* ghost pos */
	_mode: "",
	_item: null,
	_ghost: null,
	_oldDragState: null,
	_touchTimeout: null
}

MM.Mouse.init = function(port) {
	this._port = port;
	this._port.addEventListener("touchstart", this);
	this._port.addEventListener("mousedown", this);
	this._port.addEventListener("click", this);
	this._port.addEventListener("dblclick", this);
	this._port.addEventListener("wheel", this);
	this._port.addEventListener("mousewheel", this);
	this._port.addEventListener("contextmenu", this);
}

MM.Mouse.handleEvent = function(e) {
	switch (e.type) {
		case "click":
			var item = MM.App.map.getItemFor(e.target);
			if (MM.App.editing && item == MM.App.current) { return; } /* ignore on edited node */
			if (item) { MM.App.select(item); }
		break;

		case "dblclick":
			var item = MM.App.map.getItemFor(e.target);
			if (item) { MM.Command.Edit.execute(); }
		break;

		case "contextmenu":
			this._endDrag();
			e.preventDefault();

			var item = MM.App.map.getItemFor(e.target);
			item && MM.App.select(item);

			MM.Menu.open(e.clientX, e.clientY);
		break;

		case "touchstart":
			if (e.touches.length > 1) { return; }
			e.clientX = e.touches[0].clientX;
			e.clientY = e.touches[0].clientY;
		case "mousedown":
			var item = MM.App.map.getItemFor(e.target);
			if (MM.App.editing) {
				if (item == MM.App.current) { return; } /* ignore dnd on edited node */
				MM.Command.Finish.execute(); /* clicked elsewhere => finalize edit */
			}

			if (e.type == "mousedown") { e.preventDefault(); } /* to prevent blurring the clipboard node */

			if (e.type == "touchstart") { /* context menu here, after we have the item */
				this._touchTimeout = setTimeout(function() {
					item && MM.App.select(item);
					MM.Menu.open(e.clientX, e.clientY);
				}, this.TOUCH_DELAY);
			}

			this._startDrag(e, item);
		break;

		case "touchmove":
			if (e.touches.length > 1) { return; }
			e.clientX = e.touches[0].clientX;
			e.clientY = e.touches[0].clientY;
			clearTimeout(this._touchTimeout);
		case "mousemove":
			this._processDrag(e);
		break;

		case "touchend":
			clearTimeout(this._touchTimeout);
		case "mouseup":
			this._endDrag();
		break;

		case "wheel":
		case "mousewheel":
			var dir = 0;
			if (e.wheelDelta) {
				if (e.wheelDelta < 0) {
					dir = -1;
				} else if (e.wheelDelta > 0) {
					dir = 1;
				}
			}
			if (e.deltaY) {
				if (e.deltaY > 0) {
					dir = -1;
				} else if (e.deltaY < 0) {
					dir = 1;
				}
			}
			if (dir) {
				MM.App.adjustFontSize(dir);
			}
		break;
	}
}

MM.Mouse._startDrag = function(e, item) {

	if (e.type == "mousedown") {
		e.preventDefault(); /* no selections allowed. only for mouse; preventing touchstart would prevent Safari from emulating clicks */
		this._port.addEventListener("mousemove", this);
		this._port.addEventListener("mouseup", this);
	} else {
		this._port.addEventListener("touchmove", this);
		this._port.addEventListener("touchend", this);
	}

	this._cursor[0] = e.clientX;
	this._cursor[1] = e.clientY;

	if (item && !item.isRoot()) {
		this._mode = "drag";
		this._item = item;
	} else {
		this._mode = "pan";
		this._port.style.cursor = "move";
	}
}

MM.Mouse._processDrag = function(e) {
	e.preventDefault();
	var dx = e.clientX - this._cursor[0];
	var dy = e.clientY - this._cursor[1];
	this._cursor[0] = e.clientX;
	this._cursor[1] = e.clientY;

	switch (this._mode) {
		case "drag":
			if (!this._ghost) {
				this._port.style.cursor = "move";
				this._buildGhost(dx, dy);
			}
			this._moveGhost(dx, dy);
			var state = this._computeDragState();
			this._visualizeDragState(state);
		break;

		case "pan":
			MM.App.map.moveBy(dx, dy);
		break;
	}
}

MM.Mouse._endDrag = function() {
	this._port.style.cursor = "";
	this._port.removeEventListener("mousemove", this);
	this._port.removeEventListener("mouseup", this);

	if (this._mode == "pan") { return; } /* no cleanup after panning */

	if (this._ghost) {
		var state = this._computeDragState();
		this._finishDragDrop(state);

		this._ghost.parentNode.removeChild(this._ghost);
		this._ghost = null;
	}

	this._item = null;
}

MM.Mouse._buildGhost = function() {
	var content = this._item.getDOM().content;
	this._ghost = content.cloneNode(true);
	this._ghost.classList.add("ghost");
	this._pos[0] = content.offsetLeft;
	this._pos[1] = content.offsetTop;
	content.parentNode.appendChild(this._ghost);
}

MM.Mouse._moveGhost = function(dx, dy) {
	this._pos[0] += dx;
	this._pos[1] += dy;
	this._ghost.style.left = this._pos[0] + "px";
	this._ghost.style.top = this._pos[1] + "px";

	var state = this._computeDragState();
}

MM.Mouse._finishDragDrop = function(state) {
	this._visualizeDragState(null);

	var target = state.item;
	switch (state.result) {
		case "append":
			var action = new MM.Action.MoveItem(this._item, target);
		break;

		case "sibling":
			var index = target.getParent().getChildren().indexOf(target);
			var targetIndex = index + (state.direction == "right" || state.direction == "bottom" ? 1 : 0);
			var action = new MM.Action.MoveItem(this._item, target.getParent(), targetIndex, target.getSide());
		break;

		default:
			return;
		break;
	}

	MM.App.action(action);
}

/**
 * Compute a state object for a drag: current result (""/"append"/"sibling"), parent/sibling, direction
 */
MM.Mouse._computeDragState = function() {
	var rect = this._ghost.getBoundingClientRect();
	var closest = MM.App.map.getClosestItem(rect.left + rect.width/2, rect.top + rect.height/2);
	var target = closest.item;

	var state = {
		result: "",
		item: target,
		direction: ""
	}

	var tmp = target;
	while (!tmp.isRoot()) {
		if (tmp == this._item) { return state; } /* drop on a child or self */
		tmp = tmp.getParent();
	}

	var w1 = this._item.getDOM().content.offsetWidth;
	var w2 = target.getDOM().content.offsetWidth;
	var w = Math.max(w1, w2);
	var h1 = this._item.getDOM().content.offsetHeight;
	var h2 = target.getDOM().content.offsetHeight;
	var h = Math.max(h1, h2);

	if (target.isRoot()) { /* append here */
		state.result = "append";
	} else if (Math.abs(closest.dx) < w && Math.abs(closest.dy) < h) { /* append here */
		state.result = "append";
	} else {
		state.result = "sibling";
		var childDirection = target.getParent().getLayout().getChildDirection(target);
		var diff = -1 * (childDirection == "top" || childDirection == "bottom" ? closest.dx : closest.dy);

		if (childDirection == "left" || childDirection == "right") {
			state.direction = (closest.dy < 0 ? "bottom" : "top");
		} else {
			state.direction = (closest.dx < 0 ? "right" : "left");
		}
	}

	return state;
}

MM.Mouse._visualizeDragState = function(state) {
	if (this._oldState && state && this._oldState.item == state.item && this._oldState.result == state.result) { return; } /* nothing changed */

	if (this._oldDragState) { /* remove old vis */
		var item = this._oldDragState.item;
		var node = item.getDOM().content;
		node.style.boxShadow = "";
	}

	this._oldDragState = state;

	if (state) { /* show new vis */
		var item = state.item;
		var node = item.getDOM().content;

		var x = 0;
		var y = 0;
		var offset = 5;
		if (state.result == "sibling") {
			if (state.direction == "left") { x = -1; }
			if (state.direction == "right") { x = +1; }
			if (state.direction == "top") { y = -1; }
			if (state.direction == "bottom") { y = +1; }
		}
		var spread = (x || y ? -2 : 2);
		node.style.boxShadow = (x*offset) + "px " + (y*offset) + "px 2px " + spread + "px #000";
	}
}
/*
setInterval(function() {
	console.log(document.activeElement);
}, 1000);
*/

/*
 * Notes regarding app state/modes, activeElements, focusing etc.
 * ==============================================================
 * 
 * 1) There is always exactly one item selected. All executed commands 
 *    operate on this item.
 * 
 * 2) The app distinguishes three modes with respect to focus:
 *   2a) One of the UI panes has focus (inputs, buttons, selects). 
 *       Keyboard shortcuts are disabled.
 *   2b) Current item is being edited. It is contentEditable and focused. 
 *       Blurring ends the edit mode.
 *   2c) ELSE the Clipboard is focused (its invisible textarea)
 * 
 * In 2a, we try to lose focus as soon as possible
 * (after clicking, after changing select's value), switching to 2c.
 *
 * 3) Editing mode (2b) can be ended by multiple ways:
 *   3a) By calling current.stopEditing();
 *       this shall be followed by some resolution.
 *   3b) By executing MM.Command.{Finish,Cancel};
 *       these call 3a internally.
 *   3c) By blurring the item itself (by selecting another);
 *       this calls MM.Command.Finish (3b).
 *   3b) By blurring the currentElement;
 *       this calls MM.Command.Finish (3b).
 * 
 */
MM.App = {
	keyboard: null,
	current: null,
	editing: false,
	history: [],
	historyIndex: 0,
	portSize: [0, 0],
	map: null,
	ui: null,
	io: null,
	help: null,
	_port: null,
	_throbber: null,
	_drag: {
		pos: [0, 0],
		item: null,
		ghost: null
	},
	_fontSize: 100,
	
	action: function(action) {
		if (this.historyIndex < this.history.length) { /* remove undoed actions */
			this.history.splice(this.historyIndex, this.history.length-this.historyIndex);
		}
		
		this.history.push(action);
		this.historyIndex++;
		
		action.perform();
		return this;
	},
	
	setMap: function(map) {
		if (this.map) { this.map.hide(); }

		this.history = [];
		this.historyIndex = 0;

		this.map = map;
		this.map.show(this._port);
	},
	
	select: function(item) {
		if (this.current && this.current != item) { this.current.deselect(); }
		this.current = item;
		this.current.select();
	},

	adjustFontSize: function(diff) {
		this._fontSize = Math.max(30, this._fontSize + 10*diff);
		this._port.style.fontSize = this._fontSize + "%";
		this.map.update();
		this.map.ensureItemVisibility(this.current);
	},
	
	handleMessage: function(message, publisher) {
		switch (message) {
			case "ui-change":
				this._syncPort();
			break;

			case "item-change":
				if (publisher.isRoot() && publisher.getMap() == this.map) {
					document.title = this.map.getName() + " :: My Mind";
				}
			break;
		}
	},

	handleEvent: function(e) {
		switch (e.type) {
			case "resize":
				this._syncPort();
				break;

			case "keyup":
				if (e.key === "Escape") {
					MM.App.notes.close();
					MM.App.help.close();
				}
				break;

			case "message":
				if (e.data && e.data.action) {
					switch (e.data.action) {
						case "setContent":
							MM.App.notes.update(e.data.value);
							break;

						case "closeEditor":
							MM.App.notes.close();
							break;
					}
				}

				break;

			case "beforeunload":
				e.preventDefault();
				return "";
			break;
		}
	},
	
	setThrobber: function(visible) {
		this._throbber.classList[visible ? "add" : "remove"]("visible");
	},

	init: function() {
		this._port = document.querySelector("#port");
		this._throbber = document.querySelector("#throbber");
		this.ui = new MM.UI();
		this.io = new MM.UI.IO();
		this.help = new MM.UI.Help();
		this.notes = new MM.UI.Notes();

		MM.Tip.init();
		MM.Keyboard.init();
		MM.Menu.init(this._port);
		MM.Mouse.init(this._port);
		MM.Clipboard.init();

		window.addEventListener("resize", this);
		window.addEventListener("beforeunload", this);
		window.addEventListener("keyup", this);
		window.addEventListener("message", this, false);
		MM.subscribe("ui-change", this);
		MM.subscribe("item-change", this);
		
		this._syncPort();
		this.setMap(new MM.Map());
	},

	_syncPort: function() {
		this.portSize = [window.innerWidth - this.ui.getWidth(), window.innerHeight];
		this._port.style.width = this.portSize[0] + "px";
		this._port.style.height = this.portSize[1] + "px";
		this._throbber.style.right = (20 + this.ui.getWidth())+ "px";
		if (this.map) { this.map.ensureItemVisibility(this.current); }
	}
}
