// ==UserScript==
// @name        Image Extensions
// @description Expand images nicely
// @namespace   dnsev
// @version     3.0.4
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @run-at      document-start
// @icon        data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAr0lEQVRo3u2ZQQ6AIAwEW+Nj9UX623pVQ2NRDIIzZyHdMGkhqhwxSaNSh8t6Bmmc5gPo6Zi0kboNhQhAgE4CABQYZOlJsbj3kDqFzula6UK1GV1tpp1Bq2PaFLBsvzayp7O/iVpKJxT6lEIhnqgV0SlTMxRqT6FcVd7oTijUjUKrltGPLvQrhbzjLtVtMr9HIV5kvMgA/g0/OOhCBCAAAQjQ1XXabqx5bUhFakCh2mytCzMhi1UZlAAAAABJRU5ErkJggg==
// @include     http://boards.4chan.org/*
// @include     https://boards.4chan.org/*
// @include     http://boards.4channel.org/*
// @include     https://boards.4channel.org/*
// @include     http://i.4cdn.org/*
// @include     https://i.4cdn.org/*
// @updateURL   https://raw.githubusercontent.com/dnsev/iex/master/builds/iex.meta.js
// @downloadURL https://raw.githubusercontent.com/dnsev/iex/master/builds/iex.user.js
// ==/UserScript==



(function () {
	"use strict";



	// Timing function
	var timing = (function () {
		var perf, now;

		if (
			(perf = window.performance) &&
			(now = perf.now || perf.mozNow || perf.msNow || perf.oNow || perf.webkitNow)
		) {
			return function () {
				return now.call(perf);
			};
		}
		else {
			perf = null;
			now = null;
			return function () {
				return new Date().getTime();
			};
		}
	})();



	// Variables
	var doc = document,
		doc_el = doc.documentElement,
		user_agent = navigator.userAgent.toString(),
		is_firefox = (user_agent.indexOf("Firefox") >= 0),
		is_chrome = (user_agent.indexOf(" Chrome/") >= 0),
		is_opera = (!is_firefox && !is_chrome && user_agent.indexOf("MSIE") < 0),
		userscript = {"include":["http://boards.4chan.org/*","https://boards.4chan.org/*","http://boards.4channel.org/*","https://boards.4channel.org/*","http://i.4cdn.org/*","https://i.4cdn.org/*"],"name":"Image Extensions","grant":["GM_getValue","GM_setValue","GM_deleteValue","GM_listValues"],"run-at":"document-start","namespace":"dnsev","updateURL":"https://raw.githubusercontent.com/dnsev/iex/master/builds/iex.meta.js","downloadURL":"https://raw.githubusercontent.com/dnsev/iex/master/builds/iex.user.js","version":"3.0.4","icon":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAr0lEQVRo3u2ZQQ6AIAwEW+Nj9UX623pVQ2NRDIIzZyHdMGkhqhwxSaNSh8t6Bmmc5gPo6Zi0kboNhQhAgE4CABQYZOlJsbj3kDqFzula6UK1GV1tpp1Bq2PaFLBsvzayp7O/iVpKJxT6lEIhnqgV0SlTMxRqT6FcVd7oTijUjUKrltGPLvQrhbzjLtVtMr9HIV5kvMgA/g0/OOhCBCAAAQjQ1XXabqx5bUhFakCh2mytCzMhi1UZlAAAAABJRU5ErkJggg==","description":"Expand images nicely"},
		api = null,
		settings = null,
		sync = null,
		style = null,
		hotkey_manager = null,
		hover = null,
		image_hover = null,
		file_link = null,
		file_view = null;



	// Failure
	if (doc_el === null) {
		console.log("iex failed to start: document.documentElement = " + doc_el + ";");
		console.log(doc_el);
		return;
	}



	// Better binding
	Function.prototype.bind = function (self) {
		var fn = this;

		if (arguments.length > 1) {
			var slice = Array.prototype.slice,
				push = Array.prototype.push,
				args = slice.call(arguments, 1);

			return function () {
				var full_args = slice.call(args);
				push.apply(full_args, arguments);

				return fn.apply(self, full_args);
			};
		}
		else {
			return function () {
				return fn.apply(self, arguments);
			};
		}
	};

	// Helper functions
	var MutationObserver = (window.MutationObserver || window.WebKitMutationObserver);

	var wrap_generic_event = function (self, callback) {
		// Get any extra arguments
		var extra_args = Array.prototype.slice.call(arguments, 2);

		// Return the function wrapped
		return function (event) {
			// Setup arguments
			var new_args = [ event , this ],
				i = 0,
				im = extra_args.length;

			for (; i < im; ++i) new_args.push(extra_args[i]);

			// Run callback
			return callback.apply(self, new_args);
		};
	};
	var wrap_mouseenterleave_event = (function () {

		// Handle mouseover/mouseout events to make sure the target is correct
		var on_mouseenterleave_prehandle = function (event, callback, self, extra_args) {
			// Must check for same parent element
			var parent = event.relatedTarget;

			// Error handling
			try {
				// Find parents
				while (parent) {
					if (parent === this) return;
					parent = parent.parentNode;
				}

				// Setup event arguments
				var new_args = [ event , this ],
					i = 0,
					im = extra_args.length;

				for (; i < im; ++i) new_args.push(extra_args[i]);

				// Okay, trigger event
				return callback.apply(self, new_args);
			}
			catch (e) {}
		};



		// Return a wrapping function
		return function (self, callback) {
			// Get any extra arguments
			var args = Array.prototype.slice.call(arguments, 2);

			// Return the function wrapped
			return function (event) {
				return on_mouseenterleave_prehandle.call(this, event, callback, self, args);
			};
		};

	})();

	var add_event_listener = function (list, node, event, callback, capture) {
		node.addEventListener(event, callback, capture);
		list.push([node, event, callback, capture]);
	};
	var remove_event_listeners = function (list) {
		var list_len = list.length,
			i = 0,
			li;

		for (; i < list_len; ++i) {
			li = list[i];
			li[0].removeEventListener(li[1], li[2], li[3]);
		}
	};
	var stop_event = function (event) {
		event.preventDefault();
		event.stopPropagation();
		return false;
	};
	var get_event_mouse_button = function (event) {
		// 1=left, 2=middle, 3=right
		if (event.which) {
			if (event.which === 1) return 1;
			else if (event.which === 2) return 2;
			else if (event.which === 3) return 3;
		}
		else {
			if ((event.button & 1) !== 0) return 1;
			else if ((event.button & 2) !== 0) return 3;
			else if ((event.button & 4) !== 0) return 2;
		}
		return 0;
	};
	var select_input = function (node, caret_start, caret_end) {
		var len = (node.value || "").length;

		// Bound
		if (caret_start === undefined) {
			caret_start = 0;
			caret_end = len;
		}
		else if (caret_end === undefined) {
			if (caret_start < 0) caret_start = 0;
			else if (caret_start > len) caret_start = len;

			caret_end = caret_start;
		}
		else {
			if (caret_start < 0) caret_start = 0;
			else if (caret_start > len) caret_start = len;

			if (caret_end < caret_start) caret_end = caret_start;
			else if (caret_end > len) caret_end = len;
		}

		// Select
		if (node.createTextRange) {
			var range = node.createTextRange();
			range.move("character", caret_start);
			range.select();
		}
		else {
			if (node.selectionStart) {
				node.focus();
				node.setSelectionRange(caret_start, caret_end);
			}
			else {
				node.focus();
			}
		}
	};

	var $ = function (tag) {
		return doc.createElement(tag);
	};
	$.node = function (tag, class_name) {
		var n = doc.createElement(tag);
		n.className = class_name + style.theme;
		return n;
	};
	$.node_ns = function (ns, tag, class_name) {
		var n = doc.createElementNS(ns, tag);
		n.setAttribute("class", class_name + style.theme);
		return n;
	};
	$.text = function (text) {
		return doc.createTextNode(text);
	};
	$.a = function (class_name) {
		var n = doc.createElement("a");
		n.className = class_name + style.theme;
		return n;
	};
	$.div = function (class_name) {
		var n = doc.createElement("div");
		n.className = class_name + style.theme;
		return n;
	};
	$.span = function (class_name) {
		var n = doc.createElement("span");
		n.className = class_name + style.theme;
		return n;
	};
	$.label = function (class_name) {
		var n = doc.createElement("label");
		n.className = class_name + style.theme;
		return n;
	};
	$.input = function (class_name) {
		var n = doc.createElement("input");
		n.className = class_name + style.theme;
		return n;
	};
	$.input.check = function (class_name) {
		var n = doc.createElement("input");
		n.setAttribute("type", "checkbox");
		n.className = class_name + style.theme;
		return n;
	};
	$.input.text = function (class_name) {
		var n = doc.createElement("input");
		n.setAttribute("type", "text");
		n.className = class_name + style.theme;
		return n;
	};
	$.input.file = function (class_name) {
		var n = doc.createElement("input");
		n.setAttribute("type", "file");
		n.className = class_name + style.theme;
		return n;
	};
	$.input.button = function (class_name) {
		var n = doc.createElement("input");
		n.setAttribute("type", "button");
		n.className = class_name + style.theme;
		return n;
	};

	var crc32 = (function () {

		var crc_table = new Uint32Array([ //{
			0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
			0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
			0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
			0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,
			0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
			0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
			0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
			0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
			0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
			0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
			0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,
			0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
			0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,
			0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
			0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
			0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
			0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
			0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
			0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,
			0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
			0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
			0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
			0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,
			0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
			0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
			0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,
			0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
			0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
			0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,
			0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
			0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
			0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D,
		]); //}

		return function (data) {
			var crc = (0 ^ (-1)),
				data_len = data.length,
				ct = crc_table,
				i;

			for (i = 0; i < data_len; ++i) {
				crc = (crc >>> 8) ^ ct[(crc ^ data[i]) & 0xFF];
			}

			return (crc ^ (-1)) >>> 0;
		};

	})();



	// Function for performing actions as soon as possible
	var on_ready = (function () {

		// Vars
		var callbacks = [],
			check_interval = null,
			check_interval_time = 250;

		// Data to store callback and relevant data
		var Data = function (callback, condition, time_limit) {
			this.callback = callback;
			this.condition = condition;
			this.time_limit = time_limit;
		};
		Data.prototype.delay = function () {
			var limit = null,
				condition = this.condition,
				check;

			if (this.time_limit) {
				limit = setTimeout(function () {
					clearInterval(check);
				}, this.time_limit * 1000);
			}

			check = setInterval(function () {
				if (condition.call(null)) {
					clearInterval(check);
					if (limit !== null) clearTimeout(limit);
					this.callback.call(null);
				}
			}, 20);
		};

		// Check if ready and run callbacks
		var callback_check = function () {
			if (
				(document.readyState === "interactive" || document.readyState === "complete") &&
				callbacks !== null
			) {
				// Run callbacks
				var cbs = callbacks,
					cb_count = cbs.length,
					cb, i;

				// Clear
				callbacks = null;

				for (i = 0; i < cb_count; ++i) {
					cb = cbs[i];
					if (!cb.condition || cb.condition.call(null)) {
						cb.callback.call(null);
					}
					else {
						cb.delay();
					}
				}

				// Clear events and checking interval
				window.removeEventListener("load", callback_check, false);
				window.removeEventListener("readystatechange", callback_check, false);

				if (check_interval !== null) {
					clearInterval(check_interval);
					check_interval = null;
				}

				// Okay
				return true;
			}

			// Not executed
			return false;
		};

		// Listen
		window.addEventListener("load", callback_check, false);
		window.addEventListener("readystatechange", callback_check, false);

		// Callback adding function
		return function (callback, condition, time_limit) {
			if (callbacks === null) {
				// Ready to execute
				if (!condition || condition.call(null)) {
					callback.call(null);
				}
				else {
					// Delay
					new Data(callback, condition, time_limit).delay();
				}
			}
			else {
				// Delay
				callbacks.push(new Data(callback, condition, time_limit));

				// Set a check interval
				if (check_interval === null && callback_check() !== true) {
					check_interval = setInterval(callback_check, check_interval_time);
				}
			}
		};

	})();

	// Module to manage data saving with an asynchronous paradigm
	var SaveAsync = (function () {

		// Storage type
		var using_localstorage = true,
			chrome_storage = null;

		// Check for chrome storage
		try {
			chrome_storage = chrome.storage.local || null;
		}
		catch (e) {
			chrome_storage = null;
		}

		// Check for GM storage
		try {
			if (GM_setValue && GM_getValue && GM_deleteValue && GM_listValues) {
				using_localstorage = false;
			}
		}
		catch (e) {
			using_localstorage = true;
		}



		// HTML5 storage
		var decode_value = function (value) {
				if (value) {
					try {
						return JSON.parse(value);
					}
					catch (e) {}
				}
				return value;
			},
			object_byte_size = function (obj) {
				// Calculate byte length
				obj = JSON.stringify(obj);
				try {
					// Encode in utf-8
					return unescape(encodeURIComponent(obj)).length;
				}
				catch (e) {
					return obj.length;
				}
			},

			generic_get_value = function (key, callback) {
				var val = this.getItem(key, null);
				callback.call(null, decode_value(val));
			},
			generic_set_value = function (key, value, callback) {
				this.setItem(key, JSON.stringify(value));
				if (callback) callback.call(null);
			},
			generic_del_value = function (key, callback) {
				this.removeItem(key);
				if (callback) callback.call(null);
			},
			generic_get_keys = function (callback) {
				var keys = [],
					key;

				for (key in this) {
					keys.push(key);
				}

				callback.call(null, keys);
			},
			generic_get_space_used = function (callback) {
				var size = 0,
					key;

				// Create representation
				for (key in this) {
					size += object_byte_size(key) + object_byte_size(decode_value(this.getItem(key, null)));
				}

				// Return
				callback.call(null, size);
			},

			w_get_value = generic_get_value.bind(window.localStorage),
			w_set_value = generic_set_value.bind(window.localStorage),
			w_del_value = generic_del_value.bind(window.localStorage),
			w_get_keys = generic_get_keys.bind(window.localStorage),
			w_get_space_used = generic_get_space_used.bind(window.localStorage),

			s_get_value = generic_get_value.bind(window.sessionStorage),
			s_set_value = generic_set_value.bind(window.sessionStorage),
			s_del_value = generic_del_value.bind(window.sessionStorage),
			s_get_keys = generic_get_keys.bind(window.sessionStorage),
			s_get_space_used = generic_get_space_used.bind(window.sessionStorage),

			get_value, set_value, del_value, get_keys, get_space_used;



		// Userscript storage
		if (chrome_storage) {
			// Chrome storage
			var get_keys_callback = function (next_callback, obj) {
				var keys = [],
					key;

				for (key in obj) {
					keys.push(key);
				}

				next_callback.call(null, keys);
			};

			var on_get = function (key, callback, value) {
				callback.call(null, value[key]);
			};

			get_value = function (key, callback) {
				this.get(key, on_get.bind(null, key, callback));
			}.bind(chrome_storage);

			set_value = function (key, value, callback) {
				var obj = {};
				obj[key] = value;

				this.set(obj, callback);
			}.bind(chrome_storage);

			del_value = function (key, callback) {
				this.remove(key, callback);
			}.bind(chrome_storage);

			get_keys = function (callback) {
				this.get(null, get_keys_callback.bind(null, callback));
			}.bind(chrome_storage);

			get_space_used = function (callback) {
				this.getBytesInUse(null, callback);
			}.bind(chrome_storage);
		}
		else if (using_localstorage) {
			// Local storage
			get_value = w_get_value;
			set_value = w_set_value;
			del_value = w_del_value;
			get_keys = w_get_keys;
			get_space_used = w_get_space_used;
		}
		else {
			// GM storage
			get_value = function (key, callback) {
				var val = GM_getValue(key, null);
				callback.call(null, decode_value(val));
			};
			set_value = function (key, value, callback) {
				GM_setValue(key, JSON.stringify(value));
				if (callback) callback.call(null);
			};
			del_value = function (key, callback) {
				GM_deleteValue(key);
				if (callback) callback.call(null);
			};
			get_keys = function (callback) {
				var keys = GM_listValues();
				callback.call(null, keys);
			};
			get_space_used = function (callback) {
				var keys = GM_listValues(),
					size = 0,
					i;

				// Create representation
				for (i = 0; i < keys.length; ++i) {
					size += object_byte_size(keys[i]) + object_byte_size(decode_value(GM_getValue(keys[i], null)));
				}

				// Return
				callback.call(null, size);
			};
		}



		// Return function list
		return {

			set: set_value, // key, value, callback()
			get: get_value, // key, callback(value)
			del: del_value, // key, callback()
			keys: get_keys, // callback([...])
			space_used: get_space_used, // callback(bytes)

			w_set: w_set_value,
			w_get: w_get_value,
			w_del: w_del_value,
			w_keys: w_get_keys,
			w_space_used: w_get_space_used,

			s_set: s_set_value,
			s_get: s_get_value,
			s_del: s_del_value,
			s_keys: s_get_keys,
			s_space_used: s_get_space_used,

			mode: chrome_storage ? "chrome" : (using_localstorage ? "localstorage" : "gm"),

		};

	})();

	// Module to have "delayed" loops
	var Delay = (function () {

		var DelayEach = (function () {

			var DelayEach = function (i, array, callback, max_count, delay, remove) {
				this.i = i;
				this.array = array;
				this.callback = callback;
				this.max_count = max_count;
				this.delay = delay;
				this.timeout = null;
				this.timeout_fcn = remove ? run_loop_with_remove.bind(this) : run_loop.bind(this);

				// Run
				this.timeout_fcn.call(this);
			};



			var run_loop = function () {
				// Reset
				this.timeout = null;

				// Find max i
				var i = this.i;
				var array = this.array;
				var callback = this.callback;

				var i_max = i + this.max_count;
				var continue_loop = (array.length > i_max);
				if (!continue_loop) i_max = array.length;

				// Loop
				for (; i < i_max; ++i) {
					callback.call(this, array[i], i, this);
				}

				// Continue
				if (continue_loop) {
					this.timeout = setTimeout(this.timeout_fcn, this.delay);
				}
			};
			var run_loop_with_remove = function () {
				// Reset
				this.timeout = null;

				// Find max i
				var i = 0;
				var array = this.array;
				var callback = this.callback;

				var i_max = i + this.max_count;
				var continue_loop = (array.length > i_max);
				if (!continue_loop) i_max = array.length;

				// Loop
				for (; i < i_max; ++i) {
					callback.call(this, array[i], i, this);
				}
				this.array.splice(0, i);
				this.i += i;

				// Continue
				if (continue_loop) {
					this.timeout = setTimeout(this.timeout_fcn, this.delay);
				}
			};



			DelayEach.prototype = {
				constructor: DelayEach,

				pop: function (value) {
					for (var i = this.array.length - 1; i >= 0; --i) {
						if (this.array[i] === value) {
							this.array.splice(i, 1);
							return true;
						}
					}
					return false;
				},
				push: function (item) {
					this.array.push(item);
					if (this.timeout === null) this.timeout_fcn.call(this);
				},
				concat: function (other) {
					this.array = this.array.concat(other);
					if (this.timeout === null) this.timeout_fcn.call(this);
				},
			};



			return DelayEach;

		})();



		return {

			each: function (array, callback, max_count, delay) {
				// Start
				if (arguments.length < 3 || max_count <= 0) max_count = 1;
				if (arguments.length < 4 || delay <= 0) delay = 1;

				return new DelayEach(0, array, callback, max_count, delay * 1000, false);
			},
			queue: function (array, callback, max_count, delay) {
				// Start
				if (arguments.length < 3 || max_count <= 0) max_count = 1;
				if (arguments.length < 4 || delay <= 0) delay = 1;

				return new DelayEach(0, array, callback, max_count, delay * 1000, true);
			},

		};

	})();



	// Class to manage tab synchronization events
	var Sync = (function () {
		var Sync = function () {
			this.key_name = "iex_sync";

			this.listeners = {};

			window.addEventListener("storage", (this.on_storage_change_bind = on_storage_change.bind(this)), false);
		};

		var on_storage_change = function (event) {
			// Make sure it's an "addition" trigger
			if (event.key == this.key_name && event.newValue !== null) {
				var key = event.newValue, data = null;
				try {
					data = JSON.parse(key);
					key = data[0];
					data = data[1];
				}
				catch (e) {
					key = event.newValue;
					data = null;
				}
				if (key in this.listeners) {
					// Signal all
					var list = this.listeners[key];
					for (var i = 0; i < list.length; ++i) {
						// Signal
						list[i].call(this, key, data);
					}
				}
			}
		};

		Sync.instance = null;

		Sync.prototype = {
			constructor: Sync,

			/**
				Remove any bound events or leftover data used by an instance
			*/
			destroy: function () {
				// Remove events
				window.removeEventListener("storage", this.on_storage_change_bind, false);
			},

			/**
				Add a synchronization event listener

				@param key
					They sync key to listen for
				@param callback
					The function to be called
					Callback format is as follows:
						callback.call(Sync.instance, key, data)
			*/
			on: function (key, callback) {
				// Add to listener list
				if (key in this.listeners) {
					this.listeners[key].push(callback);
				}
				else {
					this.listeners[key] = [ callback ];
				}
			},
			/**
				Remove a synchronization event listener

				@param key
					They sync key used in the .on function
				@param callback
					The callback function used in the .on function
				@return
					true if it was removed successfully,
					false if it did not exist
			*/
			off: function (key, callback) {
				// Check if it exists
				if (key in this.listeners) {
					var list = this.listeners[key];
					for (var i = 0; i < list.length; ++i) {
						if (list[i] === callback) {
							// Remove from list
							list.splice(i, 1);
							// Remove key if empty
							if (list.length === 0) {
								delete this.listeners[key];
							}
							// Success
							return true;
						}
					}
				}

				// Not found
				return false;
			},
			/**
				Trigger a synchronization event

				@param key
					They sync key
				@return
					true if no errors occur,
					false otherwise
			*/
			trigger: function (key, data) {
				// Set the value
				try {
					// Trigger
					var value = JSON.stringify([ key , data ]);
					window.localStorage.setItem(this.key_name, value);
					window.localStorage.removeItem(this.key_name);
					// Okay
					return true;
				}
				catch (e) {
					// Failure
					return false;
				}
			},

		};

		return Sync;

	})();

	// 4chan site and script interface
	var API = (function () {

		var API = function () {
			// Custom events
			this.events = {
				"page_type_detected": [],
				"script_detected": [],
				"thread_update": [],
				"settings_4chanx_open": [],
				"settings_4chanx_close": [],
				"settings_4chanx_section_change": [],
				"settings_vanilla_open": [],
				"settings_vanilla_close": [],
				"menu_4chanx_open": [],
				"menu_4chanx_close": [],
				"image_hover_open": [],
				"image_hover_close": [],
				"post_add": [],
				"post_remove": [],
				"quick_reply_add": [],
				"quick_reply_remove": [],
				"quick_reply_show": [],
				"quick_reply_hide": [],
				"file_info_update": [],
			};
			this.getters = {
				"posts": get_all_posts.bind(this),
			};

			// Nodes
			this.settings_4chanx_container = null;
			this.settings_vanilla_container = null;
			this.menu_4chanx_container = null;

			// Script
			this.is_4chanx = false;
			this.is_appchanx = false;
			this.page_type = "";

			// Document listening
			this.doc_observer = null;
			this.body_observer = null;
			this.delform_observer = null;
			this.settings_observer = null;
			this.hover_ui_observer = null;
			this.header_settings_menu_observer = null;
			this.linkify_observer = null;
			this.quick_reply_observer = null;
		};



		var on_asap = function () {
			// Hook
			hook_body_observers.call(this);
			hook_hover_observers.call(this, null);
			hook_delform_observers.call(this, null);
			hook_header_observers.call(this, null);

			// Detect scripts
			var el = document.documentElement;
			if (el) {
				detect_page_type.call(this, el);
				detect_scripts.call(this, el);

				var qr;
				if ((qr = el.querySelector("body>#qr,body>#quickReply"))) {
					trigger.call(this, "quick_reply_add", qr);
					trigger.call(this, "quick_reply_show", qr);
					hook_quick_reply_observers.call(this, qr);
				}
			}
		};

		var on_document_observe = function (records) {
			var i, r;

			for (i = 0; i < records.length; ++i) {
				r = records[i];

				if (r.attributeName == "class") {
					// Detect
					detect_scripts.call(this, r.target);
				}
			}
		};

		var on_body_observe = function (records) {
			var i, j, nodes;

			for (i = 0; i < records.length; ++i) {
				if ((nodes = records[i].addedNodes)) {
					for (j = 0; j < nodes.length; ++j) {
						// Check
						on_body_element_add.call(this, nodes[j]);
					}
				}
				if ((nodes = records[i].removedNodes)) {
					for (j = 0; j < nodes.length; ++j) {
						// Check
						on_body_element_remove.call(this, nodes[j]);
					}
				}
			}
		};
		var on_body_element_add = function (element) {
			var id = element.getAttribute("id");
			if (id == "fourchanx-settings") {
				// 4chan-x / ccd0
				trigger_settings_4chanx_open.call(this, element);
			}
			else if (id == "appchanx-settings") {
				// appchan-x / zixaphir
				trigger_settings_4chanx_open.call(this, element);
			}
			else if (id == "overlay") {
				// 4chan-x / ihavenoface
				var e2 = element.querySelector("#fourchanx-settings");
				if (e2) {
					trigger_settings_4chanx_open.call(this, e2);
				}
			}
			else if (id == "settingsMenu") {
				// 4chan vanilla
				trigger_settings_vanilla_open.call(this, element);
			}
			else if (id == "hoverUI") {
				hook_hover_observers.call(this, element);
			}
			else if (id == "delform") {
				hook_delform_observers.call(this, element);
			}
			else if (id == "image-hover") {
				// Vanilla 4chan image hover
				trigger.call(this, "image_hover_open", {
					container: element
				});
			}
			else if (id == "qp") {
				// 4chan-x
				var pc = element.querySelector(".postContainer");
				if (pc) {
					trigger.call(this, "post_add", pc);
				}
				// else, post might not be loaded yet
			}
			else if (id == "header-bar" || id == "header") {
				hook_header_observers.call(this, element);
			}
			else if (id == "qr" || id == "quickReply") {
				// 4chan-x quick reply
				trigger.call(this, "quick_reply_add", element);
				trigger.call(this, "quick_reply_show", element);
				hook_quick_reply_observers.call(this, element);
			}
			/*else if (id == "quote-preview") {
				// Vanilla 4chan
				// Only a .post; no .postContainer
				trigger.call(this, "post_add", element);
			}*/
		};
		var on_body_element_remove = function (element) {
			var id = element.getAttribute("id");
			if (id == "fourchanx-settings") {
				trigger_settings_4chanx_close.call(this, element);
			}
			else if (id == "appchanx-settings") {
				trigger_settings_4chanx_close.call(this, element);
			}
			else if (id == "overlay") {
				var e2 = element.querySelector("#fourchanx-settings");
				if (e2) {
					trigger_settings_4chanx_close.call(this, e2);
				}
			}
			else if (id == "settingsMenu") {
				// 4chan vanilla
				trigger_settings_vanilla_close.call(this, element);
			}
			else if (id == "image-hover") {
				trigger.call(this, "image_hover_close", {
					container: element
				});
			}
			else if (id == "qp") {
				// 4chan-x
				var pc = element.querySelector(".postContainer");
				if (pc) {
					trigger.call(this, "post_remove", pc);
				}
			}
			else if (id == "qr" || id == "quickReply") {
				trigger.call(this, "quick_reply_hide", element);
				trigger.call(this, "quick_reply_remove", element);
				unhook_quick_reply_observers.call(this);
			}
			/*else if (id == "quote-preview") {
				// Vanilla 4chan
				// Only a .post; no .postContainer
				trigger.call(this, "post_remove", element);
			}*/
		};

		var on_delform_post_observe = function (records) {
			var nodes, n, i, j, k, im, jm, km, pc;
			i = 0;
			im = records.length;
			for (; i < im; ++i) {
				if ((nodes = records[i].addedNodes)) {
					j = 0;
					jm = nodes.length;
					for (; j < jm; ++j) {
						// Check
						n = nodes[j];
						if (style.has_class(n, "postContainer")) {
							trigger.call(this, "post_add", n);
						}
						else if (style.has_class(n, "thread") || style.has_class(n, "board")) {
							pc = n.querySelectorAll(".postContainer");
							k = 0;
							km = pc.length;
							for (; k < km; ++k) {
								trigger.call(this, "post_add", pc[k]);
							}
						}
						else if (style.has_class(n, "file-info")) {
							trigger.call(this, "file_info_update", n);
						}
						/*else if (n.tagName == "A" && style.has_class(n, "linkify")) {
							trigger.call(this, "linkify", n);
						}*/
					}
				}
				if ((nodes = records[i].removedNodes)) {
					j = 0;
					jm = nodes.length;
					for (; j < jm; ++j) {
						// Check
						n = nodes[j];
						if (style.has_class(n, "inline")) {
							pc = n.querySelector(".postContainer");
							if (pc) {
								trigger.call(this, "post_remove", pc);
							}
						}
						else if (style.has_class(n, "thread") || style.has_class(n, "board")) {
							pc = n.querySelectorAll(".postContainer");
							k = 0;
							km = pc.length;
							for (; k < km; ++k) {
								trigger.call(this, "post_remove", pc[k]);
							}
						}
					}
				}
			}
		};

		var on_settings_observe = function (records) {
			for (var i = 0; i < records.length; ++i) {
				if (records[i].attributeName == "class") {
					// Section change
					trigger.call(this, "settings_4chanx_section_change", {
						container: this.settings_4chanx_container,
						section: records[i].target
					});
				}
			}
		};

		var on_quick_reply_observe = function (records) {
			var i, r;

			for (i = 0; i < records.length; ++i) {
				r = records[i];

				if (r.attributeName == "hidden") {
					// Detect
					if (r.target.getAttribute(r.attributeName) === null) {
						trigger.call(this, "quick_reply_show", r.target);
					}
					else {
						trigger.call(this, "quick_reply_hide", r.target);
					}
				}
			}
		};

		var on_hover_ui_observe = function (records) {
			var i, j, nodes, r;

			for (i = 0; i < records.length; ++i) {
				r = records[i];

				if ((nodes = r.addedNodes)) {
					for (j = 0; j < nodes.length; ++j) {
						// Check
						on_hover_ui_element_add.call(this, nodes[j]);
					}
				}
				if ((nodes = r.removedNodes)) {
					for (j = 0; j < nodes.length; ++j) {
						// Check
						on_hover_ui_element_remove.call(this, nodes[j]);
					}
				}
			}
		};
		var on_hover_ui_element_add = function (element) {
			var id = element.getAttribute("id");
			if (id == "ihover") {
				if (element.getAttribute("data-full-i-d")) {
					trigger.call(this, "image_hover_open", {
						container: element
					});
				}
			}
			else if (id == "qp") {
				var pc = element.querySelector(".postContainer");
				if (pc) {
					trigger.call(this, "post_add", pc);
				}
				// else, post might not be loaded yet
			}
			else if (id == "menu") {
				// 4chan-x / ccd0, appchan-x
				trigger_menu_4chanx_open.call(this, element, null);
			}
		};
		var on_hover_ui_element_remove = function (element) {
			var id = element.getAttribute("id");
			if (id == "ihover") {
				trigger.call(this, "image_hover_close", {
					container: element
				});
			}
			else if (id == "qp") {
				var pc = element.querySelector(".postContainer");
				if (pc) {
					trigger.call(this, "post_remove", pc);
				}
			}
			else if (id == "menu") {
				// 4chan-x / ccd0, appchan-x
				trigger_menu_4chanx_close.call(this, element);
			}
		};

		var on_4chanx_menu_observe = function (type, records) {
			var i, j, nodes, r;

			for (i = 0; i < records.length; ++i) {
				r = records[i];

				if ((nodes = r.addedNodes)) {
					for (j = 0; j < nodes.length; ++j) {
						// Check
						on_4chanx_menu_element_add.call(this, nodes[j], type);
					}
				}
				if ((nodes = r.removedNodes)) {
					for (j = 0; j < nodes.length; ++j) {
						// Check
						on_4chanx_menu_element_remove.call(this, nodes[j], type);
					}
				}
			}
		};
		var on_4chanx_menu_element_add = function (element, type) {
			var id = element.getAttribute("id");
			if (id == "menu") {
				// 4chan-x
				trigger_menu_4chanx_open.call(this, element, type);
			}
		};
		var on_4chanx_menu_element_remove = function (element, type) {
			var id = element.getAttribute("id");
			if (id == "menu") {
				// 4chan-x
				trigger_menu_4chanx_close.call(this, element);
			}
		};

		var on_menu_4chanx_entry_mouseenter = function (event, node) {
			if (this.menu_4chanx_container) {
				var focused = this.menu_4chanx_container.querySelectorAll(".focused"),
					i;

				for (i = 0; i < focused.length; ++i) {
					style.remove_class(focused[i], "focused");
				}
			}

			style.add_class(node, "focused");
		};
		var on_menu_4chanx_entry_mouseleave = function (event, node) {
			// This is removed even for versions which menu items keep it (4chan-x / ccd0, etc.)
			style.remove_class(node, "focused");
		};

		var hook_document_observer = function () {
			var el = document.documentElement;
			if (!el) return;

			if (this.doc_observer === null) {
				// Create new observer
				this.doc_observer = new MutationObserver(on_document_observe.bind(this));

				// Observe
				this.doc_observer.observe(
					el,
					{
						attributes: true
					}
				);
			}
		};
		var hook_body_observers = function () {
			// Observe the body
			var body = document.querySelector("body");
			if (!body) return;

			if (this.body_observer === null) {
				// Create new observer
				this.body_observer = new MutationObserver(on_body_observe.bind(this));

				// Observe
				this.body_observer.observe(
					body,
					{
						childList: true,
					}
				);
			}
		};
		var hook_hover_observers = function (hover_ui) {
			if (hover_ui === null) {
				hover_ui = document.getElementById("hoverUI");
				if (!hover_ui) return;
			}

			if (this.hover_ui_observer === null) {
				// Create new observer
				this.hover_ui_observer = new MutationObserver(on_hover_ui_observe.bind(this));

				// Observe
				this.hover_ui_observer.observe(
					hover_ui,
					{
						childList: true,
					}
				);
			}
		};
		var hook_delform_observers = function (delform) {
			if (delform === null) {
				delform = document.getElementById("delform");
				if (!delform) return;
			}

			if (this.delform_observer === null) {
				// Create new observer
				this.delform_observer = new MutationObserver(on_delform_post_observe.bind(this));

				// Observe
				this.delform_observer.observe(
					delform,
					{
						childList: true,
						subtree: true,
					}
				);
			}
		};
		var hook_header_observers = function (header) {
			if (header === null) {
				header = document.getElementById("header-bar");
				if (!header) {
					header = document.getElementById("header");
					if (!header) return;
				}
			}

			var button;

			if (this.header_settings_menu_observer === null && (button = header.querySelector(".menu-button"))) {
				// Create new observer
				this.header_settings_menu_observer = new MutationObserver(on_4chanx_menu_observe.bind(this, "main"));

				// Observe
				this.header_settings_menu_observer.observe(
					button,
					{
						childList: true,
					}
				);
			}
		};
		var hook_quick_reply_observers = function (qr) {
			// Observe the quick reply
			if (!qr) return;

			if (this.quick_reply_observer === null) {
				// Create new observer
				this.quick_reply_observer = new MutationObserver(on_quick_reply_observe.bind(this));

				// Observe
				this.quick_reply_observer.observe(
					qr,
					{
						attributes: true,
						attributeOldValue: true,
					}
				);
			}
		};
		var unhook_quick_reply_observers = function () {
			// Stop observing
			if (this.quick_reply_observer !== null) {
				this.quick_reply_observer.disconnect();
				this.quick_reply_observer = null;
				return true;
			}
			return false;
		};

		var detect_page_type = function (doc_el) {
			// Detect page type
			var delform = doc_el.querySelector("#delform");
			if (delform) {
				var flash_table = delform.querySelector(".flashListing");
				if (flash_table) {
					// Flash board
					this.page_type = "board_flash";
				}
				else {
					// Thread or board or catalog
					if (delform.querySelector(".board.catalog-small")) {
						// 4chan-x
						this.page_type = "catalog";
					}
					else if (doc_el.querySelector("#search-box")) {
						this.page_type = "board";
					}
					else {
						this.page_type = "thread";
					}
				}
			}
			else {
				// Index
				if (doc_el.querySelector("#content>#threads")) {
					this.page_type = "catalog";
				}
				else if (doc_el.querySelector("#doc")) {
					this.page_type = "error";
				}
				else {
					// Image or video
					var n = doc_el.querySelectorAll("body>*");

					if (n.length === 1) {
						n = n[0];
						if (n.tagName === "IMG") {
							this.page_type = "image";
						}
						else if (n === "VIDEO") {
							this.page_type = "video";
						}
					}
				}
			}

			// Trigger event
			trigger.call(this, "page_type_detected", {
				page_type: this.page_type
			});
		};
		var detect_scripts = function (doc_el) {
			// Detect 4chan-x / appchan-x
			if (!this.is_4chanx) {
				this.is_4chanx = style.has_class(doc_el, "fourchan-x");
				if (this.is_4chanx) {
					this.is_appchanx = style.has_class(doc_el, "appchan-x");

					// Detected
					trigger.call(this, "script_detected", {
						name: "4chan-x"
					});
				}
			}
		};

		var trigger = function (event, data) {
			// Trigger
			if (event in this.events) {
				var e_list = this.events[event];
				for (var i = 0; i < e_list.length; ++i) {
					e_list[i].call(this, data);
				}
			}
		};
		var trigger_menu_4chanx_open = function (element, type) {
			// Find type
			var type_checks = {
					"main": {
						classes: [ "settings-link" , "image-expansion-link" , "gallery-link" ],
						count: 0,
					},
					"post": {
						classes: [ "report-link" , "hide-reply-link" , "delete-link" ],
						count: 0,
					},
				},
				c = element.firstChild,
				t, tc, i;

			if (type === null) {
				type = "other";

				while (c) {
					// Class
					if (style.has_class(c, "entry")) {
						// Check for class types
						for (t in type_checks) {
							tc = type_checks[t];
							for (i = 0; i < tc.classes.length; ++i) {
								if (style.has_class(c, tc.classes[i])) {
									++tc.count;
									tc = null;
									break;
								}
							}
							// Done
							if (!tc) break;
						}
					}

					// Next
					c = c.nextSibling;
				}

				// Check type
				i = 0;
				for (t in type_checks) {
					if (type_checks[t].count > i) {
						i = type_checks[t].count;
						type = t;
					}
				}
			}

			// Event
			this.menu_4chanx_container = element;
			trigger.call(this, "menu_4chanx_open", {
				container: element,
				type: type
			});
		};
		var trigger_menu_4chanx_close = function (element) {
			// Event
			this.menu_4chanx_container = null;
			trigger.call(this, "menu_4chanx_close", {
				container: element,
			});
		};
		var trigger_settings_4chanx_open = function (element) {
			// Open
			this.settings_4chanx_container = element;

			// Event
			trigger.call(this, "settings_4chanx_open", {
				container: element
			});

			// Settings observer
			if (this.settings_observer !== null) {
				this.settings_observer.disconnect();
				this.settings_observer = null;
			}
			var section = element.querySelector("section");
			if (section) {
				this.settings_observer = new MutationObserver(on_settings_observe.bind(this));

				// Observe
				this.settings_observer.observe(
					section,
					{
						attributes: true,
					}
				);

				// Section change
				trigger.call(this, "settings_4chanx_section_change", {
					container: element,
					section: section
				});
			}
		};
		var trigger_settings_4chanx_close = function (element) {
			// Close
			this.settings_4chanx_container = null;
			if (this.settings_observer !== null) {
				this.settings_observer.disconnect();
				this.settings_observer = null;
			}

			// Event
			trigger.call(this, "settings_4chanx_close", {
				container: element
			});
		};
		var trigger_settings_vanilla_open = function (element) {
			// Open
			this.settings_vanilla_container = element;

			// Event
			trigger.call(this, "settings_vanilla_open", {
				container: element
			});
		};
		var trigger_settings_vanilla_close = function (element) {
			// Close
			this.settings_vanilla_container = null;

			// Event
			trigger.call(this, "settings_vanilla_close", {
				container: element
			});
		};

		var get_all_posts = function () {
			// Acquire all .postContainer's
			var elements = null, e;
			var container = document.getElementById("delform");
			if (container) {
				elements = Array.prototype.slice.call(container.querySelectorAll(".postContainer"), 0);
			}

			container = document.getElementById("hoverUI");
			if (container) {
				e = Array.prototype.slice.call(container.querySelectorAll(".postContainer"), 0);
				if (elements) elements = elements.concat(e);
				else elements = e;
			}

			// Return
			return (elements === null ? [] : elements);
		};

		var deep_dom_wrap = (function () {

			// Internal helper class
			var Offset = function (text_offset, node) {
				this.text_offset = text_offset;
				this.node = node;
				this.node_text_length = node.nodeValue.length;
			};



			// Main function
			var deep_dom_wrap = function (container, tag, matcher, element_checker, setup_function, quick) {
				var text = "",
					offsets = [],
					d = document,
					count = 0,
					match_pos = 0,
					node, par, next, check, match,
					pos_start, pos_end, offset_start, offset_end,
					prefix, suffix, link_base, link_node, relative_node, relative_par, clone, i, n1, n2, len, offset_current, offset_node;


				// Create a string of the container's contents (similar to but not exactly the same as node.textContent)
				// Also lists all text nodes into the offsets array
				par = container;
				node = container.firstChild;
				if (node === null) return 0; // Quick exit for empty container
				while (true) {
					if (node !== null) {
						if (node.nodeType === 3) { // TEXT_NODE
							// Add to list and text
							offsets.push(new Offset(text.length, node));
							text += node.nodeValue;
						}
						else if (node.nodeType === 1) { // ELEMENT_NODE
							// Action callback
							check = element_checker.call(null, node);
							// Line break
							if ((check & deep_dom_wrap.EL_TYPE_LINE_BREAK) !== 0) {
								text += "\n";
							}
							// Parse
							if ((check & deep_dom_wrap.EL_TYPE_NO_PARSE) === 0) {
								par = node;
								node = node.firstChild;
								continue;
							}
						}

						// Next
						node = node.nextSibling;
					}
					else {
						// Done?
						if (par === container) break;

						// Move up
						node = par;
						par = node.parentNode;
						node = node.nextSibling;
					}
				}

				// Quick mode: just find all the matches
				if (quick) {
					// Match the text
					match = matcher.call(null, text, match_pos);
					if (match === null) return count;

					++count;

					match_pos = match[1];
				}

				// Loop to find all links
				while (true) {
					// Match the text
					match = matcher.call(null, text, match_pos);
					if (match === null) break;
					++count;



					// Find the beginning and ending text nodes
					pos_start = match[0];
					pos_end = match[1];

					for (offset_start = 1; offset_start < offsets.length; ++offset_start) {
						if (offsets[offset_start].text_offset > pos_start) break;
					}
					for (offset_end = offset_start; offset_end < offsets.length; ++offset_end) {
						if (offsets[offset_end].text_offset > pos_end) break;
					}
					--offset_start;
					--offset_end;



					// Vars to create the link
					prefix = text.substr(offsets[offset_start].text_offset, pos_start - offsets[offset_start].text_offset);
					suffix = text.substr(pos_end, offsets[offset_end].text_offset + offsets[offset_end].node_text_length - pos_end);
					link_base = d.createElement(tag);
					link_node = link_base;
					relative_node = null;

					// Prefix update
					i = offset_start;
					offset_current = offsets[i];
					offset_node = offset_current.node;
					if (prefix.length > 0) {
						// Insert prefix
						n1 = d.createTextNode(prefix);
						offset_node.parentNode.insertBefore(n1, offset_node);

						// Update text
						offset_node.nodeValue = offset_node.nodeValue.substr(prefix.length);

						// Set first relative
						relative_node = n1;
						relative_par = n1.parentNode;

						// Update offset for next search
						len = prefix.length;
						offset_current.text_offset += len;
						offset_current.node_text_length -= len;
					}
					else {
						// Set first relative
						relative_node = offset_node.previousSibling;
						relative_par = offset_node.parentNode;
					}

					// Loop over ELEMENT_NODEs; add TEXT_NODEs to the link, remove empty nodes where necessary
					// The only reason the par variable is necessary is because some nodes are removed during this process
					for (; i < offset_end; ++i) {
						// Next
						node = offsets[i].node;
						next = node.nextSibling;
						par = node.parentNode;

						// Add text
						link_node.appendChild(node);

						// Node loop
						while (true) {
							if (next) {
								if (next.nodeType == 3) { // TEXT_NODE
									// Done
									break;
								}
								else if (next.nodeType == 1) { // ELEMENT_NODE
									// Deeper
									node = next;
									next = node.firstChild;
									par = node;

									// Update link node
									clone = node.cloneNode(false);
									link_node.appendChild(clone);
									link_node = clone;

									continue;
								}
								else {
									// Some other node type; continue anyway
									node = next;
									next = node.nextSibling;

									// Update link node
									link_node.appendChild(node);

									continue;
								}
							}

							// Shallower
							node = par;
							next = node.nextSibling;
							par = node.parentNode;

							if (node.firstChild === null) par.removeChild(node);

							// Update link node
							if (link_node !== link_base) {
								// Simply move up tree (link_node still has a parent)
								link_node = link_node.parentNode;
							}
							else {
								// Create a new wrapper node (link_node has no parent; it's the link_base)
								clone = node.cloneNode(false);
								for (n1 = link_base.firstChild; n1; n1 = n2) {
									n2 = n1.nextSibling;
									clone.appendChild(n1);
								}
								link_base.appendChild(clone);
								link_node = link_base;

								// Placement relatives
								relative_node = (next !== null) ? next.previousSibling : null;
								relative_par = par;
							}
						}
					}

					// Suffix update
					offset_current = offsets[i];
					offset_node = offset_current.node;
					if (suffix.length > 0) {
						// Insert suffix
						n1 = d.createTextNode(suffix);
						if ((n2 = offset_node.nextSibling) !== null) {
							offset_node.parentNode.insertBefore(n1, n2);
						}
						else {
							offset_node.parentNode.appendChild(n1);
						}

						// Update text
						len = offset_node.nodeValue.length - suffix.length;
						offset_node.nodeValue = offset_node.nodeValue.substr(0, len);

						// Update offset for next search
						offset_current.text_length += len;
						offset_current.node_text_length -= len;
						offset_current.node = n1;
					}

					// Add the last segment
					par = offset_node.parentNode;
					link_node.appendChild(offset_node);



					// Setup function
					if (setup_function !== null) setup_function.call(null, link_base, match);



					// Find the proper relative node
					relative_node = (relative_node !== null) ? relative_node.nextSibling : relative_par.firstChild;

					// Insert link
					if (relative_node !== null) {
						// Insert before it
						relative_par.insertBefore(link_base, relative_node);
					}
					else {
						// Add to end
						relative_par.appendChild(link_base);
					}

					// Remove empty suffix tags
					while (par.firstChild === null) {
						node = par;
						par = par.parentNode;
						par.removeChild(node);
					}



					// Update match position
					offsets[offset_end].text_offset = pos_end;
					match_pos = pos_end;
				}

				// Done
				return count;
			};



			// Element type constants
			deep_dom_wrap.EL_TYPE_PARSE = 0;
			deep_dom_wrap.EL_TYPE_NO_PARSE = 1;
			deep_dom_wrap.EL_TYPE_LINE_BREAK = 2;



			// Return the function
			return deep_dom_wrap;

		})();

		var deep_dom_wrap_filter = function (node) {
			if (node.tagName === "BR" || node.tagName === "A") {
				return deep_dom_wrap.EL_TYPE_NO_PARSE | deep_dom_wrap.EL_TYPE_LINE_BREAK;
			}
			else if (node.tagName === "WBR") {
				return deep_dom_wrap.EL_TYPE_NO_PARSE;
			}
			else if (node.tagName === "DIV") {
				if (style.has_class(node, "inline") || style.has_class(node, "inlined")) return deep_dom_wrap.EL_TYPE_NO_PARSE | deep_dom_wrap.EL_TYPE_LINE_BREAK;
				return deep_dom_wrap.EL_TYPE_LINE_BREAK;
			}

			return deep_dom_wrap.EL_TYPE_PARSE;
		};
		var deep_dom_wrap_filter_simple = function (node) {
			if (node.tagName === "BR") {
				return deep_dom_wrap.EL_TYPE_NO_PARSE | deep_dom_wrap.EL_TYPE_LINE_BREAK;
			}
			else if (node.tagName === "WBR") {
				return deep_dom_wrap.EL_TYPE_NO_PARSE;
			}
			else if (node.tagName === "DIV") {
				if (style.has_class(node, "inline") || style.has_class(node, "inlined")) return deep_dom_wrap.EL_TYPE_NO_PARSE | deep_dom_wrap.EL_TYPE_LINE_BREAK;
				return deep_dom_wrap.EL_TYPE_LINE_BREAK;
			}

			return deep_dom_wrap.EL_TYPE_PARSE;
		};



		API.prototype = {
			constructor: API,

			destroy: function () {
				// Remove event listeners
				if (this.doc_observer !== null) {
					this.doc_observer.disconnect();
					this.doc_observer = null;
				}
				if (this.body_observer !== null) {
					this.body_observer.disconnect();
					this.body_observer = null;
				}
				if (this.settings_observer !== null) {
					this.settings_observer.disconnect();
					this.settings_observer = null;
				}
				if (this.hover_ui_observer !== null) {
					this.hover_ui_observer.disconnect();
					this.hover_ui_observer = null;
				}
				if (this.delform_observer !== null) {
					this.delform_observer.disconnect();
					this.delform_observer = null;
				}
			},

			setup: function () {
				// Check for other addons
				hook_document_observer.call(this);
				on_ready(on_asap.bind(this));
			},

			on: function (event, callback) {
				// Add callback
				if (event in this.events) {
					this.events[event].push(callback);
				}
			},
			off: function (event, callback) {
				// Add callback
				if (event in this.events) {
					var e_list = this.events[event];
					for (var i = 0; i < e_list.length; ++i) {
						if (e_list[i] === callback) {
							// Remove
							e_list.splice(i, 1);
							return true;
						}
					}
				}

				return false;
			},
			get: function (getter) {
				if (getter in this.getters) {
					return this.getters[getter].call(this);
				}

				return null;
			},

			settings_4chanx_open: function (tab) {
				// Signal 4chan-x to open settings
				document.dispatchEvent(new CustomEvent("OpenSettings", {
					detail: tab || "Main"
				}));
			},
			settings_4chanx_close: function () {
				if (!this.settings_4chanx_container) return;

				// Click the overlay
				var overlay = document.getElementById("overlay");
				if (overlay) {
					overlay.click();
				}
			},
			settings_4chanx_change_section: function (section) {
				if (!this.settings_4chanx_container) return;

				section = section.replace(/\s+/g, "-").toLowerCase();
				var target_tab = this.settings_4chanx_container.querySelector(".tab-" + section);
				if (target_tab) {
					target_tab.click();
				}
			},
			settings_4chanx_is_open: function () {
				return (this.settings_4chanx_container !== null);
			},
			settings_vanilla_open: function () {
				var settings_link = document.getElementById("settingsWindowLink");
				if (settings_link) {
					settings_link.click();
				}
			},
			settings_vanilla_close: function () {
				if (!this.settings_vanilla_container) return;

				var button = this.settings_vanilla_container.querySelector(".panelHeader>span>.pointer");
				if (button) {
					button.click();
				}
			},
			settings_vanilla_save: function () {
				if (!this.settings_vanilla_container) return;

				var button = this.settings_vanilla_container.querySelector("button[data-cmd='settings-save']");
				if (button) {
					button.click();
				}
			},
			settings_vanilla_is_open: function () {
				return (this.settings_vanilla_container !== null);
			},

			menu_4chanx_close: function () {
				if (!this.menu_4chanx_container) return;

				var par = this.menu_4chanx_container.parentNode;
				if (par) par.click();
			},
			menu_4chanx_is_open: function () {
				return (this.menu_4chanx_container !== null);
			},
			menu_4chanx_add_entry: function (menu_container, element, order) {
				// Styling
				style.add_class(element, "entry");
				element.style.order = (order || 0);

				// Events
				var e_enter = wrap_mouseenterleave_event(this, on_menu_4chanx_entry_mouseenter),
					e_leave = wrap_mouseenterleave_event(this, on_menu_4chanx_entry_mouseleave);

				element.addEventListener("mouseover", e_enter, false);
				element.addEventListener("mouseout", e_leave, false);

				// Add
				menu_container.appendChild(element);
			},

			notification: function (type, content, lifetime, callback) {
				// Setup
				var detail = {
					type: type,
					content: content,
				};
				if (arguments.length >= 3) {
					detail.lifetime = lifetime;
				}
				if (arguments.length >= 4) {
					detail.cb = callback;
				}

				// Display
				document.dispatchEvent(new CustomEvent("CreateNotification", {
					detail: detail
				}));
			},

			post_is_image_expanded_or_expanding: function (post_container) {
				var n_file, n_img;

				// Expanded
				if (this.is_4chanx) {
					if (style.has_class(post_container, "expanded-image")) return true;

					// Get the .file container
					n_file = this.post_get_file_info_container(post_container);

					// Find image
					n_img = n_file.querySelector("img");
					return (n_img && style.has_class(n_img, "expanding"));
				}
				else {
					// Get the .file container
					n_file = this.post_get_file_info_container(post_container);

					// Return
					return (style.has_class(n_file, "image-expanded") || n_file.querySelector("video.expandedWebm") !== null);
				}
			},
			post_get_file_info: function (post_container) {
				return this.post_get_file_info_from_file_info_container(this.post_get_file_info_container(post_container));
			},
			post_get_file_info_from_file_info_container: function (file_info) {
				// Setup info
				var info = {
					url: "",
					thumb: null,
					spoiler: null,
					name: "",
					resolution: {
						width: 0,
						height: 0
					},
					resolution_thumb: {
						width: 0,
						height: 0
					},
					size: 0,
					size_label: "",
				};

				// Nodes
				var n, m, n_p, style_str;

				if (file_info) {
					// Get info
					if ((n_p = file_info.querySelector(".file-info"))) {
						// 4chan-x
						if ((n = n_p.querySelector("a"))) {
							info.url = n.getAttribute("href") || "";
							var n2 = n.firstChild;
							if (n2 !== null && n2.className && style.has_class(n2, "fnswitch") && (n2 = n2.querySelector(".fnfull")) !== null) {
								info.name = n2.textContent.trim();
							}
							else {
								info.name = n.textContent.trim();
							}
						}
						if ((n = n_p.lastChild)) {
							m = /([0-9\.]+)\s*(\w?b),(?:\s*([0-9]+)x([0-9]+))?/i.exec((n.textContent || ""));
							if (m) {
								info.size = parseFloat(m[1]);
								info.size_label = m[2].toLowerCase();
								if (m[3] !== undefined) {
									info.resolution.width = parseInt(m[3], 10);
									info.resolution.height = parseInt(m[4], 10);
								}
							}
						}
					}
					else if ((n_p = file_info.querySelector(".fileText"))) {
						// Vanilla
						if ((n = n_p.querySelector("a"))) {
							info.url = n.getAttribute("href") || "";

							// File name
							info.name = n_p.getAttribute("title") || n.getAttribute("title") || n.textContent || "";

							// Attributes
							if ((n = n.nextSibling)) {
								m = /([0-9\.]+)\s*(\w?b),(?:\s*([0-9]+)x([0-9]+))?/i.exec((n.textContent || ""));
								if (m) {
									info.size = parseFloat(m[1]);
									info.size_label = m[2].toLowerCase();
									if (m[3] !== undefined) {
										info.resolution.width = parseInt(m[3], 10);
										info.resolution.height = parseInt(m[4], 10);
									}
								}
							}
						}
					}

					// Thumbnail and spoiler status
					if ((n_p = file_info.querySelector(".fileThumb"))) {
						if ((n = n_p.querySelector("img:not(.full-image):not(.expanded-thumb)"))) {
							m = n.getAttribute("src");
							if (style.has_class(n_p, "imgspoiler")) {
								info.spoiler = m;
								// Assume thumbnail
								if (info.url) {
									info.thumb = info.url.replace(/\/\/(.*)i\.4cdn\.org/g, "//t.4cdn.org").replace(/\.([^\.]*)$/, "s.jpg");
								}
							}
							else {
								info.thumb = m;
							}

							// Resolution
							style_str = n.getAttribute("style");
							m = /width\s*\:\s*([0-9\.]+)px/i.exec(style_str);
							if (m) {
								info.resolution_thumb.width = parseFloat(m[1]) || 0;
							}
							m = /height\s*\:\s*([0-9\.]+)px/i.exec(style_str);
							if (m) {
								info.resolution_thumb.height = parseFloat(m[1]) || 0;
							}
						}
					}
				}

				// Info
				return info;
			},

			get_post_container_from_id: function (id) {
				return document.getElementById("pc" + id);
			},

			post_get_file_info_container: function (post_container) {
				// Post body
				var node = post_container.querySelector(".post");
				if (node && (node = node.firstChild)) {
					while (true) {
						// File container
						if (style.has_class(node, "file")) return node;

						// Next
						if (!(node = node.nextSibling)) return null;
					}
				}

				return null;
			},
			post_get_image_container: function (post_container) {
				// Post body
				var node = post_container.querySelector(".post");
				if (node && (node = node.firstChild)) {
					while (true) {
						// File container
						if (style.has_class(node, "file")) {
							// Get thumbnail container
							return node.querySelector(".fileThumb");
						}

						// Next
						if (!(node = node.nextSibling)) return null;
					}
				}

				return null;
			},
			post_get_post_container_from_image_container: function (image_container) {
				// Parent check
				var n = image_container;
				while ((n = n.parentNode) && !style.has_class(n, "postContainer"));
				return n;
			},
			post_get_image_expanded_from_image_container: function (image_container) {
				if (this.is_4chanx) {
					return image_container.querySelector(".full-image");
				}
				else {
					var n = image_container.querySelector("img.expanded-thumb");
					if (n !== null) return n;

					if (image_container.parentNode === null) return null;

					return image_container.parentNode.querySelector("video.expandedWebm");
				}
			},
			post_get_file_nodes: function (post_container) {
				var post = post_container.querySelector(".post"),
					nodes, node;

				if (post !== null) {
					// File info container
					if ((node = post.firstChild)) {
						while (true) {
							// File container
							if (style.has_class(node, "file")) {
								nodes = {
									container: node,
									info_container: node.querySelector(".fileText,.file-info"),
									link_thumbnail: node.querySelector(".fileThumb"),
									link: null,
								};

								if (nodes.info_container !== null) {
									nodes.link = node.querySelector("a");
								}

								return nodes;
							}

							// Next
							if (!(node = node.nextSibling)) break;
						}
					}
				}

				return null;
			},
			post_has_file: function (post_container) {
				var post = post_container.querySelector(".post"),
					node;

				if (post !== null && (node = post.firstChild)) {
					while (true) {
						// File container
						if (style.has_class(node, "file")) return true;

						// Next
						if (!(node = node.nextSibling)) break;
					}
				}

				return false;
			},
			post_get_file_node_link_from_file_info: function (file_info) {
				return file_info.querySelector("a");
			},
			post_get_file_name_from_file_info: function (file_info) {
				var n = file_info.parentNode;
				if (n !== null && (n = n.parentNode) !== null) {
				}
				return null;
			},
			post_get_file_container_from_file_info: function (file_info) {
				var n = file_info.parentNode;
				if (n !== null && (n = n.parentNode) !== null) {
					if (style.has_class(n, "file")) return n;
				}
				return null;
			},
			post_get_nodes: function (post_container) {
				var post = post_container.querySelector(".post"),
					nodes, node, node2;

				if (post === null) return null;

				nodes = {
					subject: null,
					name: null,
					tripcode: null,
					date: null,
					no: null,
					number: null,
					comment: null,
				};

				// Post info
				if ((node = post.querySelector(".postInfo"))) {
					nodes.subject = node.querySelector(".subject");
					nodes.name = node.querySelector(".name");
					nodes.tripcode = node.querySelector(".posterTrip");
					nodes.date = node.querySelector(".dateTime");
					if ((node2 = node.querySelectorAll(".postNum>a")).length >= 2) {
						nodes.no = node2[0];
						nodes.number = node2[1];
					}
				}

				// Comment
				for (node = post.lastChild; node; node = node.previousSibling) {
					if (style.has_class(node, "postMessage")) {
						nodes.comment = node;
						break;
					}
				}

				return nodes;
			},
			post_get_id: function (post_container) {
				var id = post_container.getAttribute("id");
				return id ? parseInt(id.replace(/[^0-9]+/g, ""), 10) : -1;
			},
			post_get_id_from_node: function (node) {
				// Parent check
				while (true) {
					if (node === null) return -1;
					if (style.has_class(node, "postContainer")) return this.post_get_id(node);
					node = node.parentNode;
				}
			},
			post_get_comment_container: function (post_container) {
				var post = post_container.querySelector(".post"),
					comment;

				if (post === null) return null;

				// Find comment
				comment = post.lastChild;
				while (true) {
					if (comment === null) return 0;
					if (style.has_class(comment, "postMessage")) break;
					comment = comment.previousSibling;
				}

				return comment;
			},
			post_get_quotelinks: function (post_container) {
				return this.post_query_selector_all(post_container, ".quotelink");
			},
			post_comment_scan: function (post_container, regex, tag, setup_function) {
				var comment = this.post_get_comment_container(post_container),
					s_fn = null,
					m_fn;

				if (comment === null) return 0;

				// Matching function
				if (typeof(regex) === "function") {
					m_fn = regex;
				}
				else {
					if (!regex.global) return 0;
					m_fn = function (text, pos) {
						regex.lastIndex = pos;
						var m = regex.exec(text);
						if (m === null) return null;
						return [ m.index , m.index + m[0].length, m ];
					};
				}

				// Scanning function
				if (setup_function) {
					s_fn = setup_function;
					//s_fn = function (node, text) { setup_function(node, text); };
				}

				return deep_dom_wrap(comment, tag,
					m_fn,
					tag ? deep_dom_wrap_filter : deep_dom_wrap_filter_simple,
					s_fn,
					(!tag)
				);
			},
			post_get_comment_text: function (post_container) {
				var post_text = "",
					comment = this.post_get_comment_container(post_container);

				if (comment !== null) {
					deep_dom_wrap(comment, null,
						function (text) {
							post_text = text;
							return null;
						},
						deep_dom_wrap_filter_simple,
						null,
						true
					);
				}


				return post_text;
			},
			post_query_selector_all: function (post_container, selector) {
				var nodes = post_container.querySelectorAll(selector),
					other, other_len, i, first;

				if (nodes.length > 0 && (other_len = (other = post_container.querySelectorAll("div.inline,div.inlined")).length) > 0) {
					// Filter
					var filter = function (node) {
						while ((node = node.parentNode) !== post_container) {
							for (var i = 0; i < other_len; ++i) {
								if (other[i] === node) return true;
							}
						}
						return false;
					};

					// Filter out nodes
					first = true;
					for (i = 0; i < nodes.length; ++i) {
						if (filter(nodes[i])) {
							if (first) {
								first = false;
								nodes = Array.prototype.slice.call(nodes, 0);
							}
							nodes.splice(i, 1);
							continue;
						}
					}
				}

				return nodes;
			},
			post_is_floating_or_embedded: function (post_container) {
				var p = post_container.parentNode;
				if (!p) return false;

				return (
					style.has_class(p, "inline") || // 4chan x
					style.has_class(post_container, "inlined") || // vanilla
					p.id === "qp" || // 4chan x
					p === document.body || // vanilla
					p.id === "hoverUI" // 4chan x (?)
				);
			},

			get_quotelink_target: function (node) {
				return parseInt(node.textContent.replace(/[^0-9]+/g, ""), 10) || 0;
			},

			get_header_rect: function () {
				var header = document.getElementById(this.is_4chanx ? "header-bar" : "boardNavMobile");
				if (header) {
					return style.get_object_rect(header);
				}
				else {
					return {
						left: 0,
						top: 0,
						right: 0,
						bottom: 0,
					};
				}
			},

			observe_children: function (parent_node, cb) {
				// Create new observer
				var o = new MutationObserver(function (records) {
					var i, j, nodes;

					for (i = 0; i < records.length; ++i) {
						if ((nodes = records[i].addedNodes)) {
							for (j = 0; j < nodes.length; ++j) {
								// Check
								cb.call(null, true, nodes[j]);
							}
						}
						if ((nodes = records[i].removedNodes)) {
							for (j = 0; j < nodes.length; ++j) {
								// Check
								cb.call(null, false, nodes[j]);
							}
						}
					}
				});

				// Observe
				o.observe(
					parent_node,
					{
						childList: true,
					}
				);

				// Done
				return function () {
					o.disconnect();
				};
			},

			get_post_container_from_post_number: function (post_number) {
				return document.getElementById("pc" + post_number);
			},

			get_quick_reply_file_4chanx: function (callback) {
				// Event listener
				var temp_listener = function (event) {
					clearTimeout(timer);
					document.removeEventListener("QRFile", temp_listener, false);
					if (event.detail && event.detail instanceof File) {
						callback.call(event.detail);
					}
					else {
						callback.call(null);
					}
				};

				// Events
				document.addEventListener("QRFile", temp_listener, false);
				document.dispatchEvent(new CustomEvent("QRGetFile", {
					bubbles: true,
					detail: null
				}));

				// Cancel event
				var timer = setTimeout(function () {
					timer = null;
					document.removeEventListener("QRFile", temp_listener, false);
				}, 1000);
			},
			set_quick_reply_file_4chanx: function (file) {
				var detail = {
					file: this.files[0],
					name: null,
				};

				if (cloneInto) {
					detail = cloneInto(detail, document.defaultView);
				}

				document.dispatchEvent(new CustomEvent("QRSetFile", {
					bubbles: true,
					detail: detail
				}));
			},
		};



		return API;

	})();

	// Notification
	var Notification = (function () {

		var Notification = function (data) {
			this.nodes = null;
			this.overlay_can_close_on_click = ("overlay_close" in data && data.overlay_close);

			this.on_overlay_click_bind = null;
			this.on_body_click_bind = null;
			this.on_close_click_bind = null;

			this.events = {
				"close": []
			};

			var body = document.querySelector("body");
			if (body) {
				this.nodes = {
					container: null,
					body: null,
					close: null
				};

				// Create notification container
				var container, n_body_outer, n_aligner, n_body_inner, n_body, n_content_outer, n_content, n_close, n_style, nc_title, nc_body;
				container = $.div("iex_notification");
				container.addEventListener("click", this.on_overlay_click_bind = on_overlay_click.bind(this), false);
				this.nodes.container = container;

				n_body_outer = $.div("iex_notification_body_outer");
				container.appendChild(n_body_outer);

				n_aligner = $.div("iex_notification_body_aligner");
				n_body_outer.appendChild(n_aligner);

				n_body_inner = $.div("iex_notification_body_inner");
				n_body_outer.appendChild(n_body_inner);

				if ("style" in data && [ "info" , "error" , "success" , "warning" ].indexOf(data.style) >= 0) {
					n_style = "iex_notification_" + data.style;
				}
				else {
					n_style = "iex_notification_info";
				}

				n_body = $.div("iex_notification_body " + n_style);
				n_body.addEventListener("click", this.on_body_click_bind = on_body_click.bind(this), false);
				n_body_inner.appendChild(n_body);
				this.nodes.body = n_body;

				n_content_outer = $.div("iex_notification_content_outer");
				n_body.appendChild(n_content_outer);

				n_close = $.a("iex_notification_close" + (data.close === false ? " iex_notification_close_hidden" : ""));
				n_close.textContent = "\u00D7";
				n_close.addEventListener("click", this.on_close_click_bind = on_close_click.bind(this), false);
				this.nodes.close = n_close;
				n_content_outer.appendChild(n_close);

				n_content = $.div("iex_notification_content");
				n_content_outer.appendChild(n_content);

				// Title
				if ("title" in data) {
					nc_title = $.div("iex_notification_content_title");
					if (typeof(data.title) == typeof("")) {
						nc_title.textContent = data.title;
					}
					else {
						try {
							nc_title.appendChild(data.title);
						}
						catch (e) {}
					}
					n_content.appendChild(nc_title);
				}

				// Content
				if ("content" in data) {
					nc_body = $.div("iex_notification_content_body");
					if (typeof(data.content) == typeof("")) {
						nc_body.textContent = data.content;
					}
					else {
						try {
							nc_body.appendChild(data.content);
						}
						catch (e) {}
					}
					n_content.appendChild(nc_body);
				}

				// Insert
				body.appendChild(container);

				// Events
				if ("on" in data) {
					for (var event in data.on) {
						this.on(event, data.on[event]);
					}
				}
			}
		};



		var on_overlay_click = function (event) {
			// Close
			if (this.overlay_can_close_on_click) {
				destroy.call(this, "overlay");
			}

			// Stop
			return stop_event(event);
		};
		var on_body_click = function (event) {
			// Stop
			event.stopPropagation();
			return false;
		};
		var on_close_click = function (event) {
			// Remove
			destroy.call(this, "x");

			// Stop
			return stop_event(event);
		};

		var destroy = function (reason) {
			if (this.nodes) {
				// Close
				trigger.call(this, "close", {
					reason: reason
				});

				// Remove events
				if (this.on_close_click_bind !== null) {
					this.nodes.close.removeEventListener("click", this.on_close_click_bind, false);
					this.on_close_click_bind = null;
				}

				if (this.on_overlay_click_bind !== null) {
					this.nodes.container.removeEventListener("click", this.on_overlay_click_bind, false);
					this.on_overlay_click_bind = null;
				}

				if (this.on_body_click_bind !== null) {
					this.nodes.body.removeEventListener("click", this.on_body_click_bind, false);
					this.on_body_click_bind = null;
				}

				// Remove
				var par = this.nodes.container.parentNode;
				if (par) {
					par.removeChild(this.nodes.container);
				}

				// Null
				this.nodes = null;
				this.events = {};
			}
		};

		var trigger = function (event, data) {
			// Trigger
			if (event in this.events) {
				var e_list = this.events[event];
				for (var i = 0; i < e_list.length; ++i) {
					e_list[i].call(this, data);
				}
			}
		};



		Notification.prototype = {
			constructor: Notification,

			close: function () {
				// Close
				destroy.call(this, "manual");
			},

			on: function (event, callback) {
				// Add callback
				if (event in this.events) {
					this.events[event].push(callback);
				}
			},
			off: function (event, callback) {
				// Add callback
				if (event in this.events) {
					var e_list = this.events[event];
					for (var i = 0; i < e_list.length; ++i) {
						if (e_list[i] === callback) {
							// Remove
							e_list.splice(i, 1);
							return true;
						}
					}
				}

				return false;
			},

		};



		return Notification;

	})();

	// Settings control
	var Settings = (function () {

		var Settings = function () {
			// Settings values
			this.values = {
				"first_run": true,
				"image_expansion": {
					"enabled": false,
					"normal": {
						"enabled": true,
						"timeout": 0.0,
						"to_fit": true
					},
					"spoiler": {
						"enabled": true,
						"timeout": 0.0,
						"to_fit": true
					},
					"hover": {
						"zoom_invert": false,
						"zoom_borders_show": true,
						"zoom_borders_hide_time": 0.5,
						"zoom_buttons": true,
						"mouse_hide": true,
						"mouse_hide_time": 1.0,
						"header_overlap": false,
						"fit_large_allowed": true,
						"display_stats": 0
					},
					"extensions": {
						"jpg": {
							"background": 1, // 0 = never show, 1 = show before load (transparent), 2 = show before load (opaque)
							"mouse_wheel": 0 // 0 = zoom, 1 = volume control
						},
						"png": {
							"background": 1,
							"mouse_wheel": 0
						},
						"gif": {
							"background": 1,
							"mouse_wheel": 0
						},
						"webm": {
							"background": 2,
							"mouse_wheel": 0
						},
					},
					"video": {
						"autoplay": 1, // 0 = not at all, 1 = asap, 2 = when it can play "through", 3 = fully loaded
						"loop": true,
						"mute_initially": false,
						"volume": 0.5,
						"mini_controls": 1, // 0 = never, 1 = when mouse is over the video, 2 = when mouse is NOT over the video, 3 = always
						"expand_state_save": true
					},
					"style": {
						"animations_background": true,
						"controls_rounded_border": true
					}
				},
				"file_linkification": {
					"enabled": true,
				},
				"annotations": {
					"enabled": true,
					"enabled_standalone": true,
					"editor": true,
					"editor_always_enable": false,
					"modify_urls": true,
					"transparent_until_hover": false,
					"toggle_hotkey": "a",
					"defaults": {
						"font": 1,
						"bold": false,
						"italic": false,
					},
				},
			};
			this.save_key = "iex_settings";

			// Value changing events
			this.change_events = {};

			// Events
			this.ready_callbacks = [];

			// Notification instances
			this.notification_first_run = null;
			this.notification_install_fail = null;
			this.notification_image_hover = null;

			// Modifying other settings
			this.modifying_settings_4chanx_timer = null;
			this.modifying_settings_4chanx_callback = null;
			this.modifying_settings_vanilla_timer = null;
			this.modifying_settings_vanilla_callback = null;
			this.can_disable_image_hover = true; // If another image hover is detected

			// Data
			this.settings_container = null;
			this.settings_container_outer = null;
			this.settings_difficulty_container = null;
			this.settings_removal_data = null;
			this.settings_update_other_after_close = false;
		};



		var on_settings_4chanx_section_change = function (data) {
			// Get section id
			var id = /section-([a-zA-Z0-9_\-]+)/.exec(data.section.className);
			id = id ? id[1].toLowerCase() : "";

			// Hover settings
			var modify_image_hover = (id == (api.is_appchanx ? "script" : "main"));
			if (modify_image_hover) {

				// Forcibly modify 4chan-x settings
				if (this.modifying_settings_4chanx_timer !== null) {
					// Clear timer
					clearTimeout(this.modifying_settings_4chanx_timer);
					this.modifying_settings_4chanx_timer = null;

					// Get the hover setting
					var image_hover_setting = data.container.querySelector('input[name="Image Hover"]');
					if (image_hover_setting) {
						setTimeout(on_settings_4chanx_section_change_modify.bind(this, image_hover_setting), 100);
					}
					else {
						// Callback
						trigger_modifying_settings_4chanx_cb.call(this, false, false, "not found");
					}
				}

				// Modify display
				modify_settings_4chanx_display.call(this, data.container);

			}
		};
		var on_settings_4chanx_section_change_modify = function (setting) {
			// If it's checked, click it to uncheck it
			var is_checked = setting.checked;
			if (is_checked) {
				setting.click();
			}

			// Callback
			trigger_modifying_settings_4chanx_cb.call(this, true, is_checked, "okay");
			sync.trigger("image_expansion_enable");
		};
		var on_settings_vanilla_open = function (data) {
			// Forcibly modify 4chan-x settings
			if (this.modifying_settings_vanilla_timer !== null) {
				// Clear timer
				clearTimeout(this.modifying_settings_vanilla_timer);
				this.modifying_settings_vanilla_timer = null;

				// Get the hover setting
				var image_hover_setting = data.container.querySelector("input[data-option='imageHover']");
				if (image_hover_setting) {
					setTimeout(on_settings_vanilla_open_modify.bind(this, image_hover_setting), 10);
				}
				else {
					// Callback
					trigger_modifying_settings_vanilla_cb.call(this, false, false, "not found");
				}
			}

			// Modify display
			modify_settings_vanilla_display.call(this, data.container);
		};
		var on_settings_vanilla_open_modify = function (setting) {
			// If it's checked, click it to uncheck it
			var is_checked = setting.checked;
			if (is_checked) {
				setting.click();
			}

			// Callback
			trigger_modifying_settings_vanilla_cb.call(this, true, is_checked, "okay");
			sync.trigger("image_expansion_enable");
		};
		var on_modifying_settings_4chanx_timout = function () {
			// Callback
			trigger_modifying_settings_4chanx_cb.call(this, false, false, "timeout");

			// Didn't happen
			this.modifying_settings_4chanx_timer = null;
		};
		var on_modifying_settings_vanilla_timout = function () {
			// Callback
			trigger_modifying_settings_vanilla_cb.call(this, false, false, "timeout");

			// Didn't happen
			this.modifying_settings_vanilla_timer = null;
		};

		var on_menu_4chanx_open = function (data) {
			if (data.type != "main") return;

			var e = $.a("iex_4chanx_menu_link");
			e.textContent = "iex settings";
			e.addEventListener("click", on_menu_4chanx_entry_click.bind(this), false);

			api.menu_4chanx_add_entry(data.container, e, 135);
		};
		var on_menu_4chanx_entry_click = function (event) {
			// Close
			api.menu_4chanx_close();

			// Open settings
			this.settings_open();
		};

		var on_edit_iex_settings_click = function (event) {
			// Open settings
			if (api.is_4chanx) {
				api.settings_4chanx_close();
			}
			else {
				api.settings_vanilla_close();
			}
			this.settings_open();

			// Stop event
			return stop_event(event);
		};
		var on_main_page_iex_link_click = function (event) {
			// Open settings
			this.settings_open();

			// Stop event
			return stop_event(event);
		};

		var on_first_run_link_click = function (install, event) {
			// Selected
			this.change_value(["first_run"], false);
			this.save_values();

			// Install or not?
			if (install) {
				// Perform install
				var cb = on_first_run_install_callback.bind(this);
				if (api.is_4chanx) {
					this.modify_4chanx_settings(cb);
				}
				else {
					this.modify_vanilla_settings(cb);
				}
			}

			// Close
			if (this.notification_first_run !== null) {
				this.notification_first_run.close();
				this.notification_first_run = null;
			}

			// Stop event
			return stop_event(event);
		};
		var on_first_run_install_callback = function (okay, settings_were_changed, status) {
			// Update settings
			if (okay) {
				this.change_value(["first_run"], false);
				this.change_value(["image_expansion", "enabled"], true);
				this.save_values(on_first_run_install_callback_complete.bind(this, okay, settings_were_changed, status));
			}
			else {
				// Instant
				on_first_run_install_callback_complete.call(this, okay, settings_were_changed, status);
			}
		};
		var on_first_run_install_callback_complete = function (okay, settings_were_changed, status) {
			// Event
			if (okay) {
				sync.trigger("install_complete");
			}

			// Close settings
			if (api.is_4chanx) {
				// Close
				api.settings_4chanx_close();
			}
			else {
				// Close or save
				if (settings_were_changed) {
					api.settings_vanilla_save();
				}
				else {
					api.settings_vanilla_close();
				}
			}

			if (okay) {
				// Reload
				window.location.reload(false);
			}
			else {
				// Error
				this.display_install_fail_notification(status);
			}
		};
		var on_settings_update_other_complete = function (okay, settings_were_changed, status) {
			// Close settings
			if (api.is_4chanx) {
				// Close
				api.settings_4chanx_close();
			}
			else {
				// Close or save
				if (settings_were_changed) {
					api.settings_vanilla_save();
				}
				else {
					api.settings_vanilla_close();
				}
			}

			if (!okay) {
				// Error
				this.display_install_fail_notification(status);
			}
		};

		var on_initial_load = function () {
			on_ready(on_first_run_check.bind(this));
		};
		var on_first_run_check = function () {
			// First run check
			if (api.page_type == "board" || api.page_type == "thread" || api.page_type == "catalog") {
				if (this.values.first_run) {
					// Show message
					this.display_first_run_notification();
				}
			}

			// Trigger ready
			trigger_ready.call(this);
		};
		var on_insert_links = function () {
			var nav_nodes = [],
				nav, par, i, c, n, separate;

			if ((nav = document.getElementById("navtopright"))) {
				nav_nodes.push({
					node: nav,
					is_parent: true,
					add_separators: true,
					before: true,
				});
			}
			if ((nav = document.getElementById("navbotright"))) {
				nav_nodes.push({
					node: nav,
					is_parent: true,
					add_separators: true,
					before: true,
				});
			}
			if ((nav = document.getElementById("settingsWindowLinkMobile"))) {
				nav_nodes.push({
					node: nav,
					is_parent: false,
					add_separators: false,
					before: true,
				});
			}

			// Insert
			for (i = 0; i < nav_nodes.length; ++i) {
				par = nav_nodes[i].node;
				separate = nav_nodes[i].add_separators;

				if (nav_nodes[i].is_parent) {
					c = par.firstChild;
				}
				else {
					c = par;
					par = par.parentNode;
					if (!par) continue;
				}

				if (separate) {
					if (c && c.nodeType == 3) { // TEXT_NODE
						c.nodeValue = "] [";
					}
					else {
						n = $.text("]");
						if (c) par.insertBefore(n, c);
						else par.appendChild(n);

						c = n;
					}
				}

				n = $.node("a");
				n.textContent = "iex";
				n.setAttribute("target", "_blank");
				n.setAttribute("rel", "noreferrer nofollow");
				n.setAttribute("href", "https://dnsev.github.io/iex/");
				n.addEventListener("click", on_main_page_iex_link_click.bind(this), false);
				par.insertBefore(n, c);
				c = n;

				if (separate) {
					n = $.text("[");
					par.insertBefore(n, c);
				}
			}
		};
		var on_insert_links_condition = function () {
			return document.getElementById("navtopright") || document.getElementById("navbotright") || document.getElementById("settingsWindowLinkMobile");
		};

		var on_install_complete_sync = function () {
			// Close notifications
			if (this.notification_first_run !== null) {
				this.notification_first_run.close();
				this.notification_first_run = null;
			}
		};
		var on_settings_save_sync = function () {
			// Reload settings
			this.load_values(true, null);
		};
		var on_image_expansion_enable_sync = function () {
			// Image hover has been enabled in another tab; make it so this tab can't re-disable it
			this.can_disable_image_hover = false;
		};


		var on_notification_link_click = function (settings_open, close_object, event) {
			// Open
			if (settings_open) {
				this.settings_open();
			}

			// Close
			if (close_object !== null) {
				close_object.close();
			}

			// Stop event
			return stop_event(event);
		};
		var on_notification_first_run_close = function () {
			this.notification_first_run = null;
		};
		var on_notification_install_fail_close = function () {
			this.notification_install_fail = null;
		};
		var on_notification_image_hover_close = function () {
			this.notification_image_hover = null;
		};

		var on_iex_settings_overlay_click = function (event) {
			// Close
			this.settings_close();

			// Stop event
			return stop_event(event);
		};
		var on_iex_settings_body_click = function (event) {
			// Stop event
			event.stopPropagation();
			return false;
		};
		var on_iex_settings_close_click = function (event) {
			// Close
			this.settings_close();

			// Stop event
			return stop_event(event);
		};
		var on_iex_setting_checkbox_change = function (node, descriptor, event) {
			// Set
			var value_new = node.checked,
				value_old;

			if ("values" in descriptor) {
				value_new = descriptor.values[value_new ? 1 : 0];
			}
			if ("modify" in descriptor) {
				value_new = descriptor.modify.call(this, value_new, descriptor);
			}

			// Value tree
			if ("tree" in descriptor) {
				// Change
				value_old = this.change_value(descriptor.tree, value_new);
				this.save_values();
			}
			else if ("change" in descriptor) {
				// Modify
				value_new = descriptor.change.call(this, value_new, value_old);
			}

			// Update node
			node.checked = value_new;

			// After
			if ("after_callback" in descriptor) {
				descriptor.after_callback.call(this, value_new, value_old);
			}
		};
		var on_iex_setting_textbox_change = function (node, descriptor, event) {
			// Set
			var value_new = node.value,
				value_old;

			if ("modify" in descriptor) {
				value_new = descriptor.modify.call(this, value_new, descriptor);
			}

			// Value tree
			if ("tree" in descriptor) {
				// Change
				value_old = this.change_value(descriptor.tree, value_new);
				this.save_values();
			}
			else if ("change" in descriptor) {
				// Modify
				value_new = descriptor.change.call(this, value_new, value_old);
			}

			// Update node
			node.value = "" + value_new;

			// After
			if ("after_callback" in descriptor) {
				descriptor.after_callback.call(this, value_new, value_old);
			}
		};
		var on_iex_setting_text_click = function (node, descriptor, event) {
			// Get new id
			var id_current = parseInt(node.getAttribute("data-iex-setting-current-id") || "", 10) || 0;
			var id_new = (id_current + 1) % descriptor.values.length;
			var update_id = false;

			// Set
			var value_new = descriptor.values[id_new], value_old;
			if ("modify" in descriptor) {
				value_new = descriptor.modify.call(this, value_new, descriptor);
				update_id = true;
			}

			// Value tree
			if ("tree" in descriptor) {
				// Change
				value_old = this.change_value(descriptor.tree, value_new);
				this.save_values();
			}
			else if ("change" in descriptor) {
				// Modify
				value_new = descriptor.change.call(this, value_new, value_old);
				update_id = true;
			}

			// Update
			if (update_id) {
				var i = id_new;
				while (true) {
					i = (i + 1) % descriptor.values.length;
					if (i == id_new) break;

					if (descriptor.values[i] === value_new) {
						id_new = i;
						break;
					}
				}
			}
			node.setAttribute("data-iex-setting-current-id", id_new);
			node.textContent = ("value_labels" in descriptor ? descriptor.value_labels[id_new] : ("" + descriptor.values[id_new]));

			// After
			if ("after_callback" in descriptor) {
				descriptor.after_callback.call(this, value_new, value_old);
			}
		};
		var on_iex_setting_display_settings = function () {
			// Close settings
			this.settings_close();

			// Display
			this.display_settings_info_notification();
		};
		var on_iex_setting_delete_settings = function () {
			// Delete
			this.delete_values(on_iex_setting_delete_settings_complete.bind(this));
		};
		var on_iex_setting_delete_settings_complete = function () {
			// Reload
			this.settings_close();
			window.location.reload(false);
		};
		var on_iex_difficulty_link_click = function (event, node) {
			// Get target
			var target = node.getAttribute("data-iex-settings-difficulty-choice-level") || "";

			// Update difficulty
			change_settings_difficulty.call(this, target);

			// Stop event
			return stop_event(event);
		};
		var on_iex_setting_image_expansion_toggle = function (value_new, value_old) {
			// Update other settings after close
			this.settings_update_other_after_close = value_new;
		};

		var on_save_values_callback = function (next_callback) {
			// Sync
			sync.trigger("settings_save");

			if (next_callback) {
				next_callback.call(this);
			}
		};
		var on_load_values_callback = function (events, next_callback, value) {
			if (value) {
				update_values.call(this, [], this.values, value, false, events);
			}

			if (next_callback) {
				next_callback.call(this);
			}
		};
		var on_display_settings_info_notification_callback = function (saved_values) {
			// Get
			try {
				saved_values = JSON.stringify(saved_values, null, "    ");
			}
			catch (e) {}

			// Alert message
			var message = $.node("div"),
				m_part;

			// Line 1
			m_part = $.div("iex_spaced_div");
			m_part.textContent = "These are iex's saved settings, which are useful for debugging purposes:";
			message.appendChild(m_part);

			// Textarea
			m_part = $.node("textarea", "iex_notification_textarea");
			m_part.setAttribute("spellcheck", "false");
			m_part.value = saved_values;
			message.appendChild(m_part);

			// Display
			new Notification({
				close: true,
				overlay_close: true,
				style: "success",
				title: "Saved iex settings",
				content: message,
			});
		};

		var modify_settings_4chanx_display = function (container) {
			// Get the hover setting
			var image_hover_setting = container.querySelector('input[name="Image Hover"]');
			if (!image_hover_setting) return;

			// Find the parent to clone
			var image_hover_setting_container;
			try {
				image_hover_setting_container = image_hover_setting.parentNode.parentNode;
			}
			catch (e) {
				image_hover_setting_container = null;
			}
			if (image_hover_setting_container) {
				// Clone container
				var container_clone = image_hover_setting_container.cloneNode(true),
					i, description_old;

				// Remove input names and disable
				var inputs = container_clone.querySelectorAll("input");
				for (i = 0; i < inputs.length; ++i) {
					inputs[i].disabled = true;
					inputs[i].removeAttribute("name");
				}

				// Dim labels and descriptions
				var labels = container_clone.querySelectorAll("label");
				for (i = 0; i < labels.length; ++i) {
					labels[i].style.opacity = "0.5";
				}

				var descriptions = container_clone.querySelectorAll(".description");
				for (i = 0; i < descriptions.length; ++i) {
					descriptions[i].style.opacity = "0.5";
				}

				// Add new
				if (api.is_appchanx) {
					container_clone.appendChild($.text(" (disable iex)"));

					// Modify old
					description_old = image_hover_setting_container.querySelector("label");
					if (description_old) {
						description_old.appendChild($.text(" (or use iex)"));
					}
				}
				else {
					var description_new = $.span("description");

					var link_new = $("a");
					link_new.setAttribute("href", "//dnsev.github.io/iex/");
					link_new.setAttribute("target", "_blank");
					link_new.textContent = "disable iex version to use";
					link_new.addEventListener("click", on_edit_iex_settings_click.bind(this), false);

					description_new.appendChild($.text(" ("));
					description_new.appendChild(link_new);
					description_new.appendChild($.text(")"));

					container_clone.appendChild(description_new);

					// Modify old
					description_old = image_hover_setting_container.querySelector(".description");
					if (description_old) {
						link_new = $("a");
						link_new.setAttribute("href", "//dnsev.github.io/iex/");
						link_new.setAttribute("target", "_blank");
						link_new.textContent = "or use iex version";
						link_new.addEventListener("click", on_edit_iex_settings_click.bind(this), false);

						description_old.appendChild($.text(" ("));
						description_old.appendChild(link_new);
						description_old.appendChild($.text(")"));
					}
				}

				// Insert new container
				image_hover_setting_container.parentNode.insertBefore(container_clone, image_hover_setting_container);

				// Hide old
				if (this.values.image_expansion.enabled) {
					image_hover_setting_container.style.display = "none";
				}
				else {
					container_clone.style.display = "none";
				}

				// Update checkbox value
				setTimeout(modify_settings_4chanx_display_delay.bind(this, inputs, image_hover_setting), 10);
			}
		};
		var modify_settings_4chanx_display_delay = function (inputs, original) {
			for (var i = 0; i < inputs.length; ++i) {
				inputs[i].checked = original.checked;
			}
		};
		var modify_settings_vanilla_display = function (container) {
			// Get the hover setting
			var image_hover_setting = container.querySelector("input[data-option='imageHover']");
			if (!image_hover_setting) return;

			// Acquire nodes to clone
			var c, containers_original = [],
				clone, inputs, par, i, j, n, iex_link, iex_descr;

			try {
				c = image_hover_setting.parentNode.parentNode;
				if (c) {
					if ((par = c.parentNode)) {
						containers_original.push(c);
					}
					else {
						c = null;
					}
				}
			}
			catch (e) {
				c = null;
			}
			if (c) {
				c = c.nextSibling;
				if (style.has_class(c, "settings-tip")) containers_original.push(c);
			}

			for (i = 0; i < containers_original.length; ++i) {
				c = containers_original[i];

				// Clone container
				clone = c.cloneNode(true);

				// Disable inputs
				inputs = clone.querySelectorAll("input");
				for (j = 0; j < inputs.length; ++j) {
					inputs[j].disabled = true;
					inputs[j].removeAttribute("data-option");
				}

				if (i === 0) {
					// Dim
					if ((n = clone.querySelector("label"))) {
						n.style.opacity = "0.5";
					}

					// Text for new
					iex_link = $("a");
					iex_link.setAttribute("href", "//dnsev.github.io/iex/");
					iex_link.setAttribute("target", "_blank");
					iex_link.textContent = "disable iex version to use";
					iex_link.addEventListener("click", on_edit_iex_settings_click.bind(this), false);

					iex_descr = $("span");
					iex_descr.appendChild($.text(" ("));
					iex_descr.appendChild(iex_link);
					iex_descr.appendChild($.text(")"));

					clone.appendChild(iex_descr);

					// Text for old
					iex_link = $("a");
					iex_link.setAttribute("href", "//dnsev.github.io/iex/");
					iex_link.setAttribute("target", "_blank");
					iex_link.textContent = "or use iex version";
					iex_link.addEventListener("click", on_edit_iex_settings_click.bind(this), false);

					iex_descr = $("span");
					iex_descr.appendChild($.text(" ("));
					iex_descr.appendChild(iex_link);
					iex_descr.appendChild($.text(")"));
					c.appendChild(iex_descr);
				}
				else {
					// Dim
					clone.style.opacity = "0.5";
				}

				// Hide
				if (this.values.image_expansion.enabled) {
					c.style.display = "none";
				}
				else {
					clone.style.display = "none";
				}

				// Append
				par.insertBefore(clone, containers_original[0]);

				// Update checkbox value
				setTimeout(modify_settings_vanilla_display_delay.bind(this, inputs, image_hover_setting), 10);
			}
		};
		var modify_settings_vanilla_display_delay = function (inputs, original) {
			for (var i = 0; i < inputs.length; ++i) {
				inputs[i].checked = original.checked;
			}
		};

		var trigger_ready = function () {
			// Trigger ready event
			if (this.ready_callbacks !== null) {
				for (var i = 0; i < this.ready_callbacks.length; ++i) {
					this.ready_callbacks[i].call(this);
				}
				this.ready_callbacks = null;
			}
		};
		var trigger_change = function (tree, old_value, new_value, source) {
			var key = tree.join("-");
			if (key in this.change_events) {
				// Get the proper tree
				tree = this.change_events[key].tree;

				// Callback list
				var list = this.change_events[key].callbacks;
				for (var i = 0; i < list.length; ++i) {
					list[i].call(this, {
						tree: tree,
						old_value: old_value,
						new_value: new_value,
						source: source,
					});
				}
			}
		};
		var trigger_modifying_settings_4chanx_cb = function (okay, settings_were_changed, status) {
			// Show
			var body = document.querySelector("body");
			if (body) style.remove_class(body, "iex_hide_other_settings");

			// Callback
			if (this.modifying_settings_4chanx_callback !== null) {
				this.modifying_settings_4chanx_callback.call(this, okay, settings_were_changed, status);
				this.modifying_settings_4chanx_callback = null;
			}
		};
		var trigger_modifying_settings_vanilla_cb = function (okay, settings_were_changed, status) {
			// Show
			var body = document.querySelector("body");
			if (body) style.remove_class(body, "iex_hide_other_settings");

			// Callback
			if (this.modifying_settings_vanilla_callback !== null) {
				this.modifying_settings_vanilla_callback.call(this, okay, settings_were_changed, status);
				this.modifying_settings_vanilla_callback = null;
			}
		};

		var update_values = function (tree, values, new_values, remove, events) {
			// Updates
			var old_value, key;

			for (key in new_values) {
				if (key in values) {
					// Update
					if (new_values[key] instanceof Object) {
						// Deeper
						if (!(values[key] instanceof Object)) {
							values[key] = {};
						}

						tree.push(key);
						update_values.call(this, tree, values[key], new_values[key], remove, events);
						tree.pop();
					}
					else {
						// Replace
						old_value = values[key];
						values[key] = new_values[key];

						if (events && old_value !== values[key]) {
							tree.push(key);
							trigger_change.call(this, tree, old_value, values[key], "update");
							tree.pop();
						}
					}
				}
				else {
					// New
					if (new_values[key] instanceof Object) {
						// Deeper
						values[key] = {};

						tree.push(key);
						update_values.call(this, tree, values[key], new_values[key], remove, events);
						tree.pop();
					}
					else {
						// Replace
						values[key] = new_values[key];

						if (events) {
							tree.push(key);
							trigger_change.call(this, tree, undefined, values[key], "update");
							tree.pop();
						}
					}
				}
			}

			// Removals
			if (remove) {
				for (key in values) {
					if (!(key in new_values)) {
						// Remove
						old_value = values[key];
						delete values[key];

						if (events) {
							tree.push(key);
							trigger_change.call(this, tree, old_value, undefined, "update");
							tree.pop();
						}
					}
				}
			}
		};

		var string_to_float = function (value) {
			return parseFloat(value.trim()) || 0;
		};
		var string_to_hotkey = function (value, descriptor) {
			value = value.trim().toLowerCase();
			if (hotkey_manager.key_to_keycode(value) === 0) {
				// Return previous
				return this.get_value(descriptor.tree);
			}
			return value;
		};

		var settings_open = function () {
			if (this.settings_container_outer !== null) return;

			var body = document.querySelector("body"),
				bg_color, settings_content, settings_buttons, cb,
				overlay, container, inner, n1, n2, n3, n4;

			if (!body) return;

			// Get background color
			bg_color = style.parse_css_color(style.get_true_style_of_class("post reply", "backgroundColor", "div"));

			settings_buttons = generate_settings_difficulty_selector.call(this);
			settings_content = generate_settings_container.call(this, generate_settings_descriptors.call(this));
			this.settings_removal_data = settings_content[1].concat(settings_buttons[1]);


			// Create
			overlay = $.div("iex_settings_popup_overlay");
			overlay.addEventListener("click", (cb = on_iex_settings_overlay_click.bind(this)), false);
			this.settings_removal_data.push({
				node: overlay,
				event: "click",
				callback: cb,
				capture: false
			});
			this.settings_container_outer = overlay;

			container = $.div("iex_settings_popup");
			overlay.appendChild(container);

			inner = $.div("iex_settings_popup_aligner");
			container.appendChild(inner);

			inner = $.div("iex_settings_popup_inner");
			container.appendChild(inner);

			n1 = $.div("iex_settings_popup_table");
			n1.style.backgroundColor = style.color_to_css(bg_color);
			inner.appendChild(n1);
			n1.addEventListener("click", (cb = on_iex_settings_body_click.bind(this)), false);
			this.settings_removal_data.push({
				node: n1,
				event: "click",
				callback: cb,
				capture: false
			});


			// Top
			n2 = $.div("iex_settings_popup_top");
			n1.appendChild(n2);

			n3 = $.div("iex_settings_popup_top_content");
			n2.appendChild(n3);

			n4 = $.div("iex_settings_popup_top_label");
			n3.appendChild(n4);
			n4.textContent = "Image Extensions - Settings";

			settings_buttons[0].className = "iex_settings_popup_top_right" + style.theme;
			n3.appendChild(settings_buttons[0]);
			this.settings_difficulty_container = settings_buttons[0];


			// Middle
			n2 = $.div("iex_settings_popup_middle");
			n1.appendChild(n2);

			n3 = $.div("iex_settings_popup_middle_content_pad");
			n2.appendChild(n3);
			n2 = n3;

			n3 = $.div("iex_settings_popup_middle_content");
			n2.appendChild(n3);
			n2 = n3;

			n3 = $.div("iex_settings_popup_middle_content_inner");
			n2.appendChild(n3);
			n3.appendChild(settings_content[0]);
			this.settings_container = settings_content[0];


			// Bottom
			n2 = $.div("iex_settings_popup_bottom");
			n1.appendChild(n2);



			// Update difficulty
			change_settings_difficulty.call(this, "normal");


			// Append
			body.appendChild(overlay);
		};

		var generate_settings_difficulty_selector = function () {
			var d_container, d_choice, d_span, cb,
				settings_removal_data = [];

			d_container = $("div");

			// Normal
			d_choice = $.a("iex_settings_difficulty_choice");
			d_choice.textContent = "normal";
			d_choice.setAttribute("data-iex-settings-difficulty-choice-level", "normal");
			d_container.appendChild(d_choice);
			cb = wrap_generic_event(this, on_iex_difficulty_link_click);
			d_choice.addEventListener("click", cb, false);
			settings_removal_data.push({
				node: d_choice,
				event: "click",
				callback: cb,
				capture: false
			});

			d_span = $.span("iex_settings_difficulty_separator");
			d_span.textContent = " | ";
			d_container.appendChild(d_span);

			// Advanced
			d_choice = $.a("iex_settings_difficulty_choice");
			d_choice.textContent = "advanced";
			d_choice.setAttribute("data-iex-settings-difficulty-choice-level", "advanced");
			d_container.appendChild(d_choice);
			d_choice.addEventListener("click", cb, false);
			settings_removal_data.push({
				node: d_choice,
				event: "click",
				callback: cb,
				capture: false
			});

			d_span = $.span("iex_settings_difficulty_separator");
			d_span.textContent = " | ";
			d_container.appendChild(d_span);

			// Homepage
			d_choice = $.a("iex_settings_homepage_link");
			d_choice.setAttribute("target", "_blank");
			d_choice.setAttribute("href", "//dnsev.github.io/iex/");
			d_choice.textContent = "homepage";
			d_container.appendChild(d_choice);

			d_span = $.span("iex_settings_difficulty_separator");
			d_span.textContent = " | ";
			d_container.appendChild(d_span);

			// Close
			d_choice = $.a("iex_settings_popup_close_link");
			d_choice.setAttribute("target", "_blank");
			d_choice.setAttribute("href", "//dnsev.github.io/iex/");
			d_choice.textContent = "\u00D7";
			d_container.appendChild(d_choice);
			d_choice.addEventListener("click", (cb = on_iex_settings_close_click.bind(this)), false);
			settings_removal_data.push({
				node: d_choice,
				event: "click",
				callback: cb,
				capture: false
			});


			// Return
			return [ d_container , settings_removal_data ];
		};
		var generate_settings_descriptors = function () {
			var descriptors = [];

			// Image expansion
			descriptors.push({
				level: "normal",
				section: "Image Expansion",
				tree: [ "image_expansion" , "enabled" ],
				label: "Enabled",
				description: "Enable custom expansion of images on mouse hover",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "disabled" , "enabled" ],
				descriptors: [],
				after_callback: on_iex_setting_image_expansion_toggle.bind(this)
			});

			descriptors.push({
				level: "normal",
				section: "Image Expansion",
				tree: [ "image_expansion" , "normal" , "enabled" ],
				label: "Enabled",
				sublabel: "for non-spoiler images",
				description: "Enable hover for non-spoiler images",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "disabled" , "enabled" ],
			});

			descriptors.push({
				level: "normal",
				section: "Image Expansion",
				tree: [ "image_expansion" , "spoiler" , "enabled" ],
				label: "Enabled",
				sublabel: "for spoiler images",
				description: "Enable hover for spoiler images",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "disabled" , "enabled" ],
			});

			descriptors.push({
				level: "normal",
				section: "Image Expansion",
				tree: [ "image_expansion" , "normal" , "to_fit" ],
				label: "Fit image",
				sublabel: "for non-spoiler images",
				description: "Automatically fit the image to the best screen size on hover",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "don't fit" , "fit" ],
			});

			descriptors.push({
				level: "normal",
				section: "Image Expansion",
				tree: [ "image_expansion" , "spoiler" , "to_fit" ],
				label: "Fit image",
				sublabel: "for spoiler images",
				description: "Automatically fit the image to the best screen size on hover",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "don't fit" , "fit" ],
			});

			descriptors.push({
				level: "normal",
				section: "Image Expansion",
				tree: [ "image_expansion" , "normal" , "timeout" ],
				modify: string_to_float,
				label: "Open timeout",
				sublabel: "for non-spoiler images",
				description: "Time to wait before displaying the image (in seconds)",
				type: "textbox",
			});

			descriptors.push({
				level: "normal",
				section: "Image Expansion",
				tree: [ "image_expansion" , "spoiler" , "timeout" ],
				modify: string_to_float,
				label: "Open timeout",
				sublabel: "for spoiler images",
				description: "Time to wait before displaying the image (in seconds)",
				type: "textbox",
			});

			// Hover settings
			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "hover" , "header_overlap" ],
				label: "Overlap header",
				description: "If the header is visible, the preview will not overlap it",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "don't overlap" , "overlap" ],
			});

			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "hover" , "fit_large_allowed" ],
				label: "Fit large",
				description: "When enabled, image zooming can be snapped to both vertical and horizontal scales",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "disabled" , "enabled" ],
			});

			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "hover" , "display_stats" ],
				label: "Display stats",
				description: "Show the file name and relevant stats on top of the preview",
				type: "text",
				values: [ 0 , 1 , 2 ],
				value_labels: [ "show" , "hide" , "hide all" ]
			});

			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "hover" , "zoom_invert" ],
				label: "Zoom invert mouse",
				description: "Zoom location based on mouse will be inverted",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "not inverted" , "inverted" ],
			});

			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "hover" , "zoom_borders_show" ],
				label: "Zoom borders",
				description: "Display borders inside the preview displaying the mouse movement region",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "disabled" , "enabled" ],
			});

			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "hover" , "zoom_borders_hide_time" ],
				modify: string_to_float,
				label: "Zoom borders hide timeout",
				description: "Time to wait after mouse has stopped to hide the zoom borders (in seconds)",
				type: "textbox",
			});

			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "hover" , "zoom_buttons" ],
				label: "Zoom buttons",
				description: "Show zooming buttons when the zoom% is hovered",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "disabled" , "enabled" ],
			});

			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "hover" , "mouse_hide" ],
				label: "Cursor hide",
				description: "Hide the mouse cursor when hovering the image after inactivity",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "don't hide" , "hide" ],
			});

			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "hover" , "mouse_hide_time" ],
				modify: string_to_float,
				label: "Cursor hide timeout",
				description: "Time to wait after mouse has stopped to hide cursor (in seconds)",
				type: "textbox",
			});

			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "hover" , "mouse_hide_time" ],
				modify: string_to_float,
				label: "Cursor hide timeout",
				description: "Time to wait after mouse has stopped to hide cursor (in seconds)",
				type: "textbox",
			});

			// File types
			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "extensions" , "jpg" , "background" ],
				label: "Thumbnail background (.jpg)",
				description: "How to display the thumbnail image in the background while loading",
				type: "text",
				values: [ 0 , 1 , 2 ],
				value_labels: [ "never show" , "semi-transparent" , "opaque" ]
			});

			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "extensions" , "png" , "background" ],
				label: "Thumbnail background (.png)",
				description: "How to display the thumbnail image in the background while loading",
				type: "text",
				values: [ 0 , 1 , 2 ],
				value_labels: [ "never show" , "semi-transparent" , "opaque" ]
			});

			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "extensions" , "gif" , "background" ],
				label: "Thumbnail background (.gif)",
				description: "How to display the thumbnail image in the background while loading",
				type: "text",
				values: [ 0 , 1 , 2 ],
				value_labels: [ "never show" , "semi-transparent" , "opaque" ]
			});

			descriptors.push({
				level: "normal",
				section: "Hover",
				tree: [ "image_expansion" , "extensions" , "webm" , "background" ],
				label: "Thumbnail background (.webm)",
				description: "How to display the thumbnail image in the background while loading",
				type: "text",
				values: [ 0 , 1 , 2 ],
				value_labels: [ "never show" , "transparent" , "opaque" ]
			});

			// Video settings
			descriptors.push({
				level: "normal",
				section: "Video",
				tree: [ "image_expansion" , "video" , "mini_controls" ],
				label: "Mini controls",
				description: "When to display the mini control bar",
				type: "text",
				values: [ 0 , 1 , 2 , 3 ],
				value_labels: [ "never" , "mouse over preview" , "mouse NOT over preview" , "always" ]
			});

			descriptors.push({
				level: "normal",
				section: "Video",
				tree: [ "image_expansion" , "video" , "autoplay" ],
				label: "Autoplay",
				description: "When the video should automatically play",
				type: "text",
				values: [ 0 , 1 , 2 , 3 ],
				value_labels: [ "never" , "as soon as possible" , "when it can play \"through\"" , "when fully loaded" ]
			});

			descriptors.push({
				level: "normal",
				section: "Video",
				tree: [ "image_expansion" , "video" , "loop" ],
				label: "Loop",
				description: "Enable automatic video looping",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "don't loop" , "loop" ],
			});

			descriptors.push({
				level: "normal",
				section: "Video",
				tree: [ "image_expansion" , "video" , "mute_initially" ],
				label: "Mute",
				description: "Mute the video when the preview is opened",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "don't mute" , "mute" ],
			});

			descriptors.push({
				level: "normal",
				section: "Video",
				tree: [ "image_expansion" , "video" , "volume" ],
				modify: string_to_float,
				label: "Default volume",
				description: "The default volume of a video when it is opened",
				type: "textbox",
			});

			descriptors.push({
				level: "normal",
				section: "Video",
				tree: [ "image_expansion" , "video" , "expand_state_save" ],
				label: "Expansion state save",
				description: "The video state should be saved and resumed when expanding",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "don't save" , "save" ],
			});

			descriptors.push({
				level: "normal",
				section: "Video",
				tree: [ "image_expansion" , "extensions" , "webm" , "mouse_wheel" ],
				label: "Mouse wheel action (.webm)",
				description: "What scrolling the mouse should do for .webm files",
				type: "text",
				values: [ 0 , 1 ],
				value_labels: [ "zoom" , "volume control" ]
			});

			// Link settings
			descriptors.push({
				level: "normal",
				section: "Linkification",
				tree: [ "file_linkification" , "enabled" ],
				label: "Named file URLs",
				description: "Appends the filename to the url as a #fragment (asthetic purposes)",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "disabled" , "enabled" ],
			});

			// Style settings
			descriptors.push({
				level: "normal",
				section: "Style",
				tree: [ "image_expansion" , "style" , "animations_background" ],
				label: "Background image animations",
				description: "Use animated CSS transitions for the background image",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "unanimated" , "animated" ],
			});

			descriptors.push({
				level: "normal",
				section: "Style",
				tree: [ "image_expansion" , "style" , "controls_rounded_border" ],
				label: "Rounded borders on controls",
				description: "Use rounded corners on video control bars (doesn't work on Chrome)",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "square" , "rounded" ],
			});
			// Annotations
			descriptors.push({
				level: "normal",
				section: "Annotations",
				tree: [ "annotations" , "enabled" ],
				label: "Enabled",
				description: "Enable image annotations",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "disabled" , "enabled" ],
			});

			descriptors.push({
				level: "normal",
				section: "Annotations",
				tree: [ "annotations" , "enabled_standalone" ],
				label: "Enabled on standalone images",
				description: "Enable image annotations on images opened in a new tab",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "disabled" , "enabled" ],
			});

			descriptors.push({
				level: "normal",
				section: "Annotations",
				tree: [ "annotations" , "editor" ],
				label: "Annotation editor",
				description: "Show the annotation editor option on the quick reply form",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "disabled" , "enabled" ],
			});

			descriptors.push({
				level: "normal",
				section: "Annotations",
				tree: [ "annotations" , "editor_always_enable" ],
				label: "Annotation editor always enabled",
				description: "Always enable the annotation editor when opening quick reply",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "disabled" , "enabled" ],
			});

			descriptors.push({
				level: "normal",
				section: "Annotations",
				tree: [ "annotations" , "modify_urls" ],
				label: "Modify image URLs",
				description: "Append a #fragment to image urls to allow standalone image annotations",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "disabled" , "enabled" ],
			});

			descriptors.push({
				level: "normal",
				section: "Annotations",
				tree: [ "annotations" , "transparent_until_hover" ],
				label: "Annotation transparency",
				description: "Make annotations more transparent when the mouse is not over them",
				type: "checkbox",
				values: [ false , true ],
				value_labels: [ "regular" , "transparent" ],
			});

			descriptors.push({
				level: "normal",
				section: "Annotations",
				tree: [ "annotations" , "toggle_hotkey" ],
				modify: string_to_hotkey,
				label: "Toggle Hotkey",
				description: "The key to use for toggling annotations on/off",
				type: "textbox",
			});

			var vals = [],
				val_labels = [],
				i;

			for (i = 0; i < Annotation.fonts.length; ++i) {
				vals.push(i);
				val_labels.push(Annotation.fonts[i].name);
			}

			descriptors.push({
				level: "normal",
				section: "Annotation Editor Defaults",
				tree: [ "annotations" , "defaults" , "font" ],
				label: "Default font",
				description: "Default font family for new annotations",
				type: "text",
				values: vals,
				value_labels: val_labels
			});

			descriptors.push({
				level: "normal",
				section: "Annotation Editor Defaults",
				tree: [ "annotations" , "defaults" , "bold" ],
				label: "Default bold",
				description: "Default bold setting for new annotations",
				type: "text",
				values: [ false , true ],
				value_labels: [ "not bold" , "bold" ],
			});

			descriptors.push({
				level: "normal",
				section: "Annotation Editor Defaults",
				tree: [ "annotations" , "defaults" , "italic" ],
				label: "Default italic",
				description: "Default italic setting for new annotations",
				type: "text",
				values: [ false , true ],
				value_labels: [ "not italic" , "italic" ],
			});

			// Meta
			descriptors.push({
				level: "advanced",
				section: "Debugging",
				change: on_iex_setting_display_settings.bind(this),
				label: "Display local settings",
				description: "Display all saved local data in a pop-up textbox",
				type: "text",
				values: [ true ],
				value_labels: [ "display" ]
			});

			descriptors.push({
				level: "advanced",
				section: "Debugging",
				change: on_iex_setting_delete_settings.bind(this),
				label: "Delete local settings",
				description: "Delete all saved local data and refresh the page",
				type: "text",
				values: [ true ],
				value_labels: [ "delete" ]
			});

			// Return
			return descriptors;
		};
		var generate_settings_container = function (descriptors, data) {
			// Create container
			var container = $.div("iex_settings_region");

			// Groups
			data = data || [];
			var groups = {};
			var group_count = 0;

			// Setup
			var d, i, j, g_key, g_container, g_label, g_set, g_span, g_div, g_table, g_half_left, g_half_right, g_input, g_input_label, cb, val, s_val, odd = true;
			for (i = 0; i < descriptors.length; ++i) {
				d = descriptors[i];

				// Get group
				g_key = d.section || "";
				if (g_key in groups) {
					g_container = groups[g_key];
				}
				else {
					// New
					g_container = $.div("iex_settings_group" + (group_count > 0 ? " iex_settings_group_padding_top" : ""));
					container.appendChild(g_container);

					g_label = $.div("iex_settings_group_label");
					g_label.textContent = g_key;
					g_container.appendChild(g_label);

					groups[g_key] = g_container;
					group_count += 1;
					odd = true;
				}

				// Add setting
				g_set = $.div("iex_settings_setting" + (odd ? " iex_settings_setting_odd" : "") + (i > 0 ? " iex_settings_setting_top_padding" : ""));
				g_set.setAttribute("data-iex-setting-level", d.level || "normal");
				g_container.appendChild(g_set);

				g_table = $.div("iex_settings_setting_table");
				g_set.appendChild(g_table);

				// Halves
				g_half_right = $.div("iex_settings_setting_right");
				g_table.appendChild(g_half_right);

				g_half_left = $.div("iex_settings_setting_left");
				g_table.appendChild(g_half_left);

				// Left half
				g_label = $.div("iex_settings_setting_label");
				g_half_left.appendChild(g_label);

				g_span = $.span("iex_settings_setting_label_title");
				g_span.textContent = d.label;
				g_label.appendChild(g_span);

				if ("sublabel" in d) {
					g_span = $.span("iex_settings_setting_label_subtitle");
					g_span.textContent = d.sublabel;
					g_label.appendChild(g_span);
				}

				g_label = $.div("iex_settings_setting_description");
				g_label.textContent = d.description;
				g_half_left.appendChild(g_label);

				// Right half: input
				g_div = $.label("iex_settings_setting_input_container");
				g_half_right.appendChild(g_div);

				if (d.type == "checkbox") {
					g_div.className += " iex_settings_setting_input_container_reverse";

					g_input = $.input.check("iex_settings_setting_input_checkbox");
					g_div.appendChild(g_input);

					g_input_label = $.span("iex_settings_setting_input_label");
					if ("value_labels" in d) {
						g_input_label.setAttribute("data-iex-checkbox-label-off", d.value_labels[0]);
						g_input_label.setAttribute("data-iex-checkbox-label-on", d.value_labels[1]);
					}
					else if ("values" in d) {
						g_input_label.setAttribute("data-iex-checkbox-label-off", "" + d.values[0]);
						g_input_label.setAttribute("data-iex-checkbox-label-on", "" + d.values[1]);
					}
					else {
						g_input_label.setAttribute("data-iex-checkbox-label-off", "off");
						g_input_label.setAttribute("data-iex-checkbox-label-on", "on");
					}
					g_div.appendChild(g_input_label);

					// Events
					cb = on_iex_setting_checkbox_change.bind(this, g_input, d);
					g_input.addEventListener("change", cb, false);
					data.push({
						node: g_input,
						event: "change",
						callback: cb,
						capture: false
					});

					// Check status
					if ("tree" in d) {
						val = this.get_value(d.tree);
						if ("values" in d) {
							g_input.checked = (val == d.values[1]);
						}
						else {
							g_input.checked = val;
						}
					}
				}
				else if (d.type == "textbox") {
					g_input = $.input.text("iex_settings_setting_input_textbox");
					g_div.appendChild(g_input);

					// Events
					cb = on_iex_setting_textbox_change.bind(this, g_input, d);
					g_input.addEventListener("change", cb, false);
					data.push({
						node: g_input,
						event: "change",
						callback: cb,
						capture: false
					});

					// Text
					if ("tree" in d) {
						g_input.value = this.get_value(d.tree).toString();
					}
				}
				else if (d.type == "text") {
					// Get current
					val = 0;
					if ("tree" in d) {
						s_val = this.get_value(d.tree);
						for (j = 0; j < d.values.length; ++j) {
							if (d.values[j] == s_val) {
								val = j;
								break;
							}
						}
					}
					else if ("get" in d) {
						val = d.get.call(this, d);
					}

					// Text
					g_input = $.a("iex_settings_setting_input_text");
					g_input.textContent = ("value_labels" in d ? d.value_labels[val] : ("" + d.values[val]));
					g_input.setAttribute("data-iex-setting-current-id", val);
					g_div.appendChild(g_input);

					// Events
					cb = on_iex_setting_text_click.bind(this, g_input, d);
					g_input.addEventListener("click", cb, false);
					data.push({
						node: g_input,
						event: "click",
						callback: cb,
						capture: false
					});
				}

				// Sub-options
				if ("descriptors" in d) {
					g_div = $.div("iex_settings_region_container");
					g_set.appendChild(g_div);

					g_div.appendChild(generate_settings_container.call(this, d.descriptors, data)[0]);
				}

				// Next
				odd = !odd;
			}

			// Return
			return [ container , data ];
		};
		var destroy_settings_data = function (data) {
			// Remove events
			for (var i = 0; i < data.length; ++i) {
				data[i].node.removeEventListener(data[i].event, data[i].callback, data[i].capture);
			}
		};
		var change_settings_difficulty = function (target) {
			if (this.settings_container === null) return;

			// Setup
			var groups = this.settings_container.querySelectorAll(".iex_settings_group"),
				levels = [ "normal" , "advanced" ],
				level_target = levels.indexOf(target),
				settings, s, s_pre, i, j, odd, count, level, choices;

			// Modify settings
			for (i = 0; i < groups.length; ++i) {
				settings = groups[i].querySelectorAll(".iex_settings_setting");
				odd = true;
				s_pre = null;
				count = 0;

				for (j = 0; j < settings.length; ++j) {
					s = settings[j];
					level = levels.indexOf(s.getAttribute("data-iex-setting-level"));

					// Display
					if (level <= level_target) {
						// Un-hide
						style.remove_class(s, "iex_settings_setting_hidden");
						++count;

						// Oddity
						if (odd) {
							style.add_class(s, "iex_settings_setting_odd");
						}
						else {
							style.remove_class(s, "iex_settings_setting_odd");
						}
						odd = !odd;

						// Previous
						if (s_pre !== null) {
							style.add_class(s, "iex_settings_setting_top_padding");
						}
						else {
							style.remove_class(s, "iex_settings_setting_top_padding");
						}
						s_pre = s;
					}
					else {
						// Hide
						style.add_class(s, "iex_settings_setting_hidden");
					}
				}

				if (count === 0) {
					style.add_class(groups[i], "iex_settings_group_hidden");
				}
				else {
					style.remove_class(groups[i], "iex_settings_group_hidden");
				}
			}

			// Modify difficulty
			choices = this.settings_difficulty_container.querySelectorAll(".iex_settings_difficulty_choice");
			for (i = 0; i < choices.length; ++i) {
				level = choices[i].getAttribute("data-iex-settings-difficulty-choice-level");
				if (level == target) {
					style.add_class(choices[i], "iex_settings_difficulty_choice_selected");
				}
				else {
					style.remove_class(choices[i], "iex_settings_difficulty_choice_selected");
				}
			}
		};



		Settings.prototype = {
			constructor: Settings,

			setup: function () {
				// Events
				sync.on("install_complete", this.on_install_complete_sync_bind = on_install_complete_sync.bind(this));
				sync.on("settings_save", this.on_settings_save_sync_bind = on_settings_save_sync.bind(this));
				sync.on("image_expansion_enable", this.on_image_expansion_enable_sync_bind = on_image_expansion_enable_sync.bind(this));

				// Modify other settings
				api.on("settings_4chanx_section_change", this.on_settings_4chanx_section_change_bind = on_settings_4chanx_section_change.bind(this));
				api.on("settings_vanilla_open", this.on_settings_vanilla_open_bind = on_settings_vanilla_open.bind(this));
				api.on("menu_4chanx_open", this.on_menu_4chanx_open_bind = on_menu_4chanx_open.bind(this));

				// First load
				this.load_values(false, on_initial_load.bind(this));
				on_ready(on_insert_links.bind(this), on_insert_links_condition, 0.5);
			},

			get_value: function (tree) {
				// Traverse tree
				var val = this.values, i = 0, j = tree.length - 1;
				for (; i < j; ++i) {
					val = val[tree[i]];
				}

				// Get
				return val[tree[i]];
			},
			change_value: function (tree, new_value) {
				// Traverse tree
				var val = this.values, i = 0, j = tree.length - 1;
				for (; i < j; ++i) {
					val = val[tree[i]];
				}

				// Update
				var old_value = val[tree[i]];
				val[tree[i]] = new_value;

				// Change event
				trigger_change.call(this, tree, old_value, new_value, "same");

				// Return old
				return old_value;
			},
			load_values: function (events, complete_callback) {
				// Async save
				SaveAsync.get(this.save_key, on_load_values_callback.bind(this, events, complete_callback));
			},
			save_values: function (complete_callback) {
				// Async save
				SaveAsync.set(this.save_key, this.values, on_save_values_callback.bind(this, complete_callback));
			},
			delete_values: function (complete_callback) {
				// Async delete
				SaveAsync.del(this.save_key, complete_callback);
			},

			on_ready: function (callback) {
				if (this.ready_callbacks !== null) {
					// Add to callback list
					this.ready_callbacks.push(callback);
				}
				else {
					// Already ready
					callback.call(this);
				}
			},
			on_change: function (tree, callback) {
				var key = tree.join("-");
				if (key in this.change_events) {
					// Update old
					this.change_events[key].callbacks.push(callback);
				}
				else {
					// Create new
					var tree_clone = [];
					for (var i = 0; i < tree.length; ++i) {
						tree_clone.push(tree[i]);
					}

					this.change_events[key] = {
						tree: tree_clone,
						callbacks: [ callback ]
					};
				}
			},
			off_change: function (tree, callback) {
				var key = tree.join("-");
				if (key in this.change_events) {
					// Update old
					var list = this.change_events[key].callbacks;
					for (var i = 0; i < list.length; ++i) {
						if (list[i] === callback) {
							// Remove
							list[i].splice(i, 1);
							if (list.length === 0) {
								delete this.change_events[key];
							}
							return true;
						}
					}
				}

				return false;
			},

			modify_4chanx_settings: function (callback) {
				if (api.is_4chanx) {
					var body = document.querySelector("body");
					if (body) style.add_class(body, "iex_hide_other_settings");

					// Set the timer
					this.modifying_settings_4chanx_timer = setTimeout(on_modifying_settings_4chanx_timout.bind(this), 1000);
					this.modifying_settings_4chanx_callback = callback || null;

					// Open settings to Main tab
					var target_tab = api.is_appchanx ? "Script" : "Main";
					if (api.settings_4chanx_is_open()) {
						api.settings_4chanx_change_section(target_tab);
					}
					else {
						api.settings_4chanx_open(target_tab);
					}
				}
			},
			modify_vanilla_settings: function (callback) {
				if (!api.is_4chanx) {
					var body = document.querySelector("body");
					if (body) style.add_class(body, "iex_hide_other_settings");

					// Set the timer
					this.modifying_settings_vanilla_timer = setTimeout(on_modifying_settings_vanilla_timout.bind(this), 1000);
					this.modifying_settings_vanilla_callback = callback || null;

					// Open settings
					api.settings_vanilla_open();
				}
			},

			disable_image_expansion: function () {
				if (!this.can_disable_image_hover) return;

				// Disable
				settings.change_value(["image_expansion", "enabled"], false);
				settings.save_values();

				// Alert message
				this.display_image_hover_notification();
			},

			settings_open: function () {
				settings_open.call(this);
			},
			settings_close: function () {
				if (this.settings_container === null) return;

				// Remove
				destroy_settings_data.call(this, this.settings_removal_data);
				var par = this.settings_container_outer.parentNode;
				if (par) par.removeChild(this.settings_container_outer);

				this.settings_removal_data = null;
				this.settings_container = null;
				this.settings_container_outer = null;
				this.settings_difficulty_container = null;


				// Modify other settings
				if (this.settings_update_other_after_close) {
					this.settings_update_other_after_close = false;

					if (this.values.image_expansion.enabled) {
						// Modify
						var cb = on_settings_update_other_complete.bind(this);
						if (api.is_4chanx) {
							this.modify_4chanx_settings(cb);
						}
						else {
							this.modify_vanilla_settings(cb);
						}
					}
				}
			},

			display_first_run_notification: function () {
				if (this.notification_first_run !== null) return;

				// Alert message
				var message = $("div"), m_title, m_part, m_link;

				// Line 1
				m_title = $("div");
				m_title.appendChild($.text("You've just installed "));

				m_link = $("a");
				m_link.setAttribute("target", "_blank");
				m_link.setAttribute("rel", "nofollow noreferrer");
				m_link.setAttribute("href", "https://dnsev.github.io/iex/");
				m_link.textContent = "iex - Image Extensions";
				m_title.appendChild(m_link);

				m_title.appendChild($.text("!"));

				// Line 2
				m_part = $.div("iex_spaced_div");
				m_part.textContent = "If you use 4chan-x or appchan-x, you can access iex settings through the header menu. Otherwise, look for the [ iex ] link at the top of the page.";
				message.appendChild(m_part);

				// Line 3
				m_part = $.div("iex_spaced_div");
				m_part.appendChild($.text("Any questions or issues can be addressed on "));

				m_link = $("a");
				m_link.setAttribute("target", "_blank");
				m_link.setAttribute("rel", "nofollow noreferrer");
				m_link.setAttribute("href", "https://github.com/dnsev/iex/issues");
				m_link.textContent = "the github page";
				m_part.appendChild(m_link);

				m_part.appendChild($.text("."));
				message.appendChild(m_part);

				// Line 4
				m_part = $.div("iex_spaced_div");
				m_part.textContent = "Would you like to enable iex now? (page will be refreshed)";
				message.appendChild(m_part);

				// Line 5
				m_part = $("div");

				m_link = $.a("iex_highlighted_link");
				m_link.setAttribute("target", "_blank");
				m_link.setAttribute("href", "//dnsev.github.io/iex/");
				m_link.textContent = "enable now";
				m_link.addEventListener("click", on_first_run_link_click.bind(this, true), false);
				m_part.appendChild(m_link);

				m_part.appendChild($.text(" or "));

				m_link = $.a("iex_highlighted_link");
				m_link.setAttribute("target", "_blank");
				m_link.setAttribute("href", "//dnsev.github.io/iex/");
				m_link.textContent = "leave disabled";
				m_link.addEventListener("click", on_first_run_link_click.bind(this, false), false);
				m_part.appendChild(m_link);

				message.appendChild(m_part);

				// Display
				this.notification_first_run = new Notification({
					close: false,
					overlay_close: false,
					style: "info",
					title: m_title,
					content: message,
					on: {
						close: on_notification_first_run_close.bind(this)
					}
				});
			},
			display_install_fail_notification: function (status) {
				if (this.notification_install_fail !== null) return;

				// Alert message
				var message = $("div"), m_part, m_link;

				// Line 1
				m_part = $.div("iex_spaced_div");
				m_part.textContent = "Failed to resolve settings conflicts for reason: " + status + ".";
				message.appendChild(m_part);

				// Line 3
				m_part = $("div");
				m_part.textContent = "This is potentially due to other userscript extensions you have installed.";
				message.appendChild(m_part);

				// Line 4
				m_part = $.div("iex_spaced_div");
				m_part.textContent = "You can try manually turning off any \"Image Hover\" settings to get iex to work.";
				message.appendChild(m_part);

				// Line 5
				m_part = $.div("iex_spaced_div");
				m_part.appendChild($.text("If you would like to get this fixed, "));

				m_link = $.a("iex_underlined_link");
				m_link.setAttribute("target", "_blank");
				m_link.setAttribute("href", "http://github.com/dnsev/iex/issues");
				m_link.textContent = "file an issue request";
				m_part.appendChild(m_link);

				m_part.appendChild($.text("."));
				message.appendChild(m_part);

				// Line 5
				m_part = $("div");
				m_part.textContent = "This script can be re-enabled through its settings page.";
				message.appendChild(m_part);

				// Display
				this.notification_install_fail = new Notification({
					close: true,
					overlay_close: false,
					style: "error",
					title: "iex error: Failed to modify settings",
					content: message,
					on: {
						close: on_notification_install_fail_close.bind(this)
					}
				});
			},
			display_settings_info_notification: function () {
				// Async get
				SaveAsync.get(this.save_key, on_display_settings_info_notification_callback.bind(this));
			},
			display_image_hover_notification: function () {
				// Alert message
				var message = $("div"),
					m_part, m_link;

				// Line 1
				m_part = $("div");

				// Text
				m_part.appendChild($.text("Image Extensions will be disabled and can be re-enabled through "));

				// Link
				m_link = $.a("iex_underlined_link");
				m_link.setAttribute("target", "_blank");
				m_link.setAttribute("href", "//dnsev.github.io/iex/");
				m_link.textContent = "settings";
				m_part.appendChild(m_link);

				// Text
				m_part.appendChild($.text("."));

				// Add to message
				message.appendChild(m_part);

				// Display
				this.notification_image_hover = new Notification({
					close: true,
					overlay_close: true,
					style: "warning",
					title: "Default image hovering still enabled",
					content: message,
					on: {
						close: on_notification_image_hover_close.bind(this)
					}
				});
				m_link.addEventListener("click", on_notification_link_click.bind(this, true, this.notification_image_hover), false);
			},

		};



		return Settings;

	})();

	// Class to manage page styling
	var Style = (function () {

		var Style = function () {
			// Append style
			this.stylesheet = null;
			this.stylesheet_annotations = null;

			// Observers
			this.head_observer = null;
			this.style_observers = [];
			this.stylesheet_observers = [];
			this.on_style_observe_bind = on_style_observe.bind(this);
			this.on_stylesheet_observe_bind = on_stylesheet_observe.bind(this);

			// Theme
			this.theme = " iex_no_theme";
			this.theme_check_count = 0;
			this.theme_check_count_max = 100;
			this.theme_check_timer = null;
			this.theme_check_timeout_bind = on_theme_check_timeout.bind(this);
			on_ready(on_asap.bind(this), on_asap_condition.bind(this));
		};



		var loaded_fonts = null;

		var on_insert_stylesheet = function (stylesheet) {
			var head = document.querySelector("head");
			if (head) head.appendChild(stylesheet);
		};
		var on_insert_stylesheet_condition = function () {
			return document.querySelector("head");
		};

		var on_asap = function () {
			// Theme check
			this.theme_check_timeout_bind.call(this);

			// Style changing
			var head = document.querySelector("head");
			if (!head) return;

			// Create new observer
			this.head_observer = new MutationObserver(on_head_observe.bind(this));
			this.head_observer.observe(
				head,
				{
					childList: true
				}
			);

			// Add event listeners to stylesheets
			var stylesheets = head.querySelectorAll("link[rel='stylesheet']"),
				styles = head.querySelectorAll("style"),
				i;

			for (i = 0; i < styles.length; ++i) {
				// Create new observer
				add_style_observer.call(this, styles[i]);
			}
			for (i = 0; i < stylesheets.length; ++i) {
				// Create new observer
				add_stylesheet_observer.call(this, stylesheets[i]);
			}
		};
		var on_asap_condition = function () {
			return document.querySelector("head") && document.querySelector("body");
		};
		var on_theme_check_timeout = function () {
			// Stop timer
			this.theme_check_timer = null;

			// New theme
			if (!theme_update.call(this)) {
				// Timeout
				if (++this.theme_check_count < this.theme_check_count_max) {
					this.theme_check_timer = setTimeout(this.theme_check_timeout_bind, 50);
				}
			}
		};

		var on_head_observe = function (records) {
			var nodes, i, j, r;
			for (i = 0; i < records.length; ++i) {
				r = records[i];
				if ((nodes = r.addedNodes)) {
					for (j = 0; j < nodes.length; ++j) {
						// Check
						on_head_observe_add.call(this, nodes[j]);
					}
				}
				if ((nodes = r.removedNodes)) {
					for (j = 0; j < nodes.length; ++j) {
						// Check
						on_head_observe_remove.call(this, nodes[j]);
					}
				}
			}
		};
		var on_head_observe_add = function (element) {
			if (element.tagName === "STYLE") {
				add_style_observer.call(this, element);
			}
			else if (element.tagName === "LINK") {
				if (element.getAttribute("rel") == "stylesheet") {
					add_stylesheet_observer.call(this, element);
				}
			}
		};
		var on_head_observe_remove = function (element) {
			if (element.tagName === "STYLE") {
				remove_style_observer.call(this, element);
			}
			else if (element.tagName === "LINK") {
				if (element.getAttribute("rel") == "stylesheet") {
					remove_stylesheet_observer.call(this, element);
				}
			}
		};

		var on_stylesheet_observe = function (records) {
			var i, r;
			for (i = 0; i < records.length; ++i) {
				r = records[i];

				if (r.attributeName === "href") {
					// Theme update
					theme_update.call(this);
				}
			}
		};

		var on_style_observe = function (records) {
			var i;
			for (i = 0; i < records.length; ++i) {
				// Theme update
				theme_update.call(this);
			}
		};

		var add_style_observer = function (element) {
			// Create new observer
			var observer = new MutationObserver(this.on_style_observe_bind);
			observer.observe(
				element,
				{
					childList: true,
					characterData: true
				}
			);

			this.style_observers.push({
				target: element,
				observer: observer,
			});
		};
		var add_stylesheet_observer = function (element) {
			// Create new observer
			var observer = new MutationObserver(this.on_stylesheet_observe_bind);
			observer.observe(
				element,
				{
					attributes: true
				}
			);

			this.stylesheet_observers.push({
				target: element,
				observer: observer,
			});
		};
		var remove_style_observer = function (element) {
			// Delete observer
			for (var i = 0; i < this.style_observers.length; ++i) {
				if (this.style_observers[i].target === element) {
					this.style_observers[i].observer.disconnect();
					this.style_observers.splice(i, 1);
					return;
				}
			}
		};
		var remove_stylesheet_observer = function (element) {
			// Delete observer
			for (var i = 0; i < this.stylesheet_observers.length; ++i) {
				if (this.stylesheet_observers[i].target === element) {
					this.stylesheet_observers[i].observer.disconnect();
					this.stylesheet_observers.splice(i, 1);
					return;
				}
			}
		};
		var theme_update = function () {
			// Find theme
			var theme_new = this.theme_detect();
			if (theme_new !== null) {
				// Clear timer
				if (this.theme_check_timer !== null) {
					clearTimeout(this.theme_check_timer);
					this.theme_check_timer = null;
				}

				// Change theme
				this.theme_change(theme_new);

				// Okay
				return true;
			}

			// Not okay
			return false;
		};

		var get_true_style = function (element, style_name) {
			var s;
			try {
				s = document.defaultView.getComputedStyle(element);
			}
			catch (e) {}
			if (!s) {
				s = element.style || {};
			}
			return style_name ? s[style_name] : s;
		};



		Style.prototype = {
			constructor: Style,

			insert_stylesheet: function () {
				// Create stylesheet
				var stylesheet_text = "body.iex_hide_other_settings #settingsMenu,body.iex_hide_other_settings #overlay,body.iex_hide_other_settings #appchanx-settings,body.iex_hide_other_settings #fourchanx-settings{visibility:hidden !important;}.iex_no_padding_br{padding-bottom:0 !important;padding-right:0 !important;margin-bottom:0 !important;margin-right:0 !important;border-bottom-width:0 !important;border-right-width:0 !important;}.iex_no_padding_tl{padding-top:0 !important;padding-left:0 !important;margin-top:0 !important;margin-left:0 !important;border-top-width:0 !important;border-left-width:0 !important;}.iex_settings_popup_overlay{position:fixed;z-index:200;left:0;top:0;bottom:0;right:0;font-size:14px;background:rgba(0,0,0,0.25);}.iex_settings_popup_overlay.iex_dark{background:rgba(255,255,255,0.25);}.iex_settings_popup{margin:0;padding:0;width:100%;height:100%;text-align:center;white-space:nowrap;z-index:200;line-height:0;}.iex_settings_popup_aligner{height:100%;width:0;display:inline-block;vertical-align:middle;}.iex_settings_popup_inner{display:inline-block;vertical-align:middle;text-align:left;line-height:normal;white-space:normal;position:relative;width:40em;height:80%;min-height:14em;}.iex_settings_popup_table{display:table;position:relative;table-layout:fixed;width:100%;height:100%;box-shadow:0 0 1em 0.25em #000000;}.iex_settings_popup_table.iex_dark{box-shadow:0 0 1em 0.25em #ffffff;}.iex_settings_popup_top{display:table-row;width:100%;}.iex_settings_popup_top_content{display:table;width:100%;background:rgba(0,0,0,0.0625);}.iex_settings_popup_top_content.iex_dark{background:rgba(255,255,255,0.0625);}.iex_settings_popup_top_label{display:table-cell;vertical-align:middle;padding:0.25em;font-size:1.125em;font-weight:bold;text-shadow:0 0.0625em 0.25em #ffffff;}.iex_settings_popup_top_label.iex_dark{text-shadow:0 0.0625em 0.25em #000000;}.iex_settings_popup_top_right{display:table-cell;vertical-align:middle;padding:0.25em;text-align:right;}.iex_settings_popup_middle{display:table-row;width:100%;height:100%;}.iex_settings_popup_middle_content_pad{display:block;width:100%;height:100%;position:relative;}.iex_settings_popup_middle_content{display:block;position:absolute;left:0;top:0;bottom:0;right:0;padding:0;overflow:auto;}.iex_settings_popup_middle_content_inner{display:block;padding:0.5em;}.iex_settings_popup_close_link{font-weight:bold;text-decoration:none !important;padding:0em 0.2em;}.iex_settings_popup_bottom{display:table-row;width:100%;}.iex_settings_container{position:relative;}.iex_settings_region_container{margin:0.25em -0.25em -0.25em 2em;}.iex_settings_region{}.iex_settings_group{}.iex_settings_group.iex_settings_group_hidden{display:none;}.iex_settings_group_label{font-size:1.125em;padding-bottom:0.125em;margin-bottom:0.25em;font-weight:bold;text-align:left;color:#000000;border-bottom:0.125em solid rgba(0,0,0,0.5);text-shadow:0em 0em 0.25em #ffffff;}.iex_settings_group_label.iex_dark{color:#ffffff;border-bottom:0.125em solid rgba(255,255,255,0.5);text-shadow:0em 0em 0.25em #000000;}.iex_settings_group.iex_settings_group_padding_top{margin-top:2em;}.iex_settings_setting{padding:0.25em;background:transparent;}.iex_settings_setting.iex_settings_setting_top_padding{margin-top:0.25em;}.iex_settings_setting.iex_settings_setting_hidden{display:none;}.iex_settings_setting.iex_settings_setting_odd{background:rgba(0,0,0,0.0625);}.iex_settings_setting.iex_settings_setting_odd.iex_dark{background:rgba(255,255,255,0.0625);}.iex_settings_setting_table{display:block;width:100%;}.iex_settings_setting_left{display:block;overflow:hidden;text-align:left;vertical-align:top;}.iex_settings_setting_right{display:inline-block;float:right;text-align:right;vertical-align:middle;padding:0.5em 0em;}.iex_settings_setting_label{display:block;}.iex_settings_setting_label_title{color:#000000;font-weight:bold;font-size:1.125em;vertical-align:baseline;}.iex_settings_setting_label_title.iex_dark{color:#ffffff;}.iex_settings_setting_label_subtitle{color:#000000;font-weight:bold;font-size:1em;vertical-align:baseline;opacity:0.625;margin-left:0.5em;}.iex_settings_setting_label_subtitle.iex_dark{color:#ffffff;}.iex_settings_setting_label_subtitle:before{content:\"(\";}.iex_settings_setting_label_subtitle:after{content:\")\";}.iex_settings_setting_description{margin-left:0.5em;}.iex_settings_setting_input_container{display:inline-block;white-space:nowrap;cursor:pointer;}.iex_settings_setting_input_container.iex_settings_setting_input_container_reverse{direction:rtl;}.iex_settings_setting_input_checkbox{vertical-align:middle;}.iex_settings_setting_input_checkbox+.iex_settings_setting_input_label,.iex_settings_setting_input_checkbox+*+.iex_settings_setting_input_label{vertical-align:middle;padding-right:0.5em;}.iex_settings_setting_input_checkbox+.iex_settings_setting_input_label:after,.iex_settings_setting_input_checkbox+*+.iex_settings_setting_input_label:after{content:attr(data-iex-checkbox-label-off);}.iex_settings_setting_input_checkbox:checked+.iex_settings_setting_input_label:after,.iex_settings_setting_input_checkbox:checked+*+.iex_settings_setting_input_label:after{content:attr(data-iex-checkbox-label-on);}input.iex_settings_setting_input_textbox{width:5em;padding:0.125em;border:1px solid rgba(16,16,16,0.5) !important;}input.iex_settings_setting_input_textbox:focus{border-color:rgba(16,16,16,1) !important;}input.iex_settings_setting_input_textbox.iex_dark{border-color:rgba(240,240,240,0.5) !important;}input.iex_settings_setting_input_textbox.iex_dark:focus{border-color:rgba(240,240,240,1) !important;}.iex_settings_difficulty_container{position:absolute;right:0;top:0;margin-top:0.5em;}.iex_settings_popup a.iex_settings_difficulty_choice{cursor:pointer;text-decoration:none !important;}.iex_settings_popup a.iex_settings_difficulty_choice.iex_settings_difficulty_choice_selected{text-decoration:underline !important;}.iex_settings_popup a.iex_settings_homepage_link{text-decoration:none !important;}.iex_settings_difficulty_separator{}.iex_notification{position:fixed;left:0;top:0;bottom:0;right:0;z-index:100;background:rgba(255,255,255,0.5);font-size:14px;}.iex_notification.iex_dark{background:rgba(0,0,0,0.5);}.iex_notification_body_outer{margin:0;padding:0;width:100%;height:100%;text-align:center;white-space:nowrap;z-index:200;line-height:0;}.iex_notification_body_aligner{height:100%;width:0;display:inline-block;vertical-align:middle;}.iex_notification_body_inner{display:inline-block;vertical-align:middle;line-height:normal;}.iex_notification_body{position:relative;text-align:center;white-space:normal;width:36em;padding:0.375em 1em;border-radius:0.25em;box-shadow:0em 0em 0.25em 0.125em rgba(0,0,0,0.5);}.iex_notification_body.iex_dark{box-shadow:0em 0em 0.25em 0.125em rgba(255,255,255,0.5);}.iex_notification_body.iex_notification_info{background:rgba(0,120,220,0.9);}.iex_notification_body.iex_notification_success{background:rgba(0,168,20,0.9);}.iex_notification_body.iex_notification_error{background:rgba(220,0,0,0.9);}.iex_notification_body.iex_notification_warning{background:rgba(220,120,0,0.9);}a.iex_notification_close{color:#ffffff !important;position:absolute;top:0;right:0;z-index:10;cursor:pointer;font-weight:bold;}a.iex_notification_close:hover{color:#000000 !important;text-shadow:0em 0.0625em 0.125em #ffffff;}a.iex_notification_close.iex_notification_close_hidden{display:none;}.iex_notification_content_outer{position:relative;}.iex_notification_content{position:relative;}.iex_notification_content_title{font-weight:bold;margin-bottom:0.75em;color:#ffffff;text-shadow:0em 0.0625em 0.125em #000000;}.iex_notification_content_body{color:#ffffff;text-shadow:0em 0.0625em 0.125em rgba(0,0,0,0.5);}.iex_notification_content a{color:#ffffff !important;text-decoration:none;}.iex_notification_content a:hover{color:#c010e0 !important;}.iex_notification_content.iex_dark a:hover{color:#d040e8 !important;}.iex_notification_content .iex_spaced_div{margin-bottom:0.5em;}.iex_notification_content a.iex_highlighted_link{font-weight:bold;text-decoration:underline !important;}.iex_notification_content a.iex_underlined_link{text-decoration:underline !important;}textarea.iex_notification_textarea{width:100%;height:5em;min-height:2em;font-family:Courier New;font-size:1em;text-align:left;color:#ffffff !important;background-color:transparent !important;border:0.08em solid rgba(255,255,255,0.5) !important;padding:0.25em;margin:0;resize:vertical;box-sizing:border-box;-moz-box-sizing:border-box;}textarea.iex_notification_textarea:hover,textarea.iex_notification_textarea:focus{background-color:rgba(255,255,255,0.0625) !important;border:0.08em solid rgba(255,255,255,0.75) !important;}.iex_floating_container{display:block;margin:0;padding:0;border:0em hidden;}.iex_floating_image_connector{display:block;position:absolute;z-index:100;}.iex_floating_image_connector:not(.iex_floating_image_connector_visible){display:none;}.iex_cpreview_container{display:block;position:absolute;z-index:100;}.iex_cpreview_container.iex_cpreview_container_fixed{position:fixed;}.iex_cpreview_container:not(.iex_cpreview_container_visible){display:none;}.iex_cpreview_container.iex_cpreview_container_auto{z-index:0;left:0;top:0;bottom:0;right:0;}.iex_cpreview_container.iex_cpreview_container_auto>.iex_cpreview_padding,.iex_cpreview_container.iex_cpreview_container_auto>.iex_cpreview_padding>.iex_cpreview_overflow{width:100%;height:100%;}.iex_cpreview_padding{display:block;position:relative;margin:0;padding:0;left:0;top:0;}.iex_cpreview_overflow{display:block;position:relative;overflow:hidden;}.iex_cpreview_offset{display:block;position:relative;width:100%;height:100%;text-align:center;}.iex_cpreview_offset.iex_cpreview_offset_sized{}.iex_cpreview_overlay{}.iex_cpreview_content{}.iex_cpreview_padding.iex_mpreview_padding{box-shadow:0em 0em 0.6em 0.3em rgba(0,0,0,0.75);background-color:rgba(255,255,255,0.5);}.iex_cpreview_padding.iex_mpreview_padding.iex_dark{box-shadow:0em 0em 0.6em 0.3em rgba(255,255,255,0.75);background-color:rgba(0,0,0,0.5);}.iex_cpreview_padding.iex_mpreview_mouse_hidden{cursor:none;}.iex_mpreview_background{display:block;position:absolute;left:0;top:0;bottom:0;right:0;background-color:transparent;background-repeat:no-repeat;background-position:left top;background-size:100% 100%;opacity:0.375;}.iex_mpreview_background.iex_mpreview_background_disabled{opacity:0;visibility:hidden;}.iex_mpreview_background.iex_mpreview_background_visible_full{opacity:1;}.iex_mpreview_background:not(.iex_mpreview_background_visible){display:none;}.iex_mpreview_background.iex_transitions.iex_mpreview_background_visible{transition:opacity 0.5s, visibility 0s linear 0s;}.iex_mpreview_background.iex_transitions.iex_mpreview_background_visible.iex_mpreview_background_disabled{transition:opacity 0.5s, visibility 0s linear 0.5s;}.iex_mpreview_zoom_borders{display:block;position:absolute;border:0.08em solid #ffffff;opacity:0.25;left:0;top:0;bottom:0;right:0;pointer-events:none;}.iex_mpreview_zoom_borders_inner{display:block;position:absolute;border:0.08em solid #000000;left:0;top:0;bottom:0;right:0;}.iex_mpreview_zoom_borders:not(.iex_mpreview_zoom_borders_visible),.iex_mpreview_zoom_borders:not(.iex_mpreview_zoom_borders_vertical):not(.iex_mpreview_zoom_borders_horizontal){opacity:0;visibility:hidden;}.iex_mpreview_zoom_borders:not(.iex_mpreview_zoom_borders_vertical){top:0 !important;bottom:0 !important;border-top:0 hidden;border-bottom:0 hidden;}.iex_mpreview_zoom_borders:not(.iex_mpreview_zoom_borders_horizontal){left:0 !important;right:0 !important;border-left:0 hidden;border-right:0 hidden;}.iex_mpreview_zoom_borders:not(.iex_mpreview_zoom_borders_vertical)>.iex_mpreview_zoom_borders_inner{border-top:0 hidden;border-bottom:0 hidden;}.iex_mpreview_zoom_borders:not(.iex_mpreview_zoom_borders_horizontal)>.iex_mpreview_zoom_borders_inner{border-left:0 hidden;border-right:0 hidden;}.iex_mpreview_stats_container{display:block;position:absolute;padding:0.125em 0em 0em 0.5em;left:-0.5em;right:0;bottom:100%;white-space:nowrap;overflow:hidden;color:#ffffff;text-shadow:0em 0em 0.4em #000000,0em 0em 0.3em #000000;}.iex_mpreview_stats_container.iex_dark{color:#000000;text-shadow:0em 0em 0.4em #ffffff,0em 0em 0.3em #ffffff;}.iex_mpreview_stat{font-weight:bold;}.iex_mpreview_stat.iex_mpreview_stat_red{color:#f03030;}.iex_mpreview_stat.iex_mpreview_stat_red.iex_dark{color:#a00000;}.iex_mpreview_stat_sep{opacity:0.825;}.iex_mpreview_stat:not(.iex_mpreview_stat_visible),.iex_mpreview_stat_sep:not(.iex_mpreview_stat_sep_visible){display:none;}.iex_mpreview_stat.iex_mpreview_stat_zoom_controls{margin-right:0.25em;position:absolute;}.iex_mpreview_stat.iex_mpreview_stat_zoom_controls.iex_mpreview_stat_zoom_controls_fixed{position:fixed;}.iex_mpreview_stat_zoom_controls_offset{display:none;}.iex_mpreview_stat.iex_mpreview_stat_zoom_controls.iex_mpreview_stat_visible+.iex_mpreview_stat_zoom_controls_offset{display:inline-block;}.iex_mpreview_stat_zoom_control{padding:0em 0.25em;cursor:pointer;}.iex_mpreview_stat_zoom_control_increase:hover{color:#80d0ff;}.iex_mpreview_stat_zoom_control_increase.iex_dark:hover{color:#a0e0ff;}.iex_mpreview_stat_zoom_control_decrease:hover{color:#f08060;}.iex_mpreview_stat_zoom_control_decrease.iex_dark:hover{color:#ffb0a0;}.iex_mpreview_image{display:inline-block;border:none;margin:0;padding:0;outline:none;}.iex_mpreview_image:not(.iex_mpreview_image_visible){display:none;}.iex_mpreview_image.iex_mpreview_image_unsized{max-width:100%;max-height:100%;}.iex_cpreview_offset.iex_cpreview_offset_sized>.iex_mpreview_image{position:absolute;left:0;top:0;bottom:0;right:0;width:100%;height:100%;}.iex_cpreview_offset:not(.iex_cpreview_offset_sized)>.iex_mpreview_image{max-width:100%;max-height:100%;width:auto;height:auto;}.iex_mpreview_video{display:inline-block;border:0em hidden;margin:0;padding:0;outline:0em hidden;}.iex_mpreview_video:not(.iex_mpreview_video_visible){display:none;}.iex_mpreview_video.iex_mpreview_video_unsized{max-width:100%;max-height:100%;}.iex_mpreview_video.iex_mpreview_video_not_ready{visibility:hidden;}.iex_cpreview_offset.iex_cpreview_offset_sized>.iex_mpreview_video{position:absolute;left:0;top:0;bottom:0;right:0;width:100%;height:100%;}.iex_cpreview_offset:not(.iex_cpreview_offset_sized)>.iex_mpreview_video{max-width:100%;max-height:100%;width:auto;height:auto;}.iex_mpreview_vcontrols_container{display:block;position:absolute;left:0;top:0;bottom:0;right:0;background:transparent;font-size:1.5em;}.iex_mpreview_vcontrols_container_inner{display:block;position:absolute;left:0;bottom:0;right:0;padding-top:2em;height:2em;background:transparent;}.iex_mpreview_vcontrols_table{display:table;width:100%;height:100%;}.iex_mpreview_vcontrols_table:not(.iex_mpreview_vcontrols_table_visible):not(.iex_mpreview_vcontrols_table_visible_temp):not(.iex_mpreview_vcontrols_table_visible_important):not(.iex_mpreview_vcontrols_table_mini),.iex_mpreview_vcontrols_table.iex_mpreview_vcontrols_table_mini_disabled{display:none;}.iex_mpreview_vcontrols_seek_container{display:table-cell;vertical-align:bottom;min-width:1em;}.iex_mpreview_vcontrols_seek_container_inner{display:block;width:100%;height:2em;position:relative;}.iex_mpreview_vcontrols_seek_bar{display:block;position:absolute;left:0;right:0;top:0.5em;bottom:0.5em;border-radius:0.5em;overflow:hidden;cursor:pointer;border:1px solid rgba(0,0,0,0.5);}.iex_mpreview_vcontrols_seek_bar.iex_mpreview_vcontrols_no_border_radius{border-radius:0;}.iex_mpreview_vcontrols_seek_bar_bg{position:absolute;left:0;top:0;bottom:0;right:0;background-color:#c0c0c0;opacity:0.825;}.iex_mpreview_vcontrols_seek_bar_loaded{position:absolute;left:0;top:0;bottom:0;width:0;background:#ffffff;}.iex_mpreview_vcontrols_seek_bar_played{position:absolute;left:0;top:0;bottom:0;width:0;background:#66cc33;}.iex_mpreview_vcontrols_seek_time_table{position:relative;display:table;width:100%;height:100%;font-size:0.625em;}.iex_mpreview_vcontrols_seek_time_current{display:table-cell;text-align:left;vertical-align:middle;padding:0em 0.25em 0em 0.375em;}.iex_mpreview_vcontrols_seek_time_duration{display:table-cell;text-align:right;vertical-align:middle;padding:0em 0.375em 0em 0.25em;}.iex_mpreview_vcontrols_seek_time_current,.iex_mpreview_vcontrols_seek_time_duration{color:#101010;text-shadow:0em 0.125em 0.125em #ffffff,0em 0.0625em 0.0625em #ffffff;}.iex_mpreview_vcontrols_volume_container_position{display:block;position:absolute;bottom:100%;right:0;margin-bottom:-0.5em;}.iex_mpreview_vcontrols_volume_container{width:2em;height:9em;position:relative;}.iex_mpreview_vcontrols_volume_container:not(.iex_mpreview_vcontrols_volume_container_visible):not(.iex_mpreview_vcontrols_volume_container_visible_important):not(.iex_mpreview_vcontrols_volume_container_visible_temp){display:none;}.iex_mpreview_vcontrols_volume_bar{position:absolute;left:0.5em;right:0.5em;top:0.5em;bottom:0.5em;border-radius:0.5em;overflow:hidden;cursor:pointer;border:1px solid rgba(0,0,0,0.5);}.iex_mpreview_vcontrols_volume_bar.iex_mpreview_vcontrols_no_border_radius{border-radius:0;}.iex_mpreview_vcontrols_volume_bar_bg{position:absolute;left:0;top:0;bottom:0;right:0;background-color:#ffffff;opacity:0.825;}.iex_mpreview_vcontrols_volume_bar_level{position:absolute;left:0;right:0;bottom:0;height:0;background-color:#ffbb50;}.iex_mpreview_vcontrols_button_container{display:table-cell;vertical-align:bottom;width:2em;}.iex_mpreview_vcontrols_button_container_inner{display:block;width:100%;height:2em;position:relative;}.iex_mpreview_vcontrols_button_container_inner2{display:block;position:absolute;left:0;top:0;bottom:0;right:0;}.iex_mpreview_vcontrols_button_mouse_controller{position:absolute;left:0;top:0;bottom:0;right:0;margin:0.4em;cursor:pointer;}.iex_mpreview_vcontrols_table.iex_mpreview_vcontrols_table_mini:not(.iex_mpreview_vcontrols_table_visible):not(.iex_mpreview_vcontrols_table_visible_temp):not(.iex_mpreview_vcontrols_table_visible_important)>.iex_mpreview_vcontrols_button_container{display:none;}.iex_mpreview_vcontrols_table.iex_mpreview_vcontrols_table_mini:not(.iex_mpreview_vcontrols_table_visible):not(.iex_mpreview_vcontrols_table_visible_temp):not(.iex_mpreview_vcontrols_table_visible_important)>.iex_mpreview_vcontrols_seek_container{min-width:0;}.iex_mpreview_vcontrols_table.iex_mpreview_vcontrols_table_mini:not(.iex_mpreview_vcontrols_table_visible):not(.iex_mpreview_vcontrols_table_visible_temp):not(.iex_mpreview_vcontrols_table_visible_important)>*>.iex_mpreview_vcontrols_seek_container_inner{height:0.125em;}.iex_mpreview_vcontrols_table.iex_mpreview_vcontrols_table_mini:not(.iex_mpreview_vcontrols_table_visible):not(.iex_mpreview_vcontrols_table_visible_temp):not(.iex_mpreview_vcontrols_table_visible_important)>*>*>.iex_mpreview_vcontrols_seek_bar{top:0;bottom:0;border-radius:0;border:0 hidden;}.iex_mpreview_vcontrols_table.iex_mpreview_vcontrols_table_mini:not(.iex_mpreview_vcontrols_table_visible):not(.iex_mpreview_vcontrols_table_visible_temp):not(.iex_mpreview_vcontrols_table_visible_important)>*>*>*>*>.iex_mpreview_vcontrols_seek_time_table{display:none;}.iex_svg_play_button{display:block;}.iex_svg_play_button.iex_svg_play_button_playing>.iex_svg_button_scale_group>.iex_svg_play_button_play_icon,.iex_svg_play_button.iex_svg_play_button_playing>.iex_svg_button_scale_group>.iex_svg_play_button_loop_icon,.iex_svg_play_button:not(.iex_svg_play_button_playing)>.iex_svg_button_scale_group>.iex_svg_play_button_pause_icon,.iex_svg_play_button:not(.iex_svg_play_button_playing).iex_svg_play_button_looping>.iex_svg_button_scale_group>.iex_svg_play_button_play_icon,.iex_svg_play_button:not(.iex_svg_play_button_playing):not(.iex_svg_play_button_looping)>.iex_svg_button_scale_group>.iex_svg_play_button_loop_icon{visibility:hidden;}.iex_svg_volume_button{display:block;}.iex_svg_volume_button.iex_svg_volume_button_muted>.iex_svg_button_scale_group>.iex_svg_volume_button_wave_big,.iex_svg_volume_button.iex_svg_volume_button_muted>.iex_svg_button_scale_group>.iex_svg_volume_button_wave_small,.iex_svg_volume_button:not(.iex_svg_volume_button_muted)>.iex_svg_button_scale_group>.iex_svg_volume_button_wave_mute_icon,.iex_svg_volume_button.iex_svg_volume_button_high>.iex_svg_button_scale_group>.iex_svg_volume_button_wave_small,.iex_svg_volume_button:not(.iex_svg_volume_button_medium)>.iex_svg_button_scale_group>.iex_svg_volume_button_wave_small,.iex_svg_volume_button:not(.iex_svg_volume_button_high)>.iex_svg_button_scale_group>.iex_svg_volume_button_wave_big{visibility:hidden;}.iex_svg_volume_button_wave_mute_icon{opacity:0.75;}.iex_svg_volume_button_wave_mute_icon_polygon{fill:#000000;}.iex_svg_button_fill{fill:rgba(255,255,255,0.825);stroke:rgba(0,0,0,0.5);stroke-width:1px;vector-effect:non-scaling-stroke;}.iex_mpreview_vcontrols_button_mouse_controller:hover+.iex_svg_play_button>.iex_svg_button_scale_group>.iex_svg_button_fill{fill:#44aaff;}.iex_mpreview_vcontrols_button_mouse_controller:hover+.iex_svg_volume_button>.iex_svg_button_scale_group>.iex_svg_button_fill{fill:#ff88aa;}.iex_mpreview_vcontrols_button_mouse_controller:hover+.iex_svg_volume_button>.iex_svg_button_scale_group>.iex_svg_volume_button_wave_mute_icon>.iex_svg_volume_button_wave_mute_icon_polygon{fill:#a00000;}";

				// Append style
				this.stylesheet = $("style");
				this.stylesheet.textContent = stylesheet_text;
				on_ready(on_insert_stylesheet.bind(this, this.stylesheet), on_insert_stylesheet_condition);
			},
			insert_stylesheet_annotations: function () {
				// Create stylesheet
				var stylesheet_text = ".iex_quick_reply_extra{margin:0.5em 0.125em 0.25em;}.iex_quick_reply_extra.iex_quick_reply_extra_4chanx{}.iex_quick_reply_label_table{display:table;width:100%;}.iex_quick_reply_label_row{display:table-row;height:100%;}.iex_quick_reply_label_cell{display:table-cell;width:100%;height:100%;vertical-align:middle;}.iex_quick_reply_label_cell.iex_quick_reply_label_cell_small{width:0;}.iex_quick_reply_label,#qr label.iex_quick_reply_label{display:inline-block;vertical-align:middle;text-transform:none;font-size:1em !important;}.iex_quick_reply_label_check{vertical-align:middle;margin:0.125em;padding:0;}.iex_quick_reply_label_check+.riceCheck{vertical-align:middle;}.iex_quick_reply_label_text{vertical-align:middle;}.iex_quick_reply_help_link_container{white-space:nowrap;display:inline-block;vertical-align:middle;}.iex_quick_reply_extra:not(.iex_quick_reply_extra_enabled) .iex_quick_reply_help_link_container{display:none;}.iex_quick_reply_help_link{cursor:pointer;display:inline-block;padding:0 0.125em;}.iex_quick_reply_content{font-size:0.8em;}.iex_quick_reply_extra:not(.iex_quick_reply_extra_enabled)>.iex_quick_reply_content{display:none;}.iex_quick_reply_info{margin:0.25em 0;}.iex_quick_reply_source_selection{}.iex_quick_reply_source_selection_text{vertical-align:middle;}.iex_quick_reply_source_selection_file,.iex_quick_reply_source_selection_url{padding:0.125em 0.25em;vertical-align:middle;font-size:inherit !important;}.iex_quick_reply_source_selection_file.iex_quick_reply_source_selection_file_selected,.iex_quick_reply_source_selection_url.iex_quick_reply_source_selection_url_selected{font-weight:bold;}.iex_quick_reply_source_selection_post{padding:0.125em 0.25em;vertical-align:middle;font-size:inherit !important;width:8em;}.iex_quick_reply_source_selection_post.iex_quick_reply_source_selection_post_selected{font-weight:bold;}.iex_quick_reply_source_selection_post_option{font-weight:bold;font-size:inherit !important}.iex_quick_reply_source_selection_post_option_default{opacity:0.5;font-weight:normal;font-size:inherit !important}.iex_quick_reply_error{margin-top:0.25em;font-weight:bold;font-style:italic;}.iex_quick_reply_error:not(.iex_quick_reply_error_visible){display:none;}.iex_quick_reply_file_input{display:none;visibility:hidden;}#quickReply,#quickReply~.preview,#quickReply~.dd-menu{z-index:20;}.iex_annotation_editor_outer_paddings{padding:1em;}.iex_annotation_editor{display:block;position:fixed;z-index:19;}.iex_annotation_editor:not(.iex_annotation_editor_visible){display:none;}.iex_annotation_editor{box-shadow:0em 0em 0.6em 0.3em rgba(0,0,0,0.75);background-color:rgba(255,255,255,0.75);}.iex_annotation_editor.iex_dark{box-shadow:0em 0em 0.6em 0.3em rgba(255,255,255,0.75);background-color:rgba(0,0,0,0.75);}.iex_ae_t1c1{display:table;table-layout:fixed;width:100%;height:100%;}.iex_ae_t1c2{display:table-row;width:100%;height:100%;}.iex_ae_t1c3{display:table-cell;width:auto;height:100%;vertical-align:top;}.iex_ae_t1c3:not(:first-of-type){width:16em;height:100%;}.iex_ae_t1c4{display:block;width:100%;height:100%;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t1c1{display:block;width:100%;height:100%;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t1c2{display:table;table-layout:fixed;width:100%;height:100%;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t1c3{display:table-row;width:100%;height:100%;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t1c3:not(:first-of-type){width:100%;height:16em;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t1c4{display:table-cell;width:100%;height:inherit;vertical-align:top;}.iex_ae_t2c0{display:block;width:100%;height:100%;box-sizing:border-box;-moz-box-sizing:border-box;xpadding:0 0.25em;border-style:solid;border-color:#111111;border-width:0 0 0 0.125em;background:#eeeeee;}.iex_ae_t2c0.iex_dark{background:#111111;border-color:#eeeeee;}.iex_ae_t2c1{display:block;width:100%;height:100%;}.iex_ae_t2c2{display:table;table-layout:fixed;width:100%;height:100%;}.iex_ae_t2c3{display:table-row;width:100%;height:100%;}.iex_ae_t2c3:first-of-type{width:100%;height:auto;}.iex_ae_t2c4{display:table-cell;width:100%;height:inherit;vertical-align:top;}.iex_ae_t2c3:not(:first-of-type)>.iex_ae_t2c4{vertical-align:bottom;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t2c0{xpadding:0.25em 0;border-width:0.125em 0 0 0;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t2c1{display:table;table-layout:auto;width:100%;height:100%;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t2c2{display:table-row;width:100%;height:100%;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t2c3{display:table-cell;width:100%;height:100%;vertical-align:bottom;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t2c3:first-of-type{width:auto;height:100%;vertical-align:middle;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t2c4{display:inline-block;width:100%;height:100%;vertical-align:middle;}.iex_ae_t3c0{text-align:center;padding:0 0.25em;}.iex_ae_t3c1{display:inline-block;vertical-align:middle;padding:0.25em;text-align:left;border-box;-moz-box-sizing:border-box;}.iex_ae_t3c2{display:block;width:auto;height:100%;border-box;-moz-box-sizing:border-box;}.iex_ae_t3c3{display:table;table-layout:fixed;width:auto;height:100%;}.iex_ae_t3c4{display:table-row;width:100%;height:auto;}.iex_ae_t3c5{display:table-cell;width:100%;height:inherit;vertical-align:top;}.iex_ae_t3c4:first-of-type>.iex_ae_t3c5{text-align:center;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t3c0{padding:0.25em 0;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t3c1{width:100%;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t3c2{display:table;table-layout:auto;width:100%;height:100%;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t3c3{display:table-row;width:100%;height:100%;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t3c4{display:table-cell;width:100%;height:100%;vertical-align:top;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t3c4:first-of-type{width:0;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t3c5{display:block;width:100%;height:100%;box-sizing:border-box;-moz-box-sizing:border-box;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t3c4+.iex_ae_t3c4>.iex_ae_t3c5{padding-left:0.25em;}.iex_ae_t4c0{display:inline-block;vertical-align:middle;}.iex_ae_t4c1{display:table;table-layout:fixed;height:100%;}.iex_ae_t4c2{display:table-row;width:100%;height:100%;}.iex_ae_t4c3{display:table-cell;width:auto;height:100%;vertical-align:middle;}.iex_ae_t4c4{display:block;width:100%;white-space:nowrap;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t4c1{height:100%;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t4c2{display:table;table-layout:auto;width:100%;height:100%;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t4c3{display:table-row;width:100%;height:auto;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t4c4{display:table-cell;width:100%;height:inherit;vertical-align:top;text-align:center;}.iex_ae_color_selector{padding:0.125em;cursor:pointer;position:relative;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_color_selector{display:inline-block;vertical-align:middle;}.iex_ae_color_selector_inner{width:1em;height:1em;position:relative;}.iex_ae_color_selector_border{position:absolute;left:0;top:0;right:0;bottom:0;border:1px solid #000000;opacity:0.5;}.iex_ae_color_selector_border:after{content:\"\";display:block;box-sizing:border-box;-moz-box-sizing:border-box;width:100%;height:100%;border:1px solid #ffffff;}.iex_ae_color_selector_border.iex_dark{border-color:#ffffff;}.iex_ae_color_selector_border.iex_dark:after{border-color:#000000;}.iex_ae_color_selector.iex_ae_color_selector_disabled>.iex_ae_color_selector_inner{background-color:#b0b0b0 !important;}.iex_ae_color_selector.iex_ae_color_selector_disabled>.iex_ae_color_selector_inner.iex_dark{background-color:#404040 !important;}.iex_ae_color_selector_border2{position:absolute;left:0;top:0;right:0;bottom:0;border:2px solid #ffffff;}.iex_ae_color_selector_border2_alt{border-color:#808080;}.iex_ae_color_selector:not(.iex_ae_color_selector_selected)>.iex_ae_color_selector_inner>.iex_ae_color_selector_border2{display:none;}.iex_ae_t5c1{display:table;font-size:0.8em;margin-top:0.5em;}.iex_ae_t5c2{display:table-row;}.iex_ae_t5c3{display:table-cell;vertical-align:middle;}.iex_ae_t5c3:nth-of-type(n+2){padding-left:0.5em;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t5c1{display:block;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t5c2{display:block;text-align:center;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t5c3{display:block;padding-left:0;text-align:left;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t5c3+.iex_ae_t5c3{margin-top:1em;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t5c3.iex_ae_t5c3_small_space+.iex_ae_t5c3{margin-top:0.25em;}.iex_ae_new_annotation_button{display:inline-block;width:3em;height:3em;border:0.15em dashed #000000;text-align:center;white-space:nowrap;line-height:0;font-weight:bold;cursor:pointer;}.iex_ae_new_annotation_button.iex_dark{border-color:#ffffff;}.iex_ae_new_annotation_button:hover{background-color:#ffffff;color:#000000;}.iex_ae_new_annotation_button.iex_dark:hover{background-color:#000000;color:#ffffff;}.iex_ae_new_annotation_button{transition:background-color 0.25s ease-in-out 0s,color 0.25s ease-in-out 0s;}.iex_ae_new_annotation_button:before{content:\"\";display:inline-block;vertical-align:middle;width:0;height:100%;}.iex_ae_new_annotation_button_inner{display:inline-block;vertical-align:middle;white-space:normal;line-height:normal;}select.iex_ae_selection{display:block;font-size:inherit !important;}.iex_ae_selection:disabled{opacity:0.5;}.iex_ae_selection_option{}.iex_ae_option_label{display:block;white-space:nowrap;}.iex_ae_option_checkbox{vertical-align:middle;margin:0;padding:0;}.iex_ae_option_checkbox+.riceCheck{vertical-align:middle;}.iex_ae_option_label_text{vertical-align:middle;}.iex_ae_option_checkbox:disabled,.iex_ae_option_checkbox:disabled+.riceCheck,.iex_ae_option_checkbox:disabled~.iex_ae_option_label_text{opacity:0.5;}.iex_ae_option_checkbox.iex_ae_option_checkbox_bold:checked+.iex_ae_option_label_text{font-weight:bold;}.iex_ae_option_checkbox.iex_ae_option_checkbox_italic:checked+.iex_ae_option_label_text{font-style:italic;}.iex_ae_option_label+.iex_ae_option_label,.iex_ae_selection+.iex_ae_selection{margin-top:0.25em;}.iex_ae_t6c0{display:block;width:100%;height:100%;position:relative;}.iex_ae_t6c1{position:absolute;left:0;top:0;bottom:0;right:0;overflow-x:hidden;overflow-y:auto;white-space:0;line-height:0;}.iex_ae_t6c1:after{content:\"\";display:inline-block;vertical-align:bottom;width:0;height:100%;}.iex_ae_t6c2{display:inline-block;vertical-align:bottom;white-space:normal;box-sizing:border-box;-moz-box-sizing:border-box;line-height:normal;width:100%;padding:0 0.25em 0.25em;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t6c2{padding-top:0.25em;}.iex_ae_add_annotation_container_empty{text-align:center;}.iex_ae_add_annotation_container_empty:not(.iex_ae_add_annotation_container_empty_visible){display:none;}.iex_ae_add_annotation_container_empty_link{cursor:pointer;}.iex_ae_restore_annotations_container_empty{text-align:center;}.iex_ae_restore_annotations_container_empty:not(.iex_ae_restore_annotations_container_empty_visible){display:none;}.iex_ae_restore_annotations_container_empty_link{cursor:pointer;}.iex_ae_annotation_editor{}.iex_ae_annotation_editor+.iex_ae_annotation_editor{margin-top:0.5em;}.iex_ae_annotation_editor.iex_ae_annotation_editor_selected{}.iex_ae_annotation_editor_top{display:table;width:100%;font-size:0.8em;}.iex_ae_annotation_editor_top_row{display:table-row;height:100%;}.iex_ae_annotation_editor_top_cell{display:table-cell;height:100%;width:100%;white-space:nowrap;text-align:right;vertical-align:bottom;}.iex_ae_annotation_editor_top_cell:first-child{width:0;text-align:left;}.iex_ae_annotation_editor_number{display:inline-block;line-height:1em;opacity:0.8;}.iex_ae_annotation_button{position:relative;display:inline-block;cursor:pointer;width:1.5em;height:1.5em;margin-left:0.25em;background-color:#e5e5e5;color:#111111;border:1px solid #aaaaaa;border-bottom:none;text-align:center;line-height:0;vertical-align:bottom;}.iex_ae_annotation_button.iex_dark{color:#eeeeee;}.iex_ae_annotation_button.iex_dark{background-color:#1a1a1a;border-color:#555555;}.iex_ae_annotation_button:after{content:\"\";display:inline-block;vertical-align:middle;width:0;height:100%;}.iex_ae_annotation_button_inner{display:inline-block;vertical-align:middle;line-height:1em;height:1em;position:relative;}.iex_ae_annotation_button.iex_ae_annotation_button_small_text>.iex_ae_annotation_button_inner{font-size:0.8em;line-height:1em;}.iex_ae_annotation_button>.iex_ae_annotation_button_inner_color{display:block;position:absolute;left:0;top:0;right:0;bottom:0;padding:0.25em;height:auto;}.iex_ae_annotation_button>.iex_ae_annotation_button_color_text{font-size:0.8em;text-shadow:1px 1px 0 #ffffff;}.iex_ae_annotation_button>.iex_ae_annotation_button_color_text.iex_dark{text-shadow:1px 1px 0 #000000;}.iex_ae_annotation_button_color{display:block;width:100%;height:100%;box-sizing:border-box;-moz-box-sizing:border-box;border:1px solid rgba(0,0,0,0.5);}.iex_ae_annotation_button_color.iex_dark{border-color:rgba(255,255,255,0.5);}.iex_ae_annotation_button_color:after{content:\"\";display:block;width:100%;height:100%;box-sizing:border-box;-moz-box-sizing:border-box;border:1px solid rgba(255,255,255,0.5);}.iex_ae_annotation_button_color.iex_dark:after{border-color:rgba(0,0,0,0.5);}.iex_ae_annotation_button.iex_ae_annotation_button_align{position:relative;}.iex_ae_annotation_button.iex_ae_annotation_button_align>.iex_ae_annotation_button_inner{position:absolute;width:33.33%;height:33.33%;background-color:#000000;opacity:0.5;}.iex_ae_annotation_button.iex_ae_annotation_button_align>.iex_ae_annotation_button_inner.iex_dark{background-color:#ffffff;}.iex_ae_annotation_button.iex_ae_annotation_button_align_left>.iex_ae_annotation_button_inner{left:0;}.iex_ae_annotation_button.iex_ae_annotation_button_align_right>.iex_ae_annotation_button_inner{right:0;}.iex_ae_annotation_button.iex_ae_annotation_button_align_center>.iex_ae_annotation_button_inner{left:50%;margin-left:-16.66%}.iex_ae_annotation_button.iex_ae_annotation_button_align_justify>.iex_ae_annotation_button_inner{left:0;width:100%;}.iex_ae_annotation_button.iex_ae_annotation_button_valign_top>.iex_ae_annotation_button_inner{top:0;}.iex_ae_annotation_button.iex_ae_annotation_button_valign_bottom>.iex_ae_annotation_button_inner{bottom:0;}.iex_ae_annotation_button.iex_ae_annotation_button_valign_middle>.iex_ae_annotation_button_inner{top:50%;margin-top:-16.66%}.iex_ae_annotation_editor_bottom{position:relative;}.iex_ae_annotation_editor_bottom:after{content:\"\";display:block;position:absolute;left:0;top:0;bottom:0;right:0;border:0.125em dashed #c0c0c0;pointer-events:none;visibility:hidden;}input.iex_ae_annotation_editor_input_text{position:relative;font-size:inherit !important;font-family:inherit !important;padding:0.25em !important;border:1px solid #808080 !important;background-color:#dddddd !important;box-sizing:border-box;-moz-box-sizing:border-box;width:100%;background-color:#d0d0d0;}input.iex_ae_annotation_editor_input_text:hover{background-color:#c0c0c0 !important;}input.iex_ae_annotation_editor_input_text.iex_dark{background-color:#202020 !important;}input.iex_ae_annotation_editor_input_text.iex_dark:hover{background-color:#303030 !important;}input[type=text].iex_ae_annotation_editor_input_text:focus{border-color:#404040 !important;}.iex_ae_annotation_editor_input_text.iex_dark{background-color:#222222 !important;}input[type=text].iex_ae_annotation_editor_input_text.iex_dark:focus{border-color:#b0b0b0 !important;}.iex_ae_annotation_editor.iex_ae_annotation_editor_selected>.iex_ae_annotation_editor_bottom:after{visibility:visible;}.iex_ae_annotation_editor.iex_ae_annotation_editor_selected>.iex_ae_annotation_editor_bottom>input.iex_ae_annotation_editor_input_text[type=text],.iex_ae_annotation_editor.iex_ae_annotation_editor_selected>.iex_ae_annotation_editor_bottom>input.iex_ae_annotation_editor_input_text[type=text]:focus{border-color:rgba(0,0,0,0) !important;}.iex_ae_t7c0{font-size:0.8em;text-align:left;margin-bottom:0.375em;}.iex_ae_t7c1{display:inline-block;vertical-align:baseline;}.iex_ae_t7c1+.iex_ae_t7c1{margin-left:0.75em;}.iex_ae_top_color_link{cursor:pointer;border-bottom:0.125em solid rgba(0,0,0,0);}.iex_ae_top_color_link.iex_ae_top_color_link_selected{border-bottom-color:#000000;color:#000000;}.iex_ae_top_color_link.iex_ae_top_color_link_selected.iex_dark{border-bottom-color:#ffffff;color:#ffffff;}.iex_ae_top_label{font-weight: bold;}.iex_ae_top_hyphen{display:none;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_top_hyphen{display:inline;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t7c0{margin-bottom:0;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_t7c1{margin-left:0;margin-bottom:0.375em;display:block;text-align:center;}.iex_annotation_editor.iex_annotation_editor_vertical .iex_ae_top_label{display:block;}.iex_ae_preview_container{position:relative;width:100%;height:100%;}.iex_ae_preview_image{border:none;padding:0;margin:0;display:block;}.iex_ae_preview_image:not(.iex_ae_preview_image_visible){visibility:hidden;}.iex_ae_preview_image.iex_ae_preview_image_visible{width:100%;height:100%;}.iex_ae_preview_message{position:absolute;left:0;top:0;bottom:0;right:0;white-space:0;line-height:0;text-align:center;}.iex_ae_preview_message:before{content:\"\";display:inline-block;vertical-align:middle;width:0;height:100%;}.iex_ae_preview_message_inner{display:inline-block;vertical-align:middle;white-space:normal;line-height:normal;font-weight:bold;color:#000000;text-shadow:0 0 0.125em #ffffff;}.iex_ae_preview_message_inner.iex_dark{color:#ffffff;text-shadow:0 0 0.125em #000000;}.iex_annotation_overlay{position:absolute;left:0;top:0;bottom:0;right:0;text-align:left;}.iex_annotation_container{position:relative;width:100%;height:100%;transform-origin:0 0;-o-transform-origin:0 0;-moz-transform-origin:0 0;-webkit-transform-origin:0 0;}.iex_ae_preview_scroll_h{position:absolute;left:0;right:0;bottom:0;height:0.5em;pointer-events:none;}.iex_ae_preview_scroll_h_inner{height:100%;}.iex_ae_preview_scroll_v{position:absolute;top:0;bottom:0;right:0;width:0.5em;pointer-events:none;}.iex_ae_preview_scroll_v_inner{width:100%;}.iex_ae_preview_scroll_h:not(.iex_ae_preview_scroll_visible),.iex_ae_preview_scroll_v:not(.iex_ae_preview_scroll_visible),.iex_ae_preview_scroll_h:not(.iex_ae_preview_scroll_displayable),.iex_ae_preview_scroll_v:not(.iex_ae_preview_scroll_displayable){display:none;}.iex_ae_preview_scroll_h_inner,.iex_ae_preview_scroll_v_inner{position:absolute;border-radius:0.25em;background-color:#ffffff;box-sizing:border-box;-moz-box-sizing:border-box;border:1px solid #000000;opacity:0.25;}.iex_ae_preview_scroll_h_inner.iex_dark,.iex_ae_preview_scroll_v_inner.iex_dark{background-color:#000000;border-color:#ffffff;}.iex_ae_preview_scroll_h.iex_ae_preview_scroll_displayable.iex_ae_preview_scroll_visible+.iex_ae_preview_scroll_v{bottom:0.5em;}.iex_annotation{position:absolute;z-index:0;pointer-events:auto;}.iex_annotation.iex_annotation_scaling_px{}.iex_annotation:hover{z-index:1}.iex_annotation.iex_annotation_selected{z-index:2;}.iex_annotation_borders{position:absolute;left:0;top:0;bottom:0;right:0;}.iex_annotation_border{position:absolute;}.iex_annotation_border_top_left{cursor:nwse-resize;left:-16px;top:-16px;width:32px;height:32px;}.iex_annotation_border_top_right{cursor:nesw-resize;right:-16px;top:-16px;width:32px;height:32px;}.iex_annotation_border_bottom_left{cursor:nesw-resize;left:-16px;bottom:-16px;width:32px;height:32px;}.iex_annotation_border_bottom_right{cursor:nwse-resize;right:-16px;bottom:-16px;width:32px;height:32px;}.iex_annotation_border_top{cursor:ns-resize;left:16px;right:16px;top:-16px;height:32px;}.iex_annotation_border_bottom{cursor:ns-resize;left:16px;right:16px;bottom:-16px;height:32px;}.iex_annotation_border_left{cursor:ew-resize;left:-16px;width:32px;top:16px;bottom:16px;}.iex_annotation_border_right{cursor:ew-resize;right:-16px;width:32px;top:16px;bottom:16px;}.iex_annotation_content{position:absolute;left:0;top:0;bottom:0;right:0;line-height:0;white-space:nowrap;text-align:center;}.iex_annotation.iex_annotation_editing>.iex_annotation_content{cursor:move;}.iex_annotation_content:before{content:\"\";display:inline-block;vertical-align:middle;width:0;height:100%;}.iex_annotation_content2{line-height:normal;white-space:normal;display:inline-block;width:100%;vertical-align:middle;position:relative;pointer-events:none;}.iex_annotation_content3{display:inline-block;line-height:1.2em;cursor:default;pointer-events:auto;}.iex_annotation.iex_annotation_editing .iex_annotation_content3{cursor:move;pointer-events:auto;}.iex_annotation_content3.iex_annotation_content_bold{font-weight:bold;}.iex_annotation_content3.iex_annotation_content_italic{font-style:italic;}.iex_annotation_format_bold{font-weight:bold;}.iex_annotation_content3.iex_annotation_content_bold>.iex_annotation_format_bold{font-weight:normal;}.iex_annotation_format_italic{font-style:italic;}.iex_annotation_content3.iex_annotation_content_italic>.iex_annotation_format_italic{font-style:normal;}.iex_annotation_background{position:absolute;left:0;top:0;bottom:0;right:0;background:#ffffff;opacity:0.25;}.iex_annotation_outline{position:absolute;left:0;top:0;bottom:0;right:0;border:2px solid #000000;opacity:0.25;}.iex_annotation.iex_annotation_selected>.iex_annotation_background{opacity:0.5;}.iex_annotation.iex_annotation_selected>.iex_annotation_outline{border-style:dashed;opacity:0.5;}.iex_annotation:not(.iex_annotation_editing)>.iex_annotation_background{opacity:0.5;transition:opacity 0.25s ease-in-out 0s;}.iex_annotation:not(.iex_annotation_editing):hover>.iex_annotation_background{opacity:0.8;}.iex_annotation:not(.iex_annotation_editing).iex_annotation_transparent>.iex_annotation_background{opacity:0.25;}.iex_annotation:not(.iex_annotation_editing).iex_annotation_transparent:hover>.iex_annotation_background{opacity:0.75;}.iex_annotation:not(.iex_annotation_editing)>.iex_annotation_outline{opacity:0.5;transition:opacity 0.25s ease-in-out 0s;}.iex_annotation:not(.iex_annotation_editing):hover>.iex_annotation_outline{opacity:0.5;}.iex_annotation:not(.iex_annotation_editing).iex_annotation_transparent>.iex_annotation_outline{opacity:0.25;}.iex_annotation:not(.iex_annotation_editing).iex_annotation_transparent:hover>.iex_annotation_outline{opacity:0.8;}.iex_annotation:not(.iex_annotation_editing)>.iex_annotation_content{opacity:1;transition:opacity 0.25s ease-in-out 0s;}.iex_annotation:not(.iex_annotation_editing):hover>.iex_annotation_content{opacity:1;}.iex_annotation:not(.iex_annotation_editing).iex_annotation_transparent>.iex_annotation_content{opacity:0.5;}.iex_annotation:not(.iex_annotation_editing).iex_annotation_transparent:hover>.iex_annotation_content{opacity:1;}.iex_annotation.iex_annotation_align_left>.iex_annotation_content{text-align:left;}.iex_annotation.iex_annotation_align_right>.iex_annotation_content{text-align:right;}.iex_annotation.iex_annotation_align_center>.iex_annotation_content{text-align:center;}.iex_annotation.iex_annotation_align_justify>.iex_annotation_content{text-align:justify;}.iex_annotation.iex_annotation_valign_top>.iex_annotation_content>.iex_annotation_content2{vertical-align:top;}.iex_annotation.iex_annotation_valign_middle>.iex_annotation_content>.iex_annotation_content2{vertical-align:middle;}.iex_annotation.iex_annotation_valign_bottom>.iex_annotation_content>.iex_annotation_content2{vertical-align:bottom;}.iex_annotation.iex_annotation_valign_x>.iex_annotation_content:before{content:none;}.iex_annotation.iex_annotation_align_x>.iex_annotation_content>.iex_annotation_content2,.iex_annotation.iex_annotation_valign_x>.iex_annotation_content>.iex_annotation_content2{position:absolute;display:inline-block;left:0;top:0;width:100%;}.iex_annotation.iex_annotation_valign_x.iex_annotation_valign_top>.iex_annotation_content>.iex_annotation_content2{top:0;}.iex_annotation.iex_annotation_valign_x.iex_annotation_valign_bottom>.iex_annotation_content>.iex_annotation_content2{bottom:0;top:auto;}.iex_annotation.iex_annotation_valign_x.iex_annotation_valign_middle>.iex_annotation_content>.iex_annotation_content2{top:50%;}.iex_annotation.iex_annotation_valign_x.iex_annotation_valign_middle>.iex_annotation_content>.iex_annotation_content2>.iex_annotation_content3{transform:translate(0,-50%);}.iex_annotation.iex_annotation_align_x.iex_annotation_align_left>.iex_annotation_content>.iex_annotation_content2{left:0;}.iex_annotation.iex_annotation_align_x.iex_annotation_align_right>.iex_annotation_content>.iex_annotation_content2{right:0;left:auto;}.iex_annotation.iex_annotation_align_x.iex_annotation_align_center>.iex_annotation_content>.iex_annotation_content2,.iex_annotation.iex_annotation_align_x.iex_annotation_align_justify>.iex_annotation_content>.iex_annotation_content2{left:50%;text-align:left;}.iex_annotation.iex_annotation_align_x.iex_annotation_align_center>.iex_annotation_content>.iex_annotation_content2>.iex_annotation_content3{transform:translate(-50%,0);text-align:center;}.iex_annotation.iex_annotation_align_x.iex_annotation_align_justify>.iex_annotation_content>.iex_annotation_content2>.iex_annotation_content3{transform:translate(-50%,0);text-align:justify;min-width:100%;}.iex_annotation.iex_annotation_align_x.iex_annotation_align_center.iex_annotation_valign_x.iex_annotation_valign_middle>.iex_annotation_content>.iex_annotation_content2>.iex_annotation_content3,.iex_annotation.iex_annotation_align_x.iex_annotation_align_justify.iex_annotation_valign_x.iex_annotation_valign_middle>.iex_annotation_content>.iex_annotation_content2>.iex_annotation_content3{transform:translate(-50%,-50%)}.iex_annotation_standalone_overlay{display:block;position:absolute;z-index:1;pointer-events:none;font-size:1px;}";

				// Append style
				this.stylesheet_annotations = $("style");
				this.stylesheet_annotations.textContent = stylesheet_text;
				on_ready(on_insert_stylesheet.bind(this, this.stylesheet_annotations), on_insert_stylesheet_condition);
			},

			theme_detect: function () {
				// Theme detection
				var doc_el = document.documentElement,
					body = document.querySelector("body"),
					bg_colors, bg_color, e, i, j, a, a_inv;

				if (doc_el && body) {
					// Get background colors
					e = $("div");
					e.className = "post reply";

					bg_colors = [
						this.parse_css_color(this.get_true_style(doc_el, "backgroundColor")),
						this.parse_css_color(this.get_true_style(body, "backgroundColor")),
						this.parse_css_color(this.get_true_style(e, "backgroundColor")),
					];

					// Average
					bg_color = bg_colors[0];
					for (i = 1; i < bg_colors.length; ++i) {
						// Alphas
						a = bg_colors[i][3];
						a_inv = (1.0 - a) * bg_color[3];

						// Modify
						for (j = 0; j < 3; ++j) {
							bg_color[j] = (bg_color[j] * a_inv + bg_colors[i][j] * a);
						}
						bg_color[3] = Math.max(bg_color[3], a);
					}

					if (bg_color[3] > 0) {
						// Found
						return (bg_color[0] + bg_color[1] + bg_color[2] < (256 + 128)) ? "iex_dark" : "iex_light";
					}
				}

				// Not found
				return null;
			},
			theme_change: function (new_theme) {
				var theme_old = this.theme.trim(),
					theme_old_nodes, i, n;

				// No change
				if (theme_old == new_theme) return;

				// Update
				this.theme = " " + new_theme;

				// Replace themes
				theme_old_nodes = document.querySelectorAll("." + theme_old);
				for (i = 0; i < theme_old_nodes.length; ++i) {
					n = theme_old_nodes[i];
					this.remove_class(n, theme_old);
					n.className += this.theme;
				}
			},

			has_class: function (element, classname) {
				return (new RegExp("(\\s|^)" + classname + "(\\s|$)")).test(element.className);
			},
			add_class: function (element, classname) {
				if (element.classList) {
					element.classList.add(classname);
				}
				else {
					if (!(new RegExp("(\\s|^)" + classname + "(\\s|$)")).test(element.className)) {
						element.className = (element.className + " " + classname).trim();
					}
				}
			},
			remove_class: function (element, classname) {
				if (element.classList) {
					// classList
					element.classList.remove(classname);
				}
				else {
					// Regex
					var reg = new RegExp("(\\s|^)" + classname + "(\\s|$)", "g"),
						newCls = element.className.replace(reg, " ").trim();

					if (newCls != element.className) element.className = newCls;
				}
			},
			remove_classes: function (element, classnames) {
				// Split
				var i;
				classnames = classnames.split(" ");

				if (element.classList) {
					// classList
					for (i = 0; i < classnames.length; ++i) {
						element.classList.remove(classnames[i]);
					}
				}
				else {
					// Regex
					var newCls = element.className,
						reg;

					for (i = 0; i < classnames.length; ++i) {
						reg = new RegExp("(\\s|^)" + classnames[i] + "(\\s|$)", "g");
						newCls = newCls.replace(reg, " ").trim();
					}

					if (newCls != element.className) element.className = newCls;
				}
			},

			has_class_svg: function (element, classname) {
				return (new RegExp("(\\s|^)" + classname + "(\\s|$)")).test(element.getAttribute("class"));
			},
			add_class_svg: function (element, classname) {
				if (!(new RegExp("(\\s|^)" + classname + "(\\s|$)")).test(element.getAttribute("class"))) {
					element.setAttribute("class", (element.getAttribute("class") + " " + classname).trim());
				}
			},
			remove_class_svg: function (element, classname) {
				// Regex
				var reg = new RegExp("(\\s|^)" + classname + "(\\s|$)", "g"),
					oldCls = element.getAttribute("class"),
					newCls = oldCls.replace(reg, " ").trim();

				if (newCls != oldCls) element.setAttribute("class", newCls);
			},
			remove_classes_svg: function (element, classnames) {
				var oldCls = element.getAttribute("class"),
					newCls = oldCls,
					i, reg;

				// Split
				classnames = classnames.split(" ");

				for (i = 0; i < classnames.length; ++i) {
					reg = new RegExp("(\\s|^)" + classnames[i] + "(\\s|$)", "g");
					newCls = newCls.replace(reg, " ").trim();
				}

				if (newCls != oldCls) element.setAttribute("class", newCls);
			},
			change_classes_svg: function (element, classnames_add, classnames_remove) {
				var oldCls = element.getAttribute("class"),
					newCls = oldCls,
					i, reg;

				// Add classes
				if (classnames_add) {
					classnames_add = classnames_add.split(" ");
					for (i = 0; i < classnames_add.length; ++i) {
						reg = new RegExp("(\\s|^)" + classnames_add[i] + "(\\s|$)");
						if (!reg.test(newCls)) {
							newCls += " " + classnames_add[i];
						}
					}
				}

				// Remove classes
				if (classnames_remove) {
					classnames_remove = classnames_remove.split(" ");
					for (i = 0; i < classnames_remove.length; ++i) {
						reg = new RegExp("(\\s|^)" + classnames_remove[i] + "(\\s|$)", "g");
						newCls = newCls.replace(reg, " ");
					}
				}

				// Update class
				if ((newCls = newCls.trim()) != oldCls) {
					element.setAttribute("class", newCls);
				}
			},

			get_true_style_of_class: function (class_name, style_name, tag_name, parent_node) {
				var e = $(tag_name || "div"),
					s, v;

				// Set class
				e.className = class_name;

				// Add
				if (parent_node || (parent_node = document.querySelector("body"))) {
					parent_node.appendChild(e);
					v = get_true_style(e, style_name);
					parent_node.removeChild(e);
				}
				else {
					v = get_true_style(e, style_name);
				}

				// Return style
				return v;
			},
			get_true_style: get_true_style,

			parse_css_color: function (color) {
				if (/^transparent$/.test(color)) return [ 0 , 0 , 0 , 0 ];

				var m;
				if ((m = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec(color))) {
					return [
						parseInt(m[1], 16),
						parseInt(m[2], 16),
						parseInt(m[3], 16),
						1
					];
				}
				else if ((m = /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/.exec(color))) {
					return [
						Math.floor((parseInt(m[1], 16) * 255.0) / 0xF + 0.5),
						Math.floor((parseInt(m[2], 16) * 255.0) / 0xF + 0.5),
						Math.floor((parseInt(m[3], 16) * 255.0) / 0xF + 0.5),
						1
					];
				}
				else if ((m = /^rgb\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)$/.exec(color))) {
					return [
						parseInt(m[1], 10),
						parseInt(m[2], 10),
						parseInt(m[3], 10),
						1
					];
				}
				else if ((m = /^rgba\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9\.]+)\s*\)$/.exec(color))) {
					return [
						parseInt(m[1], 10),
						parseInt(m[2], 10),
						parseInt(m[3], 10),
						parseFloat(m[4])
					];
				}
				else {
					// Default
					return [ 0 , 0 , 0 , 0 ];
				}
			},
			color_to_css: function (color) {
				return "rgba(" + color[0] + "," + color[1] + "," + color[2] + "," + color[3] + ")";
			},

			get_window_rect: function () {
				var doc_el = document.documentElement,
					left = (window.pageXOffset || doc_el.scrollLeft || 0) - (doc_el.clientLeft || 0),
					top = (window.pageYOffset || doc_el.scrollTop || 0)  - (doc_el.clientTop || 0);

				return {
					left: left,
					top: top,
					right: left + (window.innerWidth || doc_el.clientWidth || 0),
					bottom: top + (window.innerHeight || doc_el.clientHeight || 0),
				};
			},
			get_document_rect: function () {
				var doc_el = document.documentElement,
					left = (window.pageXOffset || doc_el.scrollLeft || 0) - (doc_el.clientLeft || 0),
					top = (window.pageYOffset || doc_el.scrollTop || 0)  - (doc_el.clientTop || 0);

				return {
					left: left,
					top: top,
					right: left + (doc_el.clientWidth || window.innerWidth || 0),
					bottom: top + (doc_el.clientHeight || window.innerHeight || 0),
				};
			},
			get_document_size: function () {
				var doc_el = document.documentElement;

				return {
					width: (doc_el.clientWidth || window.innerWidth || 0),
					height: (doc_el.clientHeight || window.innerHeight || 0),
				};
			},
			get_document_offset: function () {
				var doc_el = document.documentElement;

				return {
					left: (window.pageXOffset || doc_el.scrollLeft || 0) - (doc_el.clientLeft || 0),
					top: (window.pageYOffset || doc_el.scrollTop || 0)  - (doc_el.clientTop || 0),
				};
			},
			get_object_rect: function (obj) {
				var bounds = obj.getBoundingClientRect(),
					doc_el = document.documentElement,
					left = (window.pageXOffset || doc_el.scrollLeft || 0) - (doc_el.clientLeft || 0),
					top = (window.pageYOffset || doc_el.scrollTop || 0)  - (doc_el.clientTop || 0);

				return {
					left: left + bounds.left,
					top: top + bounds.top,
					right: left + bounds.right,
					bottom: top + bounds.bottom,
					width: bounds.width,
					height: bounds.height,
				};
			},
			get_object_rect_relative: function (obj) {
				return obj.getBoundingClientRect();
			},
			get_object_size: function (obj) {
				var bounds = obj.getBoundingClientRect();

				return {
					width: bounds.width,
					height: bounds.height,
				};
			},
			get_object_rect_inner: function (obj) {
				// Document scroll offset
				var doc_el = document.documentElement,
					left = (window.pageXOffset || doc_el.scrollLeft || 0) - (doc_el.clientLeft || 0),
					top = (window.pageYOffset || doc_el.scrollTop || 0)  - (doc_el.clientTop || 0),
					bounds0, bounds1, bounds2, b1_w, b1_h;

				// Measure bounds with and without paddings
				bounds0 = obj.getBoundingClientRect();
				this.add_class(obj, "iex_no_padding_br");
				bounds1 = obj.getBoundingClientRect();
				this.add_class(obj, "iex_no_padding_tl");
				bounds2 = obj.getBoundingClientRect();
				this.remove_classes(obj, "iex_no_padding_br iex_no_padding_tl");

				b1_w = bounds1.right - bounds1.left;
				b1_h = bounds1.bottom - bounds1.top;

				return {
					left: left + bounds1.left + (b1_w - (bounds2.right - bounds2.left)),
					top: top + bounds1.top + (b1_h - (bounds2.bottom - bounds2.top)),
					right: left + bounds0.right + (b1_w - (bounds0.right - bounds0.left)),
					bottom: top + bounds0.bottom + (b1_h - (bounds0.bottom - bounds0.top)),
				};
			},

			get_prefixed_style: function (name) {
				// Returns
				var e = $("div"),
					prefixes, prefixes2, name2, n, i;

				name2 = name.replace(/[A-Z]/g, function (match) {
					return "-" + match.toLowerCase();
				});
				name = name.replace(/-[a-z]/g, function (match) {
					return match[1].toUpperCase();
				});

				// Un-prefixed
				if (typeof(e.style[name]) === "string") {
					return [ name, name2 ];
				}

				// Check prefixes
				prefixes = [ "webkit" , "Moz" , "O" , "ms" ];
				prefixes2 = [ "-webkit-" , "-moz-" , "-o-" , "-ms-" ];
				name = name[0].toUpperCase() + name.substr(1);

				for (i = 0; i < prefixes.length; ++i) {
					n = prefixes[i] + name;
					if (typeof(e.style[n]) === "string") {
						return [ n, prefixes2[i] + name2 ];
					}
				}

				name = name[0].toLowerCase() + name.substr(1);
				return [ name, name2 ];
			},

			add_font: function (font_name, external_name, styles) {
				if (loaded_fonts !== null && font_name in loaded_fonts) return true;

				var styles_names = [ "r", "i", "b", "bi" ],
					styles_selected = [ false, false, false, false ],
					styles_count = 0,
					base_url = "https://dnsev.github.io/web/fonts/",
					src = "",
					url, i, j, f_weight, f_style, n, p;

				// Parent node
				if (!(p = doc.head || doc.body || doc_el)) return false;

				// External name
				base_url += (external_name ? external_name : font_name) + "/";

				// Styles to load
				if (styles) {
					for (i = 0; i < styles.length; ++i) {
						j = styles_names.indexOf(arguments[i]);
						if (j >= 0) {
							styles_selected[j] = true;
							++styles_count;
						}
					}
				}

				// Must load at least one
				if (styles_count === 0) styles_selected[0] = true;

				// Prepare source string
				for (i = 0; i < styles_names.length; ++i) {
					if (!styles_selected[i]) continue;

					url = base_url + styles_names[i] + "/webfont";
					f_weight = ((i < 2) ? "normal" : "bold");
					f_style = ((i % 2) === 0 ? "normal" : "italic");

					src += "@font-face{" +
						"font-family:'" + font_name + "';" +
						"src:url('" + url + ".eot');" +
						"src:url('" + url + ".eot?#iefix') format('embedded-opentype')," +
							"url('" + url + ".woff2') format('woff2')," +
							"url('" + url + ".woff') format('woff')," +
							"url('" + url + ".ttf') format('truetype')," +
							"url('" + url + ".svg#" + external_name + "') format('svg');" +
						"font-weight:" + f_weight + ";" +
						"font-style:" + f_style + ";" +
					"}";
				}

				// Create node
				n = document.createElement("style");
				n.textContent = src;

				// State
				if (loaded_fonts === null) loaded_fonts = {};
				loaded_fonts[font_name] = n;

				// Add to doc
				p.appendChild(n);
			},

		};



		return Style;

	})();

	// Class to manage hotkey interactions
	var HotkeyManager = (function () {

		// Hotkey registration class
		var Hotkey = function (key, keyCode, modifiers, callback) {
			this.key = key;
			this.keyCode = keyCode;
			this.modifiers = modifiers;
			this.callback = callback;
		};


		// Setup key conversions
		var key_conversions = {
			"backspace": 8,
			"tab": 9,
			"enter": 13,
			"shift": 16,
			"ctrl": 17,
			"alt": 18,
			"pause": 19,
			"capslock": 20,
			"esc": 27,
			"space": 32,
			"page up": 33,
			"page down": 34,
			"end": 35,
			"home": 36,
			"left": 37,
			"up": 38,
			"right": 39,
			"down": 40,
			"insert": 45,
			"delete": 46,
			",": 188,
			".": 190,
			"/": 191,
			"`": 192,
			"[": 219,
			"\\": 220,
			"]": 221,
			"'": 222,
			";": (is_firefox || is_opera ? 59 : 186),
			"=": (is_firefox ? 107 : (is_opera ? 61 : 187)),
			"-": (is_firefox || is_opera ? 109 : 189),
			"numpad0": 96,
			"numpad1": 97,
			"numpad2": 98,
			"numpad3": 99,
			"numpad4": 100,
			"numpad5": 101,
			"numpad6": 102,
			"numpad7": 103,
			"numpad8": 104,
			"numpad9": 105,
			"numpad*": 106,
			"numpad+": 107,
			"numpad-": 109,
			"numpad.": 110,
			"numpad/": 111,
		};

		var i, iEnd, iDiff;
		for (i = "a".charCodeAt(0), iEnd = "z".charCodeAt(0), iDiff = (i - "A".charCodeAt(0)); i <= iEnd; ++i) {
			key_conversions[String.fromCharCode(i)] = i - iDiff;
		}
		for (i = "0".charCodeAt(0), iEnd = "9".charCodeAt(0); i <= iEnd; ++i) {
			key_conversions[String.fromCharCode(i)] = i;
		}
		for (i = 112; i <= 123; ++i) {
			key_conversions["f" + (i - 112)] = i;
		}


		// Constructor
		var HotkeyManager = function () {
			// Event
			document.addEventListener("keydown", this.on_window_keydown_bound = this.on_window_keydown.bind(this), false);

			// Registrations
			this.registrations = {};
		};

		// Static values
		HotkeyManager.instance = null;
		HotkeyManager.MODIFIER_NONE = 0;
		HotkeyManager.MODIFIER_SHIFT = 1;
		HotkeyManager.MODIFIER_CTRL = 2;
		HotkeyManager.MODIFIER_ALT = 4;
		HotkeyManager.MODIFIER_TYPING = 8;

		// Public functions
		HotkeyManager.prototype = {
			constructor: HotkeyManager,

			/**
				Destroy any events and/or data created by the instance that will not be necessary
				after this object is no longer used.
			*/
			destroy: function () {
				// Remove event listener
				document.removeEventListener("keydown", this.on_window_keydown_bound, false);

				// Remove registrations
				this.registrations = {};
			},

			/**
				Convert a string-based key to an integer key code.

				@param key
					The string representing a key
					This should be lowercase
				@return
					An integer value representing the key, or 0 if not found
			*/
			key_to_keycode: function (key) {
				// Convert the key
				if (key in key_conversions) {
					return key_conversions[key];
				}
				else {
					return 0;
				}
			},

			/**
				Register a hotkey event.

				@param key
					The key to use, as a lowercase string. If the string contains a non-alphabetic character,
					use whichever symbol is on the key with the shift key NOT held.
					Check key_conversions for any other issues.
				@param [modifiers]
					Bitflags of which key modifiers are required. Available values are:
						HotkeyManager.MODIFIER_NONE
						HotkeyManager.MODIFIER_SHIFT
						HotkeyManager.MODIFIER_CTRL
						HotkeyManager.MODIFIER_ALT
						HotkeyManager.MODIFIER_TYPING
					Optional parameter
				@param callback
					A callback function, which will be called using:
						callback.call(HotkeyManager.instance, keydown_event, hotkey_instance)
					If the callback function returns false, no other hotkeys registered for this key
					will recieve the event.
					The default action of the event can be canceled with:
						event.preventDefault()
						event.stopPropagation()
				@return
					An object describing the registered hotkey.
					This value can be passed to the unregister function to remove the hotkey.
			*/
			register: function (key, modifiers, callback) {
				// Argument shifting
				if (arguments.length < 3) {
					modifiers = 0;
					callback = arguments[1];
				}

				// Get key code
				var keyCode = this.key_to_keycode(key);

				// Create registration
				var reg = new Hotkey(key, keyCode, modifiers, callback);

				// Add to or create a list
				if (keyCode in this.registrations) {
					this.registrations[keyCode].push(reg);
				}
				else {
					this.registrations[keyCode] = [ reg ];
				}

				// Return
				return reg;
			},
			/**
				Unregister a hotkey event.

				@param hotkey
					The hotkey registration value returned from calling the register function.
				@return
					true if the hotkey was removed,
					false if it was not found (maybe already removed)
			*/
			unregister: function (hotkey) {
				if (hotkey.keyCode in this.registrations) {
					var list = this.registrations[hotkey.keyCode];
					for (var i = 0; i < list.length; ++i) {
						if (list[i] === hotkey) {
							// Successfully unregistered
							list.splice(i, 1);
							if (list.length === 0) {
								// Remove if empty
								delete this.registrations[hotkey.keyCode];
							}
							return true;
						}
					}
				}

				// Not found
				return false;
			},

			/**
				The event handler for the document.onkeydown event.
				This will propagate the event to any hotkeys if necessary.

				@param event
					The event event passed in
			*/
			on_window_keydown: function (event) {
				// Key and modifiers
				var key = event.which || event.keyCode || (event = window.event).keyCode;

				// Check
				if (key in this.registrations) {
					// Get modifiers
					var modifiers = (event.shiftKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.altKey ? 4 : 0);

					// Check if typing
					var t = document.activeElement;
					if (t) {
						t = t.tagName;
						if (t == "INPUT" || t == "TEXTAREA") {
							modifiers |= 8;
						}
					}

					// Get registration list
					var list = this.registrations[key];
					for (var i = 0; i < list.length; ++i) {
						// Check modifiers
						if (modifiers === list[i].modifiers) {
							// Callback
							if (list[i].callback.call(this, event, list[i]) === false) {
								break;
							}
						}
					}
				}
			}
		};

		// Return the class
		return HotkeyManager;

	})();

	// Class to re-linkify file links
	var FileLink = (function () {

		var FileLink = function () {
			// Enabled
			this.enabled = false;
			this.disabled = false;
			this.post_queue = null;
			this.modifiers = [];

			// Binds
			this.on_api_post_add_bind = on_api_post_add.bind(this);
			this.on_api_file_info_update_bind = on_api_file_info_update.bind(this);
		};



		var modify_href = function (node, filename) {
			var href = node.getAttribute("href"),
				i;

			if (href !== null) {
				href = href.replace(/#.*/, "") + "#!" + this.escape(filename);
				for (i = 0; i < this.modifiers.length; ++i) {
					href = this.modifiers[i].call(this, node, href);
				}
				node.setAttribute("href", href);
			}
		};

		var on_api_post_add = function (post_container) {
			// Queue add hooks
			this.post_queue.push(post_container);
		};
		var on_api_file_info_update = function (node) {
			var c = api.post_get_file_node_link_from_file_info(node),
				fn, n;

			if (
				c !== null &&
				(c.getAttribute("href") || "").indexOf("#") < 0 &&
				(n = api.post_get_file_container_from_file_info(node)) !== null &&
				(fn = api.post_get_file_info_from_file_info_container(n).name)
			) {
				modify_href.call(this, c, fn);
			}
		};

		var on_post_queue_callback = function (post_container) {
			// Find file
			var file_nodes = api.post_get_file_nodes(post_container),
				file_info;

			if (
				file_nodes &&
				(file_info = api.post_get_file_info(post_container)).name
			) {
				// Modify hrefs
				if (file_nodes.link !== null) {
					modify_href.call(this, file_nodes.link, file_info.name);
				}
				if (file_nodes.link_thumbnail !== null) {
					modify_href.call(this, file_nodes.link_thumbnail, file_info.name);
				}
			}
		};



		FileLink.prototype = {
			constructor: FileLink,

			destroy: function () {
				// Remove api events
				api.off("post_add", this.on_api_post_add_bind);
				api.off("file_info_update", this.on_api_file_info_update_bind);
			},

			start: function () {
				// Enable if settings allow it
				if (settings.values.file_linkification.enabled) {
					this.enable();
				}
			},
			enable: function () {
				// Enable
				if (this.enabled) return;
				this.enabled = true;

				// Get posts
				this.post_queue = Delay.queue(api.get("posts"), on_post_queue_callback.bind(this), 50, 0.25);

				// Bind post acquiring
				api.on("post_add", this.on_api_post_add_bind);
				api.on("file_info_update", this.on_api_file_info_update_bind);
			},
			disable: function () {
				// Not enabled or already disabled
				if (!this.enabled || this.disabled) return;

				// Disable
				this.disabled = true;

				// Unbind post acquiring
				api.off("post_add", this.on_api_post_add_bind);
				api.off("file_info_update", this.on_api_file_info_update_bind);
			},

			escape: function (s) {
				return encodeURIComponent(s).replace(/\%20/g, "+");
			},
			unescape: function (s) {
				return decodeURIComponent(s.replace(/\+/g, "%20"));
			},

			register_modifier: function (modifier) {
				this.modifiers.push(modifier);
			},
		};



		return FileLink;

	})();

	// Class to manage files in a separate window
	var FileView = (function () {

		var FileView = function () {
			this.events = {
				"image": [],
				"video": [],
			};
		};



		var trigger_event = function (event, data) {
			var list = this.events[event],
				i;

			for (i = 0; i < list.length; ++i) {
				list[i].call(null, data);
			}
		};



		FileView.prototype = {
			constructor: FileView,

			start: function () {
				// Auto-loop .webm's
				var n = document.body.querySelector("video");

				if (n) {
					n.loop = true;
					trigger_event.call(this, "video", n);
				}
				else if ((n = document.body.querySelector("img")) !== null) {
					trigger_event.call(this, "image", n);
				}
			},

			on: function (event, callback) {
				if (event in this.events) {
					this.events[event].push(callback);
					return true;
				}
				return false;
			},
			off: function (event, callback) {
				if (event in this.events) {
					var list = this.events[event],
						i;

					for (i = 0; i < list.length; ++i) {
						if (callback === list[i]) {
							list.splice(i, 1);
							return true;
						}
					}
				}
				return false;
			},
		};



		return FileView;

	})();

	// Hover container control
	var Hover = (function () {

		var Hover = function () {
			this.container = null;
		};



		var on_asap = function () {
			var body = document.querySelector("body");

			// Parent
			if (body) {
				// Create floating container
				this.container = $.div("iex_floating_container");

				// Append
				body.appendChild(this.container);
			}
		};
		var on_asap_condition = function () {
			return document.querySelector("body");
		};



		Hover.prototype = {
			constructor: Hover,

			destroy: function () {
				// Remove container
				var par = this.container.parentNode;
				if (par) par.removeChild(this.container);
				this.container = null;
			},

			start: function () {
				// Setup
				on_ready(on_asap.bind(this), on_asap_condition);
			},

		};



		return Hover;

	})();

	// Image hover control
	var ImageHover = (function () {

		var ImageHover = function (hover) {
			// Preview object
			this.mpreview = null;

			// Enabled
			this.enabled = false;
			this.disabled = false;
			this.current_image_container = null;
			this.current_hover_container = null;

			// Post queue
			this.post_queue = null;

			// Extension settings
			this.extensions_valid = {
				".jpg": {
					ext: "jpg",
					mime: "image/jpeg",
					type: "image",
				},
				".jpeg": {
					ext: "jpg",
					mime: "image/jpeg",
					type: "image",
				},
				".png": {
					ext: "png",
					mime: "image/png",
					type: "image",
				},
				".gif": {
					ext: "gif",
					mime: "image/gif",
					type: "image",
				},
				".webm": {
					ext: "webm",
					mime: "video/webm",
					type: "video",
				},
			};

			// Parent node
			this.hover = hover;
			this.connector = null;

			// Open and closing timers
			this.preview_open_timer = null;
			this.preview_close_timer = null;
			this.zoom_borders_hide_timer = null;
			this.mouse_hide_timer = null;

			// Events
			this.events = {
				"change": [],
			};

			// Event bindings
			this.on_image_mouseenter_bind = wrap_mouseenterleave_event(this, on_image_mouseenter);
			this.on_image_mouseleave_bind = wrap_mouseenterleave_event(this, on_image_mouseleave);
			this.on_image_connector_mouseenter_bind = wrap_mouseenterleave_event(this, on_image_connector_mouseenter);
			this.on_image_connector_mouseleave_bind = wrap_mouseenterleave_event(this, on_image_connector_mouseleave);

			this.on_image_click_bind = wrap_generic_event(this, on_image_click);

			// More bindings
			this.on_preview_close_timeout_bind = on_preview_close_timeout.bind(this);
			this.on_preview_zoom_borders_hide_timeout_bind = on_preview_zoom_borders_hide_timeout.bind(this);
			this.on_preview_mouse_hide_timeout_bind = on_preview_mouse_hide_timeout.bind(this);

			this.on_preview_image_load_bind = on_preview_image_load.bind(this);
			this.on_preview_image_error_bind = on_preview_image_error.bind(this);

			this.on_preview_video_ready_bind = on_preview_video_ready.bind(this);
			this.on_preview_video_can_play_bind = on_preview_video_can_play.bind(this);
			this.on_preview_video_can_play_through_bind = on_preview_video_can_play_through.bind(this);
			this.on_preview_video_load_bind = on_preview_video_load.bind(this);
			this.on_preview_video_error_bind = on_preview_video_error.bind(this);
			//this.on_preview_video_volume_change_bind = on_preview_video_volume_change.bind(this);

			this.on_preview_size_change_bind = on_preview_size_change.bind(this);

			this.on_preview_mouse_wheel_bind = on_preview_mouse_wheel.bind(this);
			this.on_preview_mouse_enter_bind = on_preview_mouse_enter.bind(this);
			this.on_preview_mouse_leave_bind = on_preview_mouse_leave.bind(this);
			this.on_preview_mouse_down_bind = on_preview_mouse_down.bind(this);
			this.on_preview_mouse_move_bind = on_preview_mouse_move.bind(this);
			this.on_preview_mouse_enter_main_bind = on_preview_mouse_enter_main.bind(this);

			this.on_preview_stats_zoom_control_click_bind = on_preview_stats_zoom_control_click.bind(this);

			this.on_image_connector_mousedown_bind = on_image_connector_mousedown.bind(this);

			this.on_window_resize_bind = on_window_resize.bind(this);
			this.on_window_scroll_bind = on_window_scroll.bind(this);

			this.on_api_post_add_bind = on_api_post_add.bind(this);
			this.on_api_post_remove_bind = on_api_post_remove.bind(this);
			this.on_api_image_hover_open_bind = on_api_image_hover_open.bind(this);

			// Callbacks
			this.on_settings_value_change_bind = on_settings_value_change.bind(this);
			settings.on_change(["image_expansion", "hover", "zoom_invert"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "hover", "zoom_borders_show"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "hover", "zoom_borders_hide_time"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "hover", "zoom_buttons"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "hover", "mouse_hide"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "hover", "mouse_hide_time"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "hover", "header_overlap"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "hover", "fit_large_allowed"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "hover", "display_stats"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "video", "autoplay"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "video", "loop"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "video", "mute_initially"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "video", "volume"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "video", "mini_controls"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "video", "expand_state_save"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "style", "controls_rounded_border"], this.on_settings_value_change_bind);
			settings.on_change(["image_expansion", "style", "animations_background"], this.on_settings_value_change_bind);

			// Settings
			this.hotkeys = null;
			this.mouse_x = 0;
			this.mouse_y = 0;
			this.paddings_active = false;
			this.display_window_max = {
				x: 0,
				y: 0,
				width: 0,
				height: 0,
			};
			this.settings = {
				fit_large_allowed: true,
				display_stats: 0, // 0: all, 1: some, 2: none
				zoom_limits: {
					increase: 2.0,
					decrease: 2.0,
					min: 1.0,
					max: 32.0,
				},
				zoom_margins: {
					horizontal: 0.1,
					vertical: 0.1
				},
				zoom_invert: false,
				zoom_borders_show: true,
				zoom_borders_hide_time: 500,
				zoom_buttons: true,
				mouse_hide: true,
				mouse_hide_time: 1000,
				padding_extra: {
					left: 10,
					top: 10,
					right: 10,
					bottom: 10
				},
				header_overlap: false,
				region_paddings: {
					left: 20,
					top: 20,
					right: 20,
					bottom: 20
				},
				video: {
					autoplay: 1, // 0 = not at all, 1 = asap, 2 = when it can play "through", 3 = fully loaded
					loop: true,
					mute_initially: false,
					volume: 0.5,
					mini_controls: 1, // 0 = never, 1 = when mouse is over the video, 2 = when mouse is NOT over the video, 3 = always
					expand_state_save: true
				},
				style: {
					controls_rounded_border: true,
					animations_background: true,
				},
			};
		};



		var log2 = function (x) {
			return Math.log(x) / Math.LN2;
		};

		var add_post_container_callbacks = function (post_container) {
			// Find file
			var image_container = api.post_get_image_container(post_container);
			if (image_container !== null) {
				// Add mouseover/mouseout/click listeners
				image_container.addEventListener("mouseover", this.on_image_mouseenter_bind, false);
				image_container.addEventListener("mouseout", this.on_image_mouseleave_bind, false);
				image_container.addEventListener("click", this.on_image_click_bind, false);
			}
		};
		var remove_post_container_callbacks = function (post_container) {
			// Find file
			var image_container = api.post_get_image_container(post_container);
			if (image_container !== null) {
				// Remove listeners
				image_container.removeEventListener("mouseover", this.on_image_mouseenter_bind, false);
				image_container.removeEventListener("mouseout", this.on_image_mouseleave_bind, false);
				image_container.removeEventListener("click", this.on_image_click_bind, false);
			}
		};

		var get_image_url_extension = function (url) {
			var ext = /(\.[^\.\?#]+)(?:[\?#]|$)/.exec(url);
			return (ext === null) ? "" : ext[1].toLowerCase();
		};

		var preview_open_test = function (image_container, post_container, obey_timer) {
			// Don't open if same
			if (this.current_image_container === image_container) {
				this.preview_close_cancel();
				return;
			}

			// Get info
			if (post_container === null) post_container = api.post_get_post_container_from_image_container(image_container);
			var post_info = api.post_get_file_info(post_container);
			if (!post_info.url) return;

			// Don't open if expanded
			if (api.post_is_image_expanded_or_expanding(post_container)) return;

			// Spoiler check
			var val_check = (post_info.spoiler === null) ? settings.values.image_expansion.normal : settings.values.image_expansion.spoiler;
			if (!val_check.enabled) return;

			// Extension check
			var ext = get_image_url_extension.call(this, post_info.url);
			if (!(ext in this.extensions_valid)) return;

			// Open preview
			var time = val_check.timeout;
			if (time === 0 || !obey_timer) {
				this.preview_open(image_container, post_info, val_check.to_fit);
			}
			else {
				if (this.preview_open_timer !== null) {
					clearTimeout(this.preview_open_timer);
				}
				this.preview_open_timer = setTimeout(on_preview_open_timeout.bind(this, image_container, post_info, val_check.to_fit), time * 1000);
			}
		};

		var preview_create = function () {
			var zoom_margins = this.settings.zoom_margins;

			// Create new
			this.mpreview = new MediaPreview();

			// Setup size
			this.mpreview.set_view_borders(zoom_margins.horizontal, zoom_margins.vertical);

			// Hook events
			this.mpreview.on("image_load", this.on_preview_image_load_bind);
			this.mpreview.on("image_error", this.on_preview_image_error_bind);

			this.mpreview.on("video_ready", this.on_preview_video_ready_bind);
			this.mpreview.on("video_can_play", this.on_preview_video_can_play_bind);
			this.mpreview.on("video_can_play_through", this.on_preview_video_can_play_through_bind);
			this.mpreview.on("video_load", this.on_preview_video_load_bind);
			this.mpreview.on("video_error", this.on_preview_video_error_bind);
			//this.mpreview.on("video_volume_change", this.on_preview_video_volume_change_bind);

			this.mpreview.on("size_change", this.on_preview_size_change_bind);

			this.mpreview.on("mouse_wheel", this.on_preview_mouse_wheel_bind);
			this.mpreview.on("mouse_enter", this.on_preview_mouse_enter_bind);
			this.mpreview.on("mouse_leave", this.on_preview_mouse_leave_bind);
			this.mpreview.on("mouse_down", this.on_preview_mouse_down_bind);
			this.mpreview.on("mouse_move", this.on_preview_mouse_move_bind);
			this.mpreview.on("mouse_enter_main", this.on_preview_mouse_enter_main_bind);

			this.mpreview.on("stats_zoom_control_click", this.on_preview_stats_zoom_control_click_bind);

			window.addEventListener("resize", this.on_window_resize_bind, false);
			window.addEventListener("scroll", this.on_window_scroll_bind, false);

			// Add
			this.mpreview.add_to(this.hover.container);
		};
		var preview_detach = function (destroy) {
			if (this.mpreview === null) return;

			// Unhook events
			this.mpreview.off("image_load", this.off_preview_image_load_bind);
			this.mpreview.off("image_error", this.off_preview_image_error_bind);

			this.mpreview.off("video_ready", this.off_preview_video_ready_bind);
			this.mpreview.off("video_can_play", this.off_preview_video_can_play_bind);
			this.mpreview.off("video_can_play_through", this.off_preview_video_can_play_through_bind);
			this.mpreview.off("video_load", this.off_preview_video_load_bind);
			this.mpreview.off("video_error", this.off_preview_video_error_bind);
			this.mpreview.off("video_volume_change", this.off_preview_video_volume_change_bind);

			this.mpreview.off("size_change", this.off_preview_size_change_bind);

			this.mpreview.off("mouse_wheel", this.off_preview_mouse_wheel_bind);
			this.mpreview.off("mouse_enter", this.off_preview_mouse_enter_bind);
			this.mpreview.off("mouse_leave", this.off_preview_mouse_leave_bind);
			this.mpreview.off("mouse_down", this.off_preview_mouse_down_bind);
			this.mpreview.off("mouse_move", this.off_preview_mouse_move_bind);
			this.mpreview.off("mouse_enter_main", this.off_preview_mouse_enter_main_bind);

			this.mpreview.off("stats_zoom_control_click", this.off_preview_stats_zoom_control_click_bind);

			window.removeEventListener("resize", this.on_window_resize_bind, false);
			window.removeEventListener("scroll", this.on_window_scroll_bind, false);

			// Nullify
			if (destroy) this.mpreview.destroy();
			this.mpreview = null;
		};

		var change_zoom_level = function (delta) {
			// Update zoom
			var set = this.settings,
				size_update = false,
				zoom_limits = set.zoom_limits,
				img_size = this.mpreview.size,
				disp_size_max = this.display_window_max,
				zoom = this.mpreview.zoom,
				fit = this.mpreview.fit,
				fit_large = this.mpreview.fit_large,
				fit_large_allowed = set.fit_large_allowed,
				z, w_scale, h_scale, z_scale, scale;

			if (delta > 0) {
				// Larger
				if (fit) {
					if (fit_large || !fit_large_allowed) {
						// Zoom in
						z = zoom * zoom_limits.increase;
						if (z > zoom_limits.max) z = zoom_limits.max;
						if (zoom != z) {
							// Update
							zoom = z;
							size_update = true;
						}
					}
					else {
						// Zoom in
						w_scale = disp_size_max.width / img_size.width;
						h_scale = disp_size_max.height / img_size.height;
						z_scale = (w_scale < h_scale) ? w_scale : h_scale;

						z = zoom * zoom_limits.increase;

						w_scale = disp_size_max.width / (img_size.width * z * z_scale);
						h_scale = disp_size_max.height / (img_size.height * z * z_scale);

						if (w_scale <= 1.0 && h_scale <= 1.0) {
							// Fit large
							fit_large = true;
							zoom = 1.0;
						}
						else {
							// Not fit large
							zoom = z;
						}

						// Update
						size_update = true;
					}
				}
				else {
					// Zoom in
					z = zoom * zoom_limits.increase;
					w_scale = disp_size_max.width / (img_size.width * z);
					h_scale = disp_size_max.height / (img_size.height * z);

					if (w_scale <= 1.0 || h_scale <= 1.0) {
						// Fit
						fit = true;
						fit_large = (Math.abs(w_scale - h_scale) < 1e-5) && fit_large_allowed;
						zoom = 1.0;
					}
					else {
						// Not fit
						zoom = z;
					}

					// Update
					size_update = true;
				}
			}
			else if (delta < 0) {
				// Smaller
				if (fit) {
					if (zoom == 1.0) {
						if (fit_large) {
							// Switch to normal fit
							w_scale = disp_size_max.width / img_size.width;
							h_scale = disp_size_max.height / img_size.height;
							z_scale = (w_scale < h_scale) ? w_scale : h_scale;

							w_scale = disp_size_max.width / (img_size.width * z_scale);
							h_scale = disp_size_max.height / (img_size.height * z_scale);
							scale = (w_scale > h_scale) ? w_scale : h_scale;
							scale = Math.pow(2, Math.floor(log2(scale)));

							fit_large = false;
							zoom = scale;
							size_update = true;
						}
						else {
							// Stop fitting if possible
							w_scale = disp_size_max.width / img_size.width;
							h_scale = disp_size_max.height / img_size.height;

							if (w_scale > 1.0 && h_scale > 1.0) {
								// Calculate new zoom
								scale = (w_scale < h_scale) ? w_scale : h_scale;
								scale = Math.pow(2, Math.floor(log2(scale)));

								// Update
								fit = false;
								fit_large = false;
								zoom = scale;
								size_update = true;
							}
						}
					}
					else {
						// Zoom out
						zoom /= zoom_limits.decrease;
						if (zoom < 1.0) zoom = 1.0;

						// Update
						size_update = true;
					}
				}
				else {
					// Zoom out
					z = zoom / zoom_limits.decrease;
					if (z < zoom_limits.min) z = zoom_limits.min;

					if (zoom != z) {
						// Update
						zoom = z;
						size_update = true;
					}
				}
			}

			// Update
			if (size_update) {
				// Update
				this.mpreview.set_zoom(zoom, fit, fit_large);
				this.preview_update(true, false, false);
				update_zoom_offset.call(this);
			}
		};
		var change_zoom_borders_visibility = function (visible) {
			this.mpreview.set_view_borders_visible(visible);
			if (visible) {
				if (this.zoom_borders_hide_timer !== null) clearTimeout(this.zoom_borders_hide_timer);
				this.zoom_borders_hide_timer = setTimeout(this.on_preview_zoom_borders_hide_timeout_bind, this.settings.zoom_borders_hide_time);
			}
		};
		var change_mouse_visibility = function (visible) {
			this.mpreview.set_mouse_visible(visible);
			if (visible && this.settings.mouse_hide) {
				if (this.mouse_hide_timer !== null) clearTimeout(this.mouse_hide_timer);
				this.mouse_hide_timer = setTimeout(this.on_preview_mouse_hide_timeout_bind, this.settings.mouse_hide_time);
			}
		};

		var update_zoom_offset = function () {
			// Change zoom offset
			var rect = this.mpreview.get_inner_rect(),
				r_w = (rect.right - rect.left),
				r_h = (rect.bottom - rect.top),
				zoom_margins = this.settings.zoom_margins,
				x = (this.mouse_x - (rect.left + zoom_margins.horizontal * r_w)) / (r_w * (1.0 - 2.0 * zoom_margins.horizontal)),
				y = (this.mouse_y - (rect.top + zoom_margins.vertical * r_h)) / (r_h * (1.0 - 2.0 * zoom_margins.vertical));

			if (x < 0) x = 0;
			else if (x > 1) x = 1;
			if (y < 0) y = 0;
			else if (y > 1) y = 1;

			if (this.settings.zoom_invert) {
				x = 1.0 - x;
				y = 1.0 - y;
			}

			this.mpreview.set_offset(x, y);
		};


		var on_asap = function () {
			var body = document.querySelector("body");

			// Parent
			if (body) {
				// Create connector
				this.connector = $.div("iex_floating_image_connector");
				this.connector.addEventListener("mouseover", this.on_image_connector_mouseenter_bind, false);
				this.connector.addEventListener("mouseout", this.on_image_connector_mouseleave_bind, false);
				this.connector.addEventListener("mousedown", this.on_image_connector_mousedown_bind, false);
				this.hover.container.appendChild(this.connector);
			}
		};
		var on_asap_condition = function () {
			return document.querySelector("body");
		};

		var on_post_queue_callback = function (post_container) {
			// Add hooks
			add_post_container_callbacks.call(this, post_container);
		};

		var on_preview_image_load = function (event) {
			// Hide fallback
			this.mpreview.set_background_style(0);
		};
		var on_preview_image_error = function (event) {
			// Display fallback
			this.mpreview.set_background_style(2);

			// Show error
			this.mpreview.set_stat_status(true, event.reason, "error");
		};

		var on_preview_video_ready = function (event) {
			// Hide fallback
			this.mpreview.set_background_style(0);
		};
		var on_preview_video_can_play = function (event) {
			// Auto-play
			if (this.settings.video.autoplay == 1 && !this.mpreview.video_interacted()) {
				this.mpreview.set_video_paused(false);
			}
		};
		var on_preview_video_can_play_through = function (event) {
			// Auto-play
			if (this.settings.video.autoplay == 2 && !this.mpreview.video_interacted()) {
				this.mpreview.set_video_paused(false);
			}
		};
		var on_preview_video_load = function (event) {
			// Auto-play
			if (this.settings.video.autoplay == 3 && !this.mpreview.video_interacted()) {
				this.mpreview.set_video_paused(false);
			}
		};
		var on_preview_video_error = function (event) {
			// Display fallback
			this.mpreview.set_background_style(2);

			// Show error
			this.mpreview.set_stat_status(true, event.reason, "error");
		};
		// var on_preview_video_volume_change = function (event) {
			// Save volume changes
			// if (event.reason == "seek") {
				// settings.change_value(["image_expansion", "video", "volume"], event.volume);
				// settings.save_values();
			// }
		// };

		var on_preview_size_change = function (event) {
			// Update size
			this.mpreview.set_size(event.width, event.height, true);
			this.mpreview.set_stat_resolution(undefined, event.width + "x" + event.height);

			// Reposition
			this.preview_update(false, false, false);
		};

		var on_preview_mouse_wheel = function (event) {
			if (event.mode === 0) {
				// Zoom
				change_zoom_level.call(this, event.delta);
				if (this.settings.zoom_borders_show) change_zoom_borders_visibility.call(this, true);
			}
			else { // if (event.mode === 1) {
				// Change volume
				if (this.mpreview.get_type() === MediaPreview.TYPE_VIDEO) {
					// New volume
					var v = this.mpreview.get_video_volume() + (event.delta * 5.0 / 100.0);

					// Bound
					if (v < 0.0) v = 0.0;
					else if (v > 1.0) v = 1.0;

					// Update
					this.mpreview.set_video_volume(v);
					if (this.mpreview.get_video_muted()) {
						this.mpreview.set_video_muted(false);
					}
					this.mpreview.set_show_volume_controls_temp(true, 0.5);
				}
			}
			change_mouse_visibility.call(this, true);
		};
		var on_preview_mouse_enter = function (event) {
			// Cancel close
			this.preview_close_cancel();
			change_mouse_visibility.call(this, true);

			// Mini controls
			var mc = this.settings.video.mini_controls;
			if (mc < 3) this.mpreview.set_vcontrols_mini_visible(mc == 1);
		};
		var on_preview_mouse_leave = function (event) {
			// Delay close
			this.preview_close(false);

			// Mini controls
			var mc = this.settings.video.mini_controls;
			if (mc < 3) this.mpreview.set_vcontrols_mini_visible(mc == 2);
		};
		var on_preview_mouse_down = function (event) {
			// Close
			this.preview_close(true);
		};
		var on_preview_mouse_move = function (event) {
			// Change zoom offset
			this.mouse_x = event.x;
			this.mouse_y = event.y;
			update_zoom_offset.call(this);
			if (this.settings.zoom_borders_show) change_zoom_borders_visibility.call(this, true);
			change_mouse_visibility.call(this, true);
		};
		var on_preview_mouse_enter_main = function (event) {
			// Hide paddings
			if (this.paddings_active) {
				this.paddings_active = false;
				this.mpreview.clear_paddings();
			}
		};

		var on_preview_stats_zoom_control_click = function (event) {
			// Zoom in/out
			change_zoom_level.call(this, event.delta);
		};

		var on_preview_open_timeout = function (image_container, post_info, auto_fit) {
			// Nullify timer
			this.preview_open_timer = null;
			// Close
			this.preview_open(image_container, post_info, auto_fit);
		};
		var on_preview_close_timeout = function () {
			// Nullify timer
			this.preview_close_timer = null;
			// Close
			this.preview_close(true);
		};
		var on_preview_zoom_borders_hide_timeout = function () {
			this.zoom_borders_hide_timer = null;
			change_zoom_borders_visibility.call(this, false);
		};
		var on_preview_mouse_hide_timeout = function () {
			this.mouse_hide_timer = null;
			change_mouse_visibility.call(this, false);
		};

		var on_image_mouseenter = function (event, image_container) {
			// Don't run if disabled
			if (this.disabled) return;

			// Attempt to open
			this.current_hover_container = image_container;
			preview_open_test.call(this, image_container, null, true);
		};
		var on_image_mouseleave = function (event, image_container) {
			// Don't run if disabled
			if (this.disabled) return;

			// Close preview
			this.current_hover_container = null;
			this.preview_close(false);
		};
		var on_image_click = function (event, image_container) {
			// Don't run if disabled
			if (this.disabled) return;

			// Close preview
			setTimeout(on_image_click_delay.bind(this, image_container), 10);
		};
		var on_image_click_delay = function (image_container) {
			var post_container = api.post_get_post_container_from_image_container(image_container);
			if (post_container === null) return;

			if (api.post_is_image_expanded_or_expanding(post_container)) {
				// Transfer state
				var expanded_node = null;
				if (this.settings.video.expand_state_save) {
					expanded_node = api.post_get_image_expanded_from_image_container(image_container);
					if (expanded_node !== null) {
						this.mpreview.transfer_video_state(expanded_node);
					}
				}

				// Close
				this.preview_close(true);

				// Observe expanded node
				if (expanded_node !== null && this.settings.video.expand_state_save) {
					observe_expanded.call(this, expanded_node, image_container);
				}
			}
			else {
				// Attempt to open if still hovered
				if (this.current_hover_container === image_container) {
					preview_open_test.call(this, image_container, post_container, false);
				}
			}
		};

		var on_image_connector_mouseenter = function (event, image_container) {
			// Cancel close
			this.preview_close_cancel();
		};
		var on_image_connector_mouseleave = function (event, image_container) {
			// Close preview
			this.preview_close(false);
		};
		var on_image_connector_mousedown = function (event) {
			// Close
			this.preview_close(true);
		};

		var on_window_resize = function (event) {
			// Reposition
			this.preview_update(false, false, false);
		};
		var on_window_scroll = function (event) {
			// Reposition
			this.preview_update(false, false, false);
		};

		var on_api_post_add = function (container) {
			// Queue add hooks
			this.post_queue.push(container);
		};
		var on_api_post_remove = function (container) {
			// Remove hooks
			remove_post_container_callbacks.call(this, container);
		};
		var on_api_image_hover_open = function () {
			// Disable
			if (this.disabled) return;
			this.disable();

			// Remove event
			api.off("image_hover_open", this.on_api_image_hover_open_bind);

			// Update settings
			settings.disable_image_expansion();
		};

		var on_settings_value_change = function (data) {
			// Update
			this.update_settings_from_global();
		};

		var on_hotkey = function (event, hotkey) {
			// Don't run if disabled
			if (this.disabled) return;

			if (hotkey.key == "esc") {
				// Close
				this.preview_close(true);
				return stop_event(event);
			}
		};

		var observe_expanded = function (node, image_container) {
			// Ignore if invalid node
			if (node.parentNode === null || node.tagName !== "VIDEO" || style.has_class(node, "iex_observing")) return;
			style.add_class(node, "iex_observing");

			var State = function (time, n) {
				this.time = time;
				this.volume = n.volume;
				this.muted = n.muted;
				this.paused = n.paused;
			};

			var self = this,
				time_min_interval = 0.25 * 1000,
				state_pre = new State(timing() - time_min_interval, node),
				state = state_pre,
				current_time = 0.0,
				disconnect, on_volumechange, on_timeupdate, on_pause, on_play;

			// Listen for video state changing events
			on_volumechange = function (event) {
				var t = timing();
				if (t - state.time >= time_min_interval) {
					state_pre = state;
					state = new State(t, this);
				}
				else {
					state.volume = this.volume;
					state.muted = this.muted;
				}
			};
			on_pause = function (event) {
				var t = timing();
				if (t - state.time >= time_min_interval) {
					state_pre = state;
					state = new State(t, this);
				}
				else {
					state.paused = this.paused;
				}
			};
			on_play = function (event) {
				var t = timing();
				if (t - state.time >= time_min_interval) {
					state_pre = state;
					state = new State(t, this);
				}
				else {
					state.paused = this.paused;
				}
			};
			on_timeupdate = function (event) {
				current_time = this.currentTime;
			};

			node.addEventListener("play", on_volumechange, false);
			node.addEventListener("pause", on_volumechange, false);
			node.addEventListener("timeupdate", on_timeupdate, false);
			node.addEventListener("volumechange", on_volumechange, false);

			// Observe for removal
			disconnect = api.observe_children(node.parentNode, function (added, n) {
				if (!added && n === node) {
					// Node removed
					disconnect();
					n.removeEventListener("play", on_volumechange, false);
					n.removeEventListener("pause", on_volumechange, false);
					n.removeEventListener("timeupdate", on_timeupdate, false);
					n.removeEventListener("volumechange", on_volumechange, false);
					style.remove_class(n, "iex_observing");

					// Update
					if (self.mpreview !== null && self.mpreview.is_visible()) {
						var t = timing(),
							s = state;

						if (t - s.time < time_min_interval) s = state_pre;

						// Update state
						self.mpreview.load_video_state({
							time: current_time,
							volume: s.volume,
							muted: s.muted,
							paused: s.paused,
						});
					}
				}
			});
		};

		var trigger_event = function (event, data) {
			var list = this.events[event],
				i;

			for (i = 0; i < list.length; ++i) {
				list[i].call(null, data);
			}
		};



		ImageHover.prototype = {
			constructor: ImageHover,

			destroy: function () {
				var par;

				// Detach preview
				preview_detach.call(this, true);

				// Create connector
				par = this.connector.parentNode;
				if (par) {
					this.connector.removeEventListener("mouseover", this.on_image_connector_mouseenter_bind, false);
					this.connector.removeEventListener("mouseout", this.on_image_connector_mouseleave_bind, false);
					this.connector.removeEventListener("mousedown", this.on_image_connector_mousedown_bind, false);

					par.removeChild(this.connector);
				}

				// Remove hotkeys
				if (this.hotkeys !== null) {
					for (var i = 0; i < this.hotkeys.length; ++i) {
						hotkey_manager.unregister(this.hotkeys[i]);
					}
					this.hotkeys = null;
				}

				// Remove api events
				api.off("post_add", this.on_api_post_add_bind);
				api.off("post_remove", this.on_api_post_remove_bind);
				api.off("image_hover_open", this.on_api_image_hover_open_bind);

				// Remove settings events
				settings.off_change(["image_expansion", "hover", "zoom_invert"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "hover", "zoom_borders_show"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "hover", "zoom_borders_hide_time"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "hover", "zoom_buttons"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "hover", "mouse_hide"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "hover", "mouse_hide_time"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "hover", "header_overlap"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "hover", "fit_large_allowed"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "hover", "display_stats"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "video", "autoplay"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "video", "loop"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "video", "mute_initially"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "video", "volume"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "video", "mini_controls"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "style", "controls_rounded_border"], this.off_settings_value_change_bind);
				settings.off_change(["image_expansion", "style", "animations_background"], this.off_settings_value_change_bind);
			},

			start: function () {
				// Enable if settings allow it
				if (settings.values.image_expansion.enabled) {
					this.update_settings_from_global();
					this.enable();
				}
			},
			enable: function () {
				// Enable
				if (this.enabled) return;
				this.enabled = true;

				// Setup nodes
				on_ready(on_asap.bind(this), on_asap_condition);

				// Get posts
				this.post_queue = Delay.queue(api.get("posts"), on_post_queue_callback.bind(this), 50, 0.25);

				// Bind post acquiring
				api.on("post_add", this.on_api_post_add_bind);
				api.on("post_remove", this.on_api_post_remove_bind);

				// Bind default image hover test
				api.on("image_hover_open", this.on_api_image_hover_open_bind);

				// Hotkeys
				var on_hotkey_bind = on_hotkey.bind(this);
				this.hotkeys = [
					hotkey_manager.register("esc", 0, on_hotkey_bind),
				];
			},
			disable: function () {
				// Not enabled or already disabled
				if (!this.enabled || this.disabled) return;

				// Disable
				this.disabled = true;

				// Stop the current preview
				this.preview_close(true);
			},

			preview_open: function (image_container, post_info, auto_fit) {
				var display_stats = this.settings.display_stats,
					v_set = this.settings.video,
					mc = this.settings.video.mini_controls,
					ext, ext_info, ext_bg_show;

				// Clear timer
				if (this.preview_open_timer !== null) {
					clearTimeout(this.preview_open_timer);
					this.preview_open_timer = null;
				}
				if (this.preview_close_timer !== null) {
					clearTimeout(this.preview_close_timer);
					this.preview_close_timer = null;
				}

				// Set
				this.current_image_container = image_container;

				// Create new
				if (this.mpreview === null) {
					preview_create.call(this);
					this.update_mpreview();
				}

				// Expansion type
				ext = get_image_url_extension.call(this, post_info.url);
				ext_info = this.extensions_valid[ext] || this.extensions_valid[""];
				ext_bg_show = settings.values.image_expansion.extensions[ext_info.ext].background;

				if (ext_info.type == "video") {
					// Set video
					this.mpreview.set_vcontrols_mini_available(true);
					this.mpreview.set_video(post_info.url);

					// Playback settings
					this.mpreview.set_video_muted(v_set.mute_initially);
					this.mpreview.set_video_volume(v_set.volume);
					this.mpreview.set_video_paused(true, v_set.loop);
					this.mpreview.clear_video_interactions();
				}
				else {
					// Set image
					this.mpreview.set_vcontrols_mini_available(false);
					this.mpreview.set_image(post_info.url);
				}

				// Background
				this.mpreview.set_background(post_info.thumb, ext_bg_show);
				this.mpreview.set_mouse_wheel_mode(settings.values.image_expansion.extensions[ext_info.ext].mouse_wheel);

				// Size
				if (post_info.resolution.width > 0 && post_info.resolution.height > 0) {
					this.mpreview.set_size(post_info.resolution.width, post_info.resolution.height, true);
					this.mpreview.set_stat_resolution((display_stats === 0), post_info.resolution.width + "x" + post_info.resolution.height);
				}
				else {
					this.mpreview.set_size(post_info.resolution_thumb.width, post_info.resolution_thumb.height, false);
					this.mpreview.set_stat_resolution((display_stats === 0), "unknown");
				}

				// Stats
				if (post_info.name) {
					this.mpreview.set_stat_file_name((display_stats === 0), post_info.name);
				}
				this.mpreview.set_stat_file_size((display_stats === 0), post_info.size + post_info.size_label);

				// Mini controls
				this.mpreview.set_vcontrols_mini_visible(mc == 3 || mc == 2);

				// Set visible
				this.mpreview.set_visible(true);
				style.add_class(this.connector, "iex_floating_image_connector_visible");

				// Update position
				this.mpreview.set_fixed(true);
				this.preview_update(false, true, auto_fit);

				// Trigger
				trigger_event.call(this, "change", {
					image_hover: this,
					image_container: image_container,
					type: ext_info.type,
				});
			},
			preview_close: function (immediate) {
				// Cancel the open timer
				if (this.preview_open_timer !== null) {
					clearTimeout(this.preview_open_timer);
					this.preview_open_timer = null;
				}

				if (this.mpreview === null || !this.mpreview.is_visible()) return;

				if (immediate) {
					// Immediate
					this.mpreview.set_visible(false);
					this.current_image_container = null;

					// Connector
					style.remove_class(this.connector, "iex_floating_image_connector_visible");

					// Trigger
					trigger_event.call(this, "change", {
						image_hover: this,
						image_container: null,
						type: null,
					});
				}
				else {
					// Delay
					if (this.preview_close_timer !== null) {
						clearTimeout(this.preview_close_timer);
					}
					this.preview_close_timer = setTimeout(this.on_preview_close_timeout_bind, 20);
				}
			},
			preview_close_cancel: function () {
				// Cancel the timer
				if (this.preview_close_timer !== null) {
					clearTimeout(this.preview_close_timer);
					this.preview_close_timer = null;
				}
			},

			preview_update: function (keep_mouse_inside, fit_reset, fit_start) {
				// Update the position and sizing of the preview after a zoom, resize, or open event
				if (this.mpreview === null || !this.mpreview.is_visible()) return;

				// Vars
				var set = this.settings,
					header_overlap = set.header_overlap,
					paddings = set.region_paddings,
					max_rect = style.get_document_rect(),
					img = this.current_image_container.querySelector("img") || this.current_image_container,
					img_rect = style.get_object_rect_inner(img),
					left = max_rect.left,
					top = max_rect.top,
					fit = this.mpreview.fit,
					fit_large = this.mpreview.fit_large,
					fit_large_allowed = set.fit_large_allowed,
					fit_axis = 0,
					w = this.mpreview.size.width,
					h = this.mpreview.size.height,
					zoom = this.mpreview.zoom,
					x, y, w_max, h_max, w_scale, h_scale;

				// Include header
				if (!header_overlap) {
					var header_rect = api.get_header_rect();

					// Subtract
					if (header_rect.top <= max_rect.top && header_rect.bottom > max_rect.top) {
						// It's on the top
						max_rect.top = header_rect.bottom;
					}
					else if (header_rect.bottom >= max_rect.bottom && header_rect.top < max_rect.bottom) {
						// It's on the bottom
						max_rect.bottom = header_rect.top;
					}
				}

				// Update max_rect
				max_rect.left = img_rect.right;
				max_rect.left += paddings.left;
				max_rect.top += paddings.top;
				max_rect.right -= paddings.right;
				max_rect.bottom -= paddings.bottom;

				// Maximum size
				w_max = (max_rect.right - max_rect.left);
				h_max = (max_rect.bottom - max_rect.top);
				if (w_max < 1) w_max = 1;
				if (h_max < 1) h_max = 1;
				this.display_window_max.width = w_max;
				this.display_window_max.height = h_max;

				// Get the minimum scale and apply it
				w_scale = w_max / w;
				h_scale = h_max / h;

				if (fit_reset) {
					zoom = 1;
					fit = fit_start;
					fit_large = false;
					if (w_scale < 1.0 || h_scale < 1.0) {
						fit = true;
						fit_large = (Math.abs(w_scale - h_scale) < 1e-5) && fit_large_allowed;
					}
				}
				if (fit) {
					var best_scale;
					if ((w_scale > h_scale) == fit_large) {
						best_scale = w_scale;
						fit_axis = 0;
					}
					else {
						best_scale = h_scale;
						fit_axis = 1;
					}

					w *= best_scale;
					h *= best_scale;
				}
				w *= zoom;
				h *= zoom;

				// Limit
				this.mpreview.set_view_borders_visible_sides((w - w_max) > 1e-5, (h - h_max) > 1e-5);
				if (w > w_max) w = w_max;
				if (h > h_max) h = h_max;

				// Position
				x = max_rect.left;
				y = (img_rect.top + img_rect.bottom - h) / 2.0;
				if (y + h >= max_rect.bottom) y = max_rect.bottom - h;
				if (y < max_rect.top) y = max_rect.top;

				// Mouse inside
				if (keep_mouse_inside) {
					// Setup
					var off_right = 0,
						off_top = 0,
						off_bottom = 0,
						padding_extra = set.padding_extra,
						x_r = x + w,
						y_b = y + h;

					off_right = (this.mouse_x - x_r);
					off_top = (y - this.mouse_y);
					off_bottom = (this.mouse_y - y_b);

					// Right offset
					if (off_right <= 0) {
						off_right = 0;
					}
					else {
						off_right += padding_extra.right;
						if (x_r + off_right > max_rect.right) {
							off_right = Math.max(0, max_rect.right - x_r);
						}
					}

					// Top offset
					if (off_top <= 0) {
						off_top = 0;
					}
					else {
						off_top += padding_extra.top;
						if (y - off_top < max_rect.top) {
							off_top = Math.max(0, y - max_rect.top);
						}
					}

					// Bottom offset
					if (off_bottom <= 0) {
						off_bottom = 0;
					}
					else {
						off_bottom += padding_extra.bottom;
						if (y_b + off_bottom > max_rect.bottom) {
							off_bottom = Math.max(0, max_rect.bottom - y_b);
						}
					}

					// Set
					if (off_top > 0 || off_right > 0 || off_bottom > 0) {
						this.paddings_active = true;
						this.mpreview.set_paddings(off_top, off_right, off_bottom, 0);
					}
					else {
						this.paddings_active = false;
						this.mpreview.clear_paddings();
					}
				}
				else {
					// No paddings
					this.paddings_active = false;
					this.mpreview.clear_paddings();
				}

				// Set
				this.mpreview.set_window(x - left, y - top, w, h);
				this.mpreview.set_zoom(zoom, fit, fit_large, fit_axis);

				// Connector
				this.connector.style.left = (img_rect.right).toFixed(2) + "px";
				this.connector.style.top = (img_rect.top).toFixed(2) + "px";
				this.connector.style.width = (paddings.left).toFixed(2) + "px";
				this.connector.style.height = (img_rect.bottom - img_rect.top).toFixed(2) + "px";

				// Stats
				this.mpreview.set_stat_zoom((set.display_stats <= 1), (zoom * 100) + "%", fit ? " fit-" + (this.mpreview.fit_axis === 0 ? "x" : "y") : "");
			},

			update_settings_from_global: function () {
				// Copy settings
				var hs = settings.values.image_expansion.hover,
					vs = settings.values.image_expansion.video,
					ss = settings.values.image_expansion.style,
					set = this.settings,
					v_set = set.video,
					s_set = set.style;

				set.zoom_invert = hs.zoom_invert;
				set.zoom_borders_show = hs.zoom_borders_show;
				set.zoom_borders_hide_time = hs.zoom_borders_hide_time * 1000;
				set.zoom_buttons = hs.zoom_buttons;
				set.mouse_hide = hs.mouse_hide;
				set.mouse_hide_time = hs.mouse_hide_time * 1000;
				set.header_overlap = hs.header_overlap;
				set.fit_large_allowed = hs.fit_large_allowed;
				set.display_stats = hs.display_stats;

				v_set.autoplay = vs.autoplay;
				v_set.loop = vs.loop;
				v_set.mute_initially = vs.mute_initially;
				v_set.volume = vs.volume;
				v_set.mini_controls = vs.mini_controls;
				v_set.expand_state_save = vs.expand_state_save;

				s_set.controls_rounded_border = ss.controls_rounded_border;
				s_set.animations_background = ss.animations_background;

				if (this.mpreview !== null) {
					this.update_mpreview();
				}
			},
			update_mpreview: function () {
				// Rounded borders
				this.mpreview.set_vcontrols_borders_rounded(!is_chrome && this.settings.style.controls_rounded_border);
				this.mpreview.set_background_animations(this.settings.style.animations_background);

				// Zoom buttons
				this.mpreview.set_zoom_control_buttons_enabled(this.settings.zoom_buttons);
			},

			on: function (event, callback) {
				if (event in this.events) {
					this.events[event].push(callback);
					return true;
				}
				return false;
			},
			off: function (event, callback) {
				if (event in this.events) {
					var list = this.events[event],
						i;

					for (i = 0; i < list.length; ++i) {
						if (callback === list[i]) {
							list.splice(i, 1);
							return true;
						}
					}
				}
				return false;
			},
		};



		return ImageHover;

	})();

	// Media preview base
	var MediaPreview = (function () {

		var MediaPreviewGenericNodes = function (parent) {
			// Create the preview container
			var zoom_border_inner,
				stat_zoom_inc,
				stat_zoom_dec,
				stat_sep;


			// Vars
			this.stats_zoom_controls_enabled = true;
			this.zoom_controls_hide_timer = null;
			this.mouse_in_stats = false;
			this.on_hide_zoom_controls_timeout_bind = hide_zoom_controls.bind(parent, true, false);


			// Background
			this.background = $.div("iex_mpreview_background"); // iex_mpreview_background_disabled iex_mpreview_background_visible iex_mpreview_background_fallback


			// Zoom borders
			this.zoom_borders = $.div("iex_mpreview_zoom_borders"); // iex_mpreview_zoom_borders_visible iex_mpreview_zoom_borders_vertical iex_mpreview_zoom_borders_horizontal

			zoom_border_inner = $.div("iex_mpreview_zoom_borders_inner");
			this.zoom_borders.appendChild(zoom_border_inner);


			// Stats container
			this.stats_container = $.div("iex_mpreview_stats_container");

			// Zoom controls
			this.zoom_controls = $.span("iex_mpreview_stat iex_mpreview_stat_zoom_controls"); // iex_mpreview_stat_zoom_controls_visible iex_mpreview_stat_zoom_controls_fixed
			this.stats_container.appendChild(this.zoom_controls);

			stat_zoom_inc = $.span("iex_mpreview_stat_zoom_control iex_mpreview_stat_zoom_control_increase");
			stat_zoom_inc.textContent = "+";
			this.zoom_controls.appendChild(stat_zoom_inc);

			stat_zoom_dec = $.span("iex_mpreview_stat_zoom_control iex_mpreview_stat_zoom_control_decrease");
			stat_zoom_dec.textContent = "\u2212";
			this.zoom_controls.appendChild(stat_zoom_dec);

			this.zoom_controls_offset = $.span("iex_mpreview_stat_zoom_controls_offset");
			this.stats_container.appendChild(this.zoom_controls_offset);



			// Stats items
			this.stat_zoom_container = $.span("iex_mpreview_stat");
			this.stats_container.appendChild(this.stat_zoom_container);

			this.stat_zoom = $.span("");
			this.stat_zoom_container.appendChild(this.stat_zoom);

			this.stat_zoom_fit = $.span("");
			this.stat_zoom_container.appendChild(this.stat_zoom_fit);

			stat_sep = $.span("iex_mpreview_stat_sep");
			stat_sep.textContent = ", ";
			this.stats_container.appendChild(stat_sep);

			this.stat_status = $.span("iex_mpreview_stat");
			this.stats_container.appendChild(this.stat_status);

			stat_sep = $.span("iex_mpreview_stat_sep");
			stat_sep.textContent = ", ";
			this.stats_container.appendChild(stat_sep);

			this.stat_resolution = $.span("iex_mpreview_stat");
			this.stats_container.appendChild(this.stat_resolution);

			stat_sep = $.span("iex_mpreview_stat_sep");
			stat_sep.textContent = ", ";
			this.stats_container.appendChild(stat_sep);

			this.stat_filesize = $.span("iex_mpreview_stat");
			this.stats_container.appendChild(this.stat_filesize);

			stat_sep = $.span("iex_mpreview_stat_sep");
			stat_sep.textContent = ", ";
			this.stats_container.appendChild(stat_sep);

			this.stat_filename = $.span("iex_mpreview_stat");
			this.stats_container.appendChild(this.stat_filename);


			// Events
			parent.add_event_listener(this.stats_container, "mouseover", wrap_mouseenterleave_event(parent, on_stats_mouseenter), false);
			parent.add_event_listener(this.stats_container, "mouseout", wrap_mouseenterleave_event(parent, on_stats_mouseleave), false);
			parent.add_event_listener(this.stat_zoom, "mouseover", wrap_mouseenterleave_event(parent, on_stats_zoom_mouseenter), false);
			parent.add_event_listener(this.zoom_controls, "mousedown", on_stats_zoom_controls_mousedown.bind(parent), false);
			parent.add_event_listener(this.zoom_controls, "mouseover", wrap_mouseenterleave_event(parent, on_stats_zoom_controls_mouseenter), false);
			parent.add_event_listener(this.zoom_controls, "mouseout", wrap_mouseenterleave_event(parent, on_stats_zoom_controls_mouseleave), false);
			parent.add_event_listener(stat_zoom_inc, "click", on_stats_zoom_control_click.bind(parent, 1), false);
			parent.add_event_listener(stat_zoom_dec, "click", on_stats_zoom_control_click.bind(parent, -1), false);


			// Add
			parent.cpreview.add_content(this.background);
			parent.cpreview.add_overlay(this.stats_container, true);
			parent.cpreview.add_overlay(this.zoom_borders);
		};
		var MediaPreviewImageNodes = function (parent) {
			// Image
			this.image = $.node("img", "iex_mpreview_image");
			this.image.setAttribute("alt", "");
			this.image.setAttribute("title", "");


			// Events
			parent.add_event_listener(this.image, "load", on_image_load.bind(parent), false);
			parent.add_event_listener(this.image, "error", on_image_error.bind(parent), false);


			// Add
			parent.cpreview.add_content(this.image);
		};
		var MediaPreviewVideoNodes = function (parent) {
			var svgns = "http://www.w3.org/2000/svg",
				c_inner,
				svg_e1, svg_e2,
				c_div1, c_div2, c_div3, c_div4, c_buttons_left, c_buttons_right,
				c_play_button, c_volume_button;

			// Vars
			this.interacted = false;

			this.seeking = false;
			this.seeking_paused = false;
			this.volume_modifying = false;

			this.mouse_capturing_rect = null;
			this.mouse_capturing_element = null;
			this.on_capture_mousemove = on_vcontrols_capture_mousemove.bind(parent);
			this.on_capture_mouseup = on_vcontrols_capture_mouseup.bind(parent);
			this.on_capture_window_resize = on_window_resize.bind(parent);
			this.on_capture_window_scroll = on_window_scroll.bind(parent);


			// Video
			this.video = $.node("video", "iex_mpreview_video");
			this.video.setAttribute("preload", "auto");



			// Controller overlay
			this.overlay = $.div("iex_mpreview_vcontrols_container");

			c_inner = $.div("iex_mpreview_vcontrols_container_inner");
			this.overlay.appendChild(c_inner);

			this.overlay_table = $.div("iex_mpreview_vcontrols_table"); // iex_mpreview_vcontrols_table_visible iex_mpreview_vcontrols_table_visible_important iex_mpreview_vcontrols_table_mini
			c_inner.appendChild(this.overlay_table);



			// Pause/play button
			c_buttons_left = $.div("iex_mpreview_vcontrols_button_container");
			this.overlay_table.appendChild(c_buttons_left);

			c_div1 = $.div("iex_mpreview_vcontrols_button_container_inner");
			c_buttons_left.appendChild(c_div1);

			c_div2 = $.div("iex_mpreview_vcontrols_button_container_inner2");
			c_div1.appendChild(c_div2);

			// Mouse event controller
			c_play_button = $.div("iex_mpreview_vcontrols_button_mouse_controller");
			c_div2.appendChild(c_play_button);

			// SVG image
			this.svg_play = $.node_ns(svgns, "svg", "iex_svg_play_button"); // iex_svg_play_button_playing iex_svg_play_button_looping
			this.svg_play.setAttribute("svgns", svgns);
			this.svg_play.setAttribute("width", "2em");
			this.svg_play.setAttribute("height", "2em");
			this.svg_play.setAttribute("viewBox", "0 0 1 1");
			c_div2.appendChild(this.svg_play);

			svg_e1 = $.node_ns(svgns, "g", "iex_svg_button_scale_group");
			svg_e1.setAttribute("transform", "translate(0.25,0.25) scale(0.5)");
			this.svg_play.appendChild(svg_e1);

			svg_e2 = $.node_ns(svgns, "polygon", "iex_svg_play_button_play_icon iex_svg_button_fill");
			svg_e2.setAttribute("points", "0,0 0,1 1,0.5");
			svg_e1.appendChild(svg_e2);

			svg_e2 = $.node_ns(svgns, "polygon", "iex_svg_play_button_loop_icon iex_svg_button_fill");
			svg_e2.setAttribute("points", "0.1,0.05 1,0.5 0.1,0.95");
			svg_e1.appendChild(svg_e2);

			svg_e2 = $.node_ns(svgns, "polygon", "iex_svg_play_button_loop_icon iex_svg_button_fill");
			svg_e2.setAttribute("points", "0,0.7 0.4,0.5 0,0.3");
			svg_e1.appendChild(svg_e2);

			svg_e2 = $.node_ns(svgns, "polygon", "iex_svg_play_button_pause_icon iex_svg_button_fill");
			svg_e2.setAttribute("points", "0.125,0 0.375,0 0.375,1 0.125,1");
			svg_e1.appendChild(svg_e2);

			svg_e2 = $.node_ns(svgns, "polygon", "iex_svg_play_button_pause_icon iex_svg_button_fill");
			svg_e2.setAttribute("points", "0.625,0 0.875,0 0.875,1 0.625,1");
			svg_e1.appendChild(svg_e2);



			// Seek bar
			c_div1 = $.div("iex_mpreview_vcontrols_seek_container");
			this.overlay_table.appendChild(c_div1);

			c_div2 = $.div("iex_mpreview_vcontrols_seek_container_inner");
			c_div1.appendChild(c_div2);

			this.seek_bar = $.div("iex_mpreview_vcontrols_seek_bar"); // iex_mpreview_vcontrols_no_border_radius
			c_div2.appendChild(this.seek_bar);

			c_div3 = $.div("iex_mpreview_vcontrols_seek_bar_bg");
			this.seek_bar.appendChild(c_div3);

			this.load_progress = $.div("iex_mpreview_vcontrols_seek_bar_loaded");
			c_div3.appendChild(this.load_progress);

			this.play_progress = $.div("iex_mpreview_vcontrols_seek_bar_played");
			c_div3.appendChild(this.play_progress);

			c_div4 = $.div("iex_mpreview_vcontrols_seek_time_table");
			c_div3.appendChild(c_div4);

			this.time_current = $.div("iex_mpreview_vcontrols_seek_time_current");
			c_div4.appendChild(this.time_current);

			this.time_duration = $.div("iex_mpreview_vcontrols_seek_time_duration");
			c_div4.appendChild(this.time_duration);



			// Volume bar/mute button
			c_buttons_right = $.div("iex_mpreview_vcontrols_button_container");
			this.overlay_table.appendChild(c_buttons_right);

			c_div1 = $.div("iex_mpreview_vcontrols_button_container_inner");
			c_buttons_right.appendChild(c_div1);

			c_div2 = $.div("iex_mpreview_vcontrols_button_container_inner2");
			c_div1.appendChild(c_div2);

			// Mouse event controller
			c_volume_button = $.div("iex_mpreview_vcontrols_button_mouse_controller");
			c_div2.appendChild(c_volume_button);

			// SVG image
			this.svg_volume = $.node_ns(svgns, "svg", "iex_svg_volume_button"); // iex_svg_volume_button_muted iex_svg_volume_button_high iex_svg_volume_button_medium iex_svg_volume_button_low
			this.svg_volume.setAttribute("svgns", svgns);
			this.svg_volume.setAttribute("width", "2em");
			this.svg_volume.setAttribute("height", "2em");
			this.svg_volume.setAttribute("viewBox", "0 0 1 1");
			c_div2.appendChild(this.svg_volume);

			svg_e1 = $.node_ns(svgns, "g", "iex_svg_button_scale_group");
			svg_e1.setAttribute("transform", "translate(0.25,0.25) scale(0.5)");
			this.svg_volume.appendChild(svg_e1);

			svg_e2 = $.node_ns(svgns, "polygon", "iex_svg_volume_button_speaker iex_svg_button_fill");
			svg_e2.setAttribute("points", "0,0.3 0.2,0.3 0.5,0 0.6,0 0.6,1 0.5,1 0.2,0.7 0,0.7");
			svg_e1.appendChild(svg_e2);

			svg_e2 = $.node_ns(svgns, "path", "iex_svg_volume_button_wave_big iex_svg_button_fill");
			svg_e2.setAttribute("d", "M 0.75,0.1 Q 1.3,0.5 0.75,0.9 L 0.75,0.75 Q 1.05,0.5 0.75,0.25 Z");
			svg_e1.appendChild(svg_e2);

			svg_e2 = $.node_ns(svgns, "path", "iex_svg_volume_button_wave_small iex_svg_button_fill");
			svg_e2.setAttribute("d", "M 0.75,0.75 Q 1.05,0.5 0.75,0.25 Z");
			svg_e1.appendChild(svg_e2);

			svg_e2 = $.node_ns(svgns, "g", "iex_svg_volume_button_wave_mute_icon");
			svg_e1.appendChild(svg_e2);
			svg_e1 = svg_e2;

			svg_e2 = $.node_ns(svgns, "polygon", "iex_svg_volume_button_wave_mute_icon_polygon");
			svg_e2.setAttribute("points", "0.7,0.3 1.0,0.6 0.9,0.7 0.6,0.4");
			svg_e1.appendChild(svg_e2);

			svg_e2 = $.node_ns(svgns, "polygon", "iex_svg_volume_button_wave_mute_icon_polygon");
			svg_e2.setAttribute("points", "0.7,0.7 1.0,0.4 0.9,0.3 0.6,0.6");
			svg_e1.appendChild(svg_e2);



			// Volume bar
			c_div2 = $.div("iex_mpreview_vcontrols_volume_container_position");
			c_div1.appendChild(c_div2);

			this.volume_container = $.div("iex_mpreview_vcontrols_volume_container"); // iex_mpreview_vcontrols_volume_container_visible iex_mpreview_vcontrols_volume_container_visible_important
			c_div2.appendChild(this.volume_container);

			this.volume_bar = $.div("iex_mpreview_vcontrols_volume_bar"); // iex_mpreview_vcontrols_no_border_radius
			this.volume_container.appendChild(this.volume_bar);

			c_div3 = $.div("iex_mpreview_vcontrols_volume_bar_bg");
			this.volume_bar.appendChild(c_div3);

			this.volume_progress = $.div("iex_mpreview_vcontrols_volume_bar_level");
			c_div3.appendChild(this.volume_progress);


			// Events
			parent.add_event_listener(this.video, "loadedmetadata", on_video_loadedmetadata.bind(parent), false);
			parent.add_event_listener(this.video, "canplay", on_video_canplay.bind(parent), false);
			parent.add_event_listener(this.video, "canplaythrough", on_video_canplaythrough.bind(parent), false);
			parent.add_event_listener(this.video, "error", on_video_error.bind(parent), false);
			parent.add_event_listener(this.video, "progress", on_video_progress.bind(parent), false);
			parent.add_event_listener(this.video, "timeupdate", on_video_timeupdate.bind(parent), false);
			parent.add_event_listener(this.video, "ended", on_video_ended.bind(parent), false);

			var prevent = on_vcontrols_prevent_default_mousedown.bind(parent);

			parent.add_event_listener(c_inner, "mouseover", wrap_mouseenterleave_event(parent, on_vcontrols_mouseenter), false);
			parent.add_event_listener(c_inner, "mouseout", wrap_mouseenterleave_event(parent, on_vcontrols_mouseleave), false);
			parent.add_event_listener(this.overlay_table, "mousedown", on_vcontrols_container_mousedown.bind(parent), false);
			parent.add_event_listener(c_play_button, "click", on_vcontrols_play_button_click.bind(parent), false);
			parent.add_event_listener(c_volume_button, "click", on_vcontrols_volume_button_click.bind(parent), false);
			parent.add_event_listener(c_volume_button, "mouseover", wrap_mouseenterleave_event(parent, on_vcontrols_volume_button_mouseenter), false);
			parent.add_event_listener(c_buttons_right, "mouseout", wrap_mouseenterleave_event(parent, on_vcontrols_volume_button_container_mouseleave), false);
			parent.add_event_listener(this.seek_bar, "mousedown", on_vcontrols_seek_bar_mousedown.bind(parent), false);
			parent.add_event_listener(this.volume_bar, "mousedown", on_vcontrols_volume_bar_mousedown.bind(parent), false);
			parent.add_event_listener(this.time_current, "mousedown", prevent, false);
			parent.add_event_listener(this.time_duration, "mousedown", prevent, false);


			// Add
			parent.cpreview.add_content(this.video);
			parent.cpreview.add_overlay(this.overlay);
		};



		var MediaPreview = function () {
			// Setup preview
			this.cpreview = new ContentPreview();
			this.event_listeners = [];

			// Events
			var cp_nodes = this.cpreview.nodes,
				event_bind;

			this.add_event_listener(cp_nodes.container, "mouseover", wrap_mouseenterleave_event(this, on_mouseenter), false);
			this.add_event_listener(cp_nodes.container, "mouseout", wrap_mouseenterleave_event(this, on_mouseleave), false);
			this.add_event_listener(cp_nodes.container, "mousemove", on_mousemove.bind(this), false);
			this.add_event_listener(cp_nodes.container, "mousewheel", (event_bind = on_mousewheel.bind(this)), false);
			this.add_event_listener(cp_nodes.container, "DOMMouseScroll", event_bind, false);
			this.add_event_listener(cp_nodes.container, "mousedown", on_mousedown.bind(this), false);
			this.add_event_listener(cp_nodes.container, "contextmenu", on_contextmenu.bind(this), false);

			this.add_event_listener(cp_nodes.overflow, "mouseover", wrap_mouseenterleave_event(this, on_overflow_mouseenter), false);


			// Setup classes
			style.add_class(this.cpreview.nodes.padding, "iex_mpreview_padding");

			// Setup nodes
			this.nodes = new MediaPreviewGenericNodes(this);
			this.nodes_image = null;
			this.nodes_video = null;


			// Settings
			this.type = MediaPreview.TYPE_NONE;

			this.zoom = 1;

			this.fit = false;
			this.fit_large = false;
			this.fit_large_allowed = true;
			this.fit_axis = 0;

			this.mouse_wheel_mode = 0;
			this.volume_controls_temp_timer = null;

			this.style = {
				vcontrols_rounded: true,
			};

			this.size = {
				width: 0,
				height: 0,
				acquired: false
			};

			// Events
			this.events = {
				"image_load": [],
				"image_error": [],

				"video_ready": [],
				"video_can_play": [],
				"video_can_play_through": [],
				"video_load": [],
				"video_error": [],
				"video_volume_change": [],

				"size_change": [],

				"mouse_wheel": [],
				"mouse_enter": [],
				"mouse_leave": [],
				"mouse_down": [],
				"mouse_move": [],
				"mouse_enter_main": [],

				"stats_zoom_control_click": [],

				"resize": [],
			};
		};



		MediaPreview.TYPE_NONE = 0;
		MediaPreview.TYPE_IMAGE = 1;
		MediaPreview.TYPE_VIDEO = 2;



		var trigger = function (event, data) {
			// Trigger
			if (event in this.events) {
				var e_list = this.events[event];
				for (var i = 0; i < e_list.length; ++i) {
					e_list[i].call(this, data);
				}
			}
		};

		var format_video_time = function (time) {
			time = Math.floor(time + 0.5);

			var s = (time % 60).toString();
			if (s.length < 2) s = "0" + s;

			time = Math.floor(time / 60);
			return time + ":" + s;
		};

		var update_size = function (width_new, height_new) {
			// Change
			this.size.width = width_new;
			this.size.height = height_new;
			this.size.acquired = true;

			// Event
			trigger.call(this, "size_change", {
				width: width_new,
				height: height_new,
			});
		};
		var update_video_duration_status = function () {
			var video = this.nodes_video.video,
				duration = video.duration;

			// Update seek bar
			if (isNaN(duration)) {
				// Update time numbers
				this.nodes_video.time_duration.textContent = "";
			}
			else {
				// Update time numbers
				this.nodes_video.time_duration.textContent = format_video_time.call(this, duration);
			}
		};
		var update_video_seek_status = function (time) {
			var video = this.nodes_video.video,
				duration = video.duration;

			// Update seek bar
			if (time < 0 || isNaN(duration)) {
				// Bar
				this.nodes_video.play_progress.style.width = "0";

				// Update time numbers
				this.nodes_video.time_current.textContent = "";
			}
			else {
				// Bar
				this.nodes_video.play_progress.style.width = ((time / duration) * 100).toFixed(2) + "%";

				// Update time numbers
				this.nodes_video.time_current.textContent = format_video_time.call(this, time);
			}
		};
		var update_video_play_status = function (paused) {
			var loop = this.nodes_video.video.loop,
				svg_play = this.nodes_video.svg_play;

			// Update button
			if (paused) {
				style.remove_class(svg_play, "iex_svg_play_button_playing");
			}
			else {
				style.add_class(svg_play, "iex_svg_play_button_playing");
			}
			if (loop) {
				style.add_class(svg_play, "iex_svg_play_button_looping");
			}
			else {
				style.remove_class(svg_play, "iex_svg_play_button_looping");
			}
		};

		var set_video_volume = function (volume, reason, interacted) {
			var svg_volume = this.nodes_video.svg_volume;

			// Set volume on video
			if (volume >= 0) {
				if (volume > 1) volume = 1;
				this.nodes_video.video.volume = volume;
			}
			else {
				volume = this.nodes_video.video.volume;
			}

			// Update bar
			this.nodes_video.volume_progress.style.height = (volume * 100).toFixed(2) + "%";

			// Update icons
			if (volume > 0.625) {
				style.change_classes_svg(svg_volume, "iex_svg_volume_button_high", "iex_svg_volume_button_low iex_svg_volume_button_medium");
			}
			else if (volume > 0.125) {
				style.change_classes_svg(svg_volume, "iex_svg_volume_button_medium", "iex_svg_volume_button_low iex_svg_volume_button_high");
			}
			else {
				style.change_classes_svg(svg_volume, "iex_svg_volume_button_low", "iex_svg_volume_button_medium iex_svg_volume_button_high");
			}

			// Interacted
			if (interacted) this.nodes_video.interacted = true;

			// Event
			trigger.call(this, "video_volume_change", {
				reason: reason,
				volume: volume
			});
		};

		var setup_vcontrols_mouse_capture = function () {
			// Setup mouse capturing events
			if (
				this.nodes_video.mouse_capturing_element === null &&
				(this.nodes_video.mouse_capturing_element = document.documentElement) !== null
			) {
				this.nodes_video.mouse_capturing_element.addEventListener("mousemove", this.nodes_video.on_capture_mousemove, false);
				this.nodes_video.mouse_capturing_element.addEventListener("mouseup", this.nodes_video.on_capture_mouseup, false);
				window.addEventListener("resize", this.nodes_video.on_capture_window_resize, false);
				window.addEventListener("scroll", this.nodes_video.on_capture_window_scroll, false);
				update_vcontrols_mouse_capture.call(this);
			}
		};
		var teardown_vcontrols_mouse_capture = function () {
			// Destroy mouse capturing events
			if (this.nodes_video.mouse_capturing_element !== null) {
				this.nodes_video.mouse_capturing_element.removeEventListener("mousemove", this.nodes_video.on_capture_mousemove, false);
				this.nodes_video.mouse_capturing_element.removeEventListener("mouseup", this.nodes_video.on_capture_mouseup, false);
				window.removeEventListener("resize", this.nodes_video.on_capture_window_resize, false);
				window.removeEventListener("scroll", this.nodes_video.on_capture_window_scroll, false);
				this.nodes_video.mouse_capturing_element = null;
			}
		};
		var update_vcontrols_mouse_capture = function () {
			// Update node rectangle
			if (this.nodes_video.seeking) {
				this.nodes_video.mouse_capturing_rect = style.get_object_rect(this.nodes_video.seek_bar);
			}
			else if (this.nodes_video.volume_modifying) {
				this.nodes_video.mouse_capturing_rect = style.get_object_rect(this.nodes_video.volume_bar);
			}
		};

		var show_zoom_controls = function (force_open) {
			// Stop timer
			if (this.nodes.zoom_controls_hide_timer !== null) {
				clearTimeout(this.nodes.zoom_controls_hide_timer);
				this.nodes.zoom_controls_hide_timer = null;
			}

			// Display
			if (force_open) {
				var zoom_controls = this.nodes.zoom_controls,
					z_rect;

				style.add_class(zoom_controls, "iex_mpreview_stat_visible");
				z_rect = style.get_object_rect(zoom_controls);

				this.nodes.zoom_controls_offset.style.width = (z_rect.right - z_rect.left).toFixed(2) + "px";
			}
		};
		var hide_zoom_controls = function (instant, timer_check) {
			if (instant) {
				// Hide
				style.remove_class(this.nodes.zoom_controls, "iex_mpreview_stat_visible");
				this.nodes.zoom_controls_offset.style.width = "";

				// Stop timer
				if (timer_check && this.nodes.zoom_controls_hide_timer !== null) {
					clearTimeout(this.nodes.zoom_controls_hide_timer);
				}
				this.nodes.zoom_controls_hide_timer = null;
			}
			else {
				// Set timer
				this.nodes.zoom_controls_hide_timer = setTimeout(this.nodes.on_hide_zoom_controls_timeout_bind, 10);
			}
		};
		var set_zoom_controls_fixed = function (fixed) {
			// Set fixed
			var zoom_controls = this.nodes.zoom_controls;

			if (fixed) {
				var w_rect = style.get_document_rect(),
					z_rect = style.get_object_rect(zoom_controls);

				// Fix
				style.add_class(zoom_controls, "iex_mpreview_stat_zoom_controls_fixed");
				zoom_controls.style.left = (z_rect.left - w_rect.left).toFixed(2) + "px";
				zoom_controls.style.top = (z_rect.top - w_rect.top).toFixed(2) + "px";
			}
			else {
				// Un-fix
				style.remove_class(zoom_controls, "iex_mpreview_stat_zoom_controls_fixed");
				zoom_controls.style.left = "";
				zoom_controls.style.top = "";
			}
		};

		var update_stat_sep_visibility = function (node, visible) {
			var n;

			if (visible) {
				// Previous
				for (n = node; (n = n.previousSibling); ) {
					if (style.has_class(n, "iex_mpreview_stat_sep_visible")) break;
					if (style.has_class(n, "iex_mpreview_stat_visible")) {
						if ((n = node.previousSibling) !== null && style.has_class(n, "iex_mpreview_stat_sep")) {
							style.add_class(n, "iex_mpreview_stat_sep_visible");
						}
						break;
					}
				}

				// Next
				for (n = node; (n = n.nextSibling); ) {
					if (style.has_class(n, "iex_mpreview_stat_sep_visible")) break;
					if (style.has_class(n, "iex_mpreview_stat_visible")) {
						if ((n = node.nextSibling) !== null && style.has_class(n, "iex_mpreview_stat_sep")) {
							style.add_class(n, "iex_mpreview_stat_sep_visible");
						}
						break;
					}
				}
			}
			else {
				// Hide next
				n = node.nextSibling;
				if (n !== null && style.has_class(n, "iex_mpreview_stat_sep")) {
					if (style.has_class(n, "iex_mpreview_stat_sep_visible")) {
						style.remove_class(n, "iex_mpreview_stat_sep_visible");
						return;
					}
				}

				// Hide previous
				n = node.previousSibling;
				if (n !== null && style.has_class(n, "iex_mpreview_stat_sep")) {
					style.remove_class(n, "iex_mpreview_stat_sep_visible");
				}
			}
		};


		var on_window_resize = function (event) {
			// Update bar position
			if (this.nodes_video.mouse_capturing_element !== null) {
				update_vcontrols_mouse_capture.call(this);
			}
		};
		var on_window_scroll = function (event) {
			// Update bar position
			if (this.nodes_video.mouse_capturing_element !== null) {
				update_vcontrols_mouse_capture.call(this);
			}
		};

		var on_video_loadedmetadata = function (event) {
			if (this.type !== MediaPreview.TYPE_VIDEO) return;

			var video = this.nodes_video.video,
				w = video.videoWidth,
				h = video.videoHeight;


			// Update seek bar
			update_video_duration_status.call(this);
			update_video_seek_status.call(this, 0);

			// Un-hide video
			style.remove_class(video, "iex_mpreview_video_not_ready");

			// Update size
			if (!this.size.acquired || (this.size.width != w || this.size.height != h)) {
				update_size.call(this, w, h);
				style.remove_class(video, "iex_mpreview_video_unsized");
			}

			// Event
			trigger.call(this, "video_ready", {});
		};
		var on_video_canplay = function (event) {
			if (this.type !== MediaPreview.TYPE_VIDEO) return;

			// Event
			on_video_progress.call(this, event);
			trigger.call(this, "video_can_play", {});
		};
		var on_video_canplaythrough = function (event) {
			if (this.type !== MediaPreview.TYPE_VIDEO) return;

			// Event
			on_video_progress.call(this, event);
			trigger.call(this, "video_can_play_through", {});
		};
		var on_video_progress = function (event) {
			if (this.type !== MediaPreview.TYPE_VIDEO) return;

			var video = this.nodes_video.video,
				percent = 0.0;

			if (video.buffered.length > 0) {
				percent = (video.buffered.end(0) / video.duration);
			}

			// Update progress bar
			this.nodes_video.load_progress.style.width = (percent * 100).toFixed(2) + "%";

			// Complete
			if (percent >= 1.0) {
				trigger.call(this, "video_load", {});
			}
		};
		var on_video_error = function (event) {
			if (this.type !== MediaPreview.TYPE_VIDEO) return;

			// Hide video
			style.remove_class(this.nodes_video.video, "iex_mpreview_video_visible");
			this.nodes_video.video.removeAttribute("src");

			// Error
			trigger.call(this, "video_error", {
				reason: "error"
			});
		};
		var on_video_timeupdate = function (event) {
			if (this.type !== MediaPreview.TYPE_VIDEO) return;

			// Update time progress
			update_video_seek_status.call(this, this.nodes_video.video.currentTime);
		};
		var on_video_ended = function (event) {
			if (this.type !== MediaPreview.TYPE_VIDEO) return;

			// Update buttons
			update_video_play_status.call(this, this.nodes_video.video.paused);
		};

		var on_vcontrols_mouseenter = function (event, node) {
			// Do not show for non-video
			if (this.type !== MediaPreview.TYPE_VIDEO) return;

			// Show controls
			style.remove_class(this.nodes_video.volume_container, "iex_mpreview_vcontrols_volume_container_visible");
			style.add_class(this.nodes_video.overlay_table, "iex_mpreview_vcontrols_table_visible");
		};
		var on_vcontrols_mouseleave = function (event, node) {
			// Hide controls
			style.remove_class(this.nodes_video.overlay_table, "iex_mpreview_vcontrols_table_visible");
		};
		var on_vcontrols_container_mousedown = function (event) {
			// Ignore if not left mb
			if (get_event_mouse_button(event) !== 1) return;

			// Stop event
			return stop_event(event);
		};
		var on_vcontrols_play_button_click = function (event) {
			// Play/pause
			var video = this.nodes_video.video;

			this.set_video_paused(!video.paused, (event.shiftKey && video.paused) ? (!video.loop) : undefined);
		};
		var on_vcontrols_volume_button_click = function (event) {
			// Mute/unmute
			this.set_video_muted(!this.nodes_video.video.muted);
		};
		var on_vcontrols_volume_button_mouseenter = function (event, node) {
			// Show volume bar
			style.add_class(this.nodes_video.volume_container, "iex_mpreview_vcontrols_volume_container_visible");
		};
		var on_vcontrols_volume_button_container_mouseleave = function (event, node) {
			// Hide volume bar
			style.remove_class(this.nodes_video.volume_container, "iex_mpreview_vcontrols_volume_container_visible");
		};
		var on_vcontrols_seek_bar_mousedown = function (event) {
			// Get the mouse button
			if (get_event_mouse_button(event) !== 1) return;

			// Begin seeking
			var video = this.nodes_video.video;

			// Set paused state
			this.nodes_video.seeking = true;
			this.nodes_video.seeking_paused = video.paused;
			if (!video.paused) video.pause();

			// Force visibility
			style.add_class(this.nodes_video.overlay_table, "iex_mpreview_vcontrols_table_visible_important");

			// Setup window mousemove and mouseup events
			setup_vcontrols_mouse_capture.call(this);

			// Initial update
			on_vcontrols_capture_mousemove.call(this, event);

			// Stop
			return stop_event(event);
		};
		var on_vcontrols_volume_bar_mousedown = function (event) {
			// Get the mouse button
			if (get_event_mouse_button(event) !== 1) return;

			// Set paused state
			this.nodes_video.volume_modifying = true;

			// Force visibility
			style.add_class(this.nodes_video.overlay_table, "iex_mpreview_vcontrols_table_visible_important");
			style.add_class(this.nodes_video.volume_container, "iex_mpreview_vcontrols_volume_container_visible_important");

			// Setup window mousemove and mouseup events
			setup_vcontrols_mouse_capture.call(this);

			// Initial update
			on_vcontrols_capture_mousemove.call(this, event);

			// Stop
			return stop_event(event);
		};
		var on_vcontrols_prevent_default_mousedown = function (event) {
			event.preventDefault();
			return false;
		};

		var on_vcontrols_capture_mousemove = function (event) {
			// Begin seeking
			var video = this.nodes_video.video,
				bar_rect = this.nodes_video.mouse_capturing_rect,
				percent, x, y;


			// Get mouse x/y
			if (event.pageX === undefined && (event = event || window.event).clientX !== undefined) {
				var html = document.documentElement, body = document.body;
				if (html && body) {
					x = ((event.clientX || 0) + (html.scrollLeft || (body && body.scrollLeft) || 0)) - (html.clientLeft || 0);
					y = ((event.clientY || 0) + (html.scrollTop || (body && body.scrollTop) || 0)) - (html.clientTop || 0);
				}
			}
			else {
				x = event.pageX || 0;
				y = event.pageY || 0;
			}


			// Set paused state
			if (this.nodes_video.seeking) {
				var duration = video.duration;
				if (!isNaN(duration)) {
					var loaded = (video.buffered.length > 0) ? video.buffered.end(0) / duration : 0.0;

					// Get percent
					percent = (x - bar_rect.left) / (bar_rect.right - bar_rect.left);

					// Bound
					if (percent < 0) percent = 0;
					else if (percent > loaded) percent = loaded;

					// Apply
					percent *= duration;
					video.currentTime = percent;
					update_video_seek_status.call(this, percent);
				}
			}
			else if (this.nodes_video.volume_modifying) {
				// Get percent
				percent = 1.0 - (y - bar_rect.top) / (bar_rect.bottom - bar_rect.top);

				// Bound
				if (percent < 0) percent = 0;
				else if (percent > 1.0) percent = 1.0;

				// Set volume
				set_video_volume.call(this, percent, "seeking", true);
			}
		};
		var on_vcontrols_capture_mouseup = function (event) {
			if (this.nodes_video.seeking) {
				// Unpause
				if (!this.nodes_video.seeking_paused) this.nodes_video.video.play();

				// Un-force visibility
				style.remove_class(this.nodes_video.overlay_table, "iex_mpreview_vcontrols_table_visible_important");

				// Done
				this.nodes_video.seeking = false;
			}
			else if (this.nodes_video.volume_modifying) {
				// Save volume
				set_video_volume.call(this, -1, "seek", true);

				// Un-force visibility
				style.remove_class(this.nodes_video.overlay_table, "iex_mpreview_vcontrols_table_visible_important");
				style.remove_class(this.nodes_video.volume_container, "iex_mpreview_vcontrols_volume_container_visible_important");

				// Done
				this.nodes_video.volume_modifying = false;
			}

			// Stop capture
			teardown_vcontrols_mouse_capture.call(this);
		};

		var on_image_load = function (event) {
			if (this.type != MediaPreview.TYPE_IMAGE) return;

			// True size
			var w = this.nodes_image.image.naturalWidth,
				h = this.nodes_image.image.naturalHeight;

			// Update size
			if (!this.size.acquired || (this.size.width != w || this.size.height != h)) {
				update_size.call(this, w, h);
				style.remove_class(this.nodes_image.image, "iex_mpreview_image_unsized");
			}

			// Event
			trigger.call(this, "image_load", {});
		};
		var on_image_error = function (event) {
			if (this.type != MediaPreview.TYPE_IMAGE) return;

			// Hide image
			style.remove_class(this.nodes_image.image, "iex_mpreview_image_visible");
			this.nodes_image.image.removeAttribute("src");

			// Error
			trigger.call(this, "image_error", {
				reason: "error"
			});
		};

		var on_mousewheel = function (event) {
			// Get direction
			var delta = (event.wheelDelta || -event.detail || 0);
			if (delta < 0) delta = -1;
			else if (delta > 0) delta = 1;

			// Trigger event
			trigger.call(this, "mouse_wheel", {
				delta: delta,
				mode: this.mouse_wheel_mode
			});

			// Prevent
			return stop_event(event);
		};
		var on_mouseenter = function (event, node) {
			// Trigger event
			trigger.call(this, "mouse_enter", {});
		};
		var on_mouseleave = function (event, node) {
			// Trigger event
			trigger.call(this, "mouse_leave", {});
		};
		var on_mousedown = function (event) {
			// Get the mouse button
			var button = get_event_mouse_button(event);

			// Activate close
			trigger.call(this, "mouse_down", {
				button: button
			});
			return stop_event(event);
		};
		var on_contextmenu = function (event) {
			// Stop
			return stop_event(event);
		};
		var on_mousemove = function (event) {
			// Get mouse x/y
			var x, y;
			if (event.pageX === undefined && (event = event || window.event).clientX !== undefined) {
				var html = document.documentElement, body = document.body;
				if (html && body) {
					x = ((event.clientX || 0) + (html.scrollLeft || (body && body.scrollLeft) || 0)) - (html.clientLeft || 0);
					y = ((event.clientY || 0) + (html.scrollTop || (body && body.scrollTop) || 0)) - (html.clientTop || 0);
				}
			}
			else {
				x = event.pageX || 0;
				y = event.pageY || 0;
			}

			// Event
			trigger.call(this, "mouse_move", {
				x: x,
				y: y,
			});
		};
		var on_overflow_mouseenter = function (event, node) {
			// Trigger event
			trigger.call(this, "mouse_enter_main", {});
		};

		var on_stats_mouseenter = function (event, node) {
			// Stop hiding if it's already open
			this.nodes.mouse_in_stats = true;
			show_zoom_controls.call(this, false);
		};
		var on_stats_mouseleave = function (event, node) {
			// Hide controls
			this.nodes.mouse_in_stats = false;
			hide_zoom_controls.call(this, false, true);
		};
		var on_stats_zoom_mouseenter = function (event, node) {
			// Show controls
			if (this.nodes.stats_zoom_controls_enabled) {
				show_zoom_controls.call(this, true);
			}
		};
		var on_stats_zoom_controls_mouseenter = function (event, node) {
			// Display controls and make fixed
			show_zoom_controls.call(this, true);
			set_zoom_controls_fixed.call(this, true);
		};
		var on_stats_zoom_controls_mouseleave = function (event, node) {
			// Make un-fixed and possibly hide
			set_zoom_controls_fixed.call(this, false);
			if (!this.nodes.mouse_in_stats) {
				hide_zoom_controls.call(this, true, true);
			}
		};
		var on_stats_zoom_controls_mousedown = function (event) {
			// Stop
			return stop_event(event);
		};
		var on_stats_zoom_control_click = function (delta, event) {
			// Event
			trigger.call(this, "stats_zoom_control_click", {
				delta: delta
			});

			// Stop
			return stop_event(event);
		};



		MediaPreview.prototype = {
			constructor: MediaPreview,

			destroy: function () {
				// Clear
				this.clear();

				// Remove events
				for (var i = 0, el; i < this.event_listeners.length; ++i) {
					el = this.event_listeners[i];
					this.remove_event_listener(el[0], el[1], el[2], el[3]);
				}
				this.event_listeners = [];

				// Remove
				this.remove();
			},

			clear: function () {
				var type = this.type;
				this.type = MediaPreview.TYPE_NONE;

				// Clear background
				this.nodes.background.style.backgroundImage = "";
				style.remove_classes(this.nodes.background, "iex_mpreview_background_visible iex_mpreview_background_visible_full iex_mpreview_background_disabled iex_mpreview_background_fallback");

				if (type == MediaPreview.TYPE_IMAGE) {
					// Remove previous image
					style.remove_class(this.nodes_image.image, "iex_mpreview_image_visible");
					this.nodes_image.image.removeAttribute("src");
				}
				else if (type == MediaPreview.TYPE_VIDEO) {
					// Clear video
					style.remove_classes(this.nodes_video.video, "iex_mpreview_video_visible iex_mpreview_video_not_ready");
					this.set_video_paused(true);
					this.nodes_video.video.removeAttribute("src");

					// Clear seek status
					this.nodes_video.load_progress.style.width = "0";
					this.nodes_video.play_progress.style.width = "0";

					// Update time numbers
					this.nodes_video.time_current.textContent = "";
					this.nodes_video.time_duration.textContent = "";

					update_video_play_status.call(this, true);
				}

				// Clear stats
				this.clear_stats();
			},

			on: function (event, callback) {
				// Add callback
				if (event in this.events) {
					this.events[event].push(callback);
				}
			},
			off: function (event, callback) {
				// Add callback
				if (event in this.events) {
					var e_list = this.events[event];
					for (var i = 0; i < e_list.length; ++i) {
						if (e_list[i] === callback) {
							// Remove
							e_list.splice(i, 1);
							return true;
						}
					}
				}

				return false;
			},

			get_type: function () {
				return this.type;
			},
			get_video_volume: function () {
				return (this.type !== MediaPreview.TYPE_VIDEO) ? 0.0 : this.nodes_video.video.volume;
			},
			get_video_muted: function () {
				return (this.type !== MediaPreview.TYPE_VIDEO) ? true : this.nodes_video.video.muted;
			},

			set_image: function (image) {
				// Unset previous
				this.clear();

				// Set type
				this.type = MediaPreview.TYPE_IMAGE;
				if (this.nodes_image === null) this.nodes_image = new MediaPreviewImageNodes(this);

				// Set
				style.add_class(this.nodes_image.image, "iex_mpreview_image_visible");
				this.nodes_image.image.setAttribute("src", image.replace(/#.*$/, ""));
			},
			set_video: function (video) {
				// Unset previous
				this.clear();

				// Set type
				this.type = MediaPreview.TYPE_VIDEO;
				if (this.nodes_video === null) {
					this.nodes_video = new MediaPreviewVideoNodes(this);
					this.set_vcontrols_borders_rounded(this.style.vcontrols_rounded);
				}

				// Set
				style.add_class(this.nodes_video.video, "iex_mpreview_video_visible");
				style.add_class(this.nodes_video.video, "iex_mpreview_video_not_ready");
				this.nodes_video.video.setAttribute("src", video.replace(/#.*$/, ""));
			},
			set_background: function (image, visibility_level) {
				// visibility_level: 0=hidden, 1=transparent, 2=full
				if (image) {
					// Set background
					style.add_class(this.nodes.background, "iex_mpreview_background_visible");
					this.set_background_style(visibility_level);
					this.nodes.background.style.backgroundImage = 'url("' + image + '")';
				}
				else {
					// Clear background
					style.remove_classes(this.nodes.background, "iex_mpreview_background_visible iex_mpreview_background_disabled iex_mpreview_background_visible_full");
					this.nodes.background.style.backgroundImage = "";
				}
			},
			set_background_style: function (visibility_level) {
				// visibility_level: 0=hidden, 1=transparent, 2=full
				if (visibility_level === 0) {
					style.add_class(this.nodes.background, "iex_mpreview_background_disabled");
					style.remove_class(this.nodes.background, "iex_mpreview_background_visible_full");
				}
				else if (visibility_level == 1) {
					style.remove_classes(this.nodes.background, "iex_mpreview_background_disabled iex_mpreview_background_visible_full");
				}
				else {
					style.add_class(this.nodes.background, "iex_mpreview_background_visible_full");
					style.remove_class(this.nodes.background, "iex_mpreview_background_disabled");
				}
			},
			set_background_animations: function (animate) {
				if (animate) style.add_class(this.nodes.background, "iex_transitions");
				else style.remove_class(this.nodes.background, "iex_transitions");
			},

			set_mouse_wheel_mode: function (mode) {
				this.mouse_wheel_mode = mode;
			},

			set_view_borders: function (horizontal, vertical) {
				var zoom_borders = this.nodes.zoom_borders,
					w = (horizontal * 100).toFixed(2) + "%",
					h = (vertical * 100).toFixed(2) + "%";

				// Set view borders sizing
				zoom_borders.style.left = w;
				zoom_borders.style.top = h;
				zoom_borders.style.right = w;
				zoom_borders.style.bottom = h;
			},
			set_view_borders_visible: function (visible) {
				var zoom_borders = this.nodes.zoom_borders;

				// Change visibility
				if (visible === true) {
					style.add_class(zoom_borders, "iex_mpreview_zoom_borders_visible");
				}
				else {
					style.remove_class(zoom_borders, "iex_mpreview_zoom_borders_visible");
				}
			},
			set_view_borders_visible_sides: function (horizontal, vertical) {
				var zoom_borders = this.nodes.zoom_borders;

				// Change visibility
				if (horizontal) style.add_class(zoom_borders, "iex_mpreview_zoom_borders_horizontal");
				else style.remove_class(zoom_borders, "iex_mpreview_zoom_borders_horizontal");

				if (vertical) style.add_class(zoom_borders, "iex_mpreview_zoom_borders_vertical");
				else style.remove_class(zoom_borders, "iex_mpreview_zoom_borders_vertical");
			},
			set_mouse_visible: function (visible) {
				if (visible) style.remove_class(this.cpreview.nodes.padding, "iex_mpreview_mouse_hidden");
				else style.add_class(this.cpreview.nodes.padding, "iex_mpreview_mouse_hidden");
			},

			add_to: function (parent) {
				this.cpreview.add_to(parent);
			},
			remove: function () {
				this.cpreview.remove();
			},

			set_visible: function (visible) {
				this.cpreview.set_visible(visible);
				if (!visible) this.clear();
			},
			set_fixed: function (fixed) {
				this.cpreview.set_fixed(fixed);
			},
			set_window: function (x, y, width, height, fixed) {
				this.cpreview.set_window(x, y, width, height, fixed);
				trigger.call(this, "resize", {});
			},
			set_zoom: function (zoom, fit, large, axis) {
				// Update fit
				if (fit !== undefined) {
					this.fit = fit;
					this.fit_large = large;
				}

				// Detect scale
				if (axis !== undefined) this.fit_axis = axis;
				var scale = 1.0;
				if (this.fit) {
					scale = (this.fit_axis === 0) ?
						(this.cpreview.display.width / this.size.width) :
						(this.cpreview.display.height / this.size.height);
				}

				// Set zoom
				if (zoom !== undefined) {
					this.zoom = zoom;
				}

				scale *= this.zoom;
				this.cpreview.set_size(this.size.width * scale, this.size.height * scale);
				trigger.call(this, "resize", {});
			},
			set_size: function (width, height, acquired) {
				this.size.width = width;
				this.size.height = height;
				if (acquired === true || acquired === false) this.size.acquired = acquired;
			},
			set_offset: function (x, y) {
				this.cpreview.set_offset(x, y);
			},
			set_paddings: function (top, right, bottom, left) {
				this.cpreview.set_paddings(top, right, bottom, left);
			},
			clear_size: function () {
				this.cpreview.clear_size();
			},
			clear_paddings: function () {
				this.cpreview.clear_paddings();
			},

			video_interacted: function () {
				return (this.nodes_video !== null && this.nodes_video.interacted);
			},
			clear_video_interactions: function () {
				if (this.nodes_video !== null) this.nodes_video.interacted = false;
			},

			set_video_muted: function (muted) {
				if (this.nodes_video === null) return;

				// Apply
				this.nodes_video.video.muted = muted;

				// Change icon
				if (muted) style.add_class_svg(this.nodes_video.svg_volume, "iex_svg_volume_button_muted");
				else style.remove_class_svg(this.nodes_video.svg_volume, "iex_svg_volume_button_muted");

				// Interacted
				this.nodes_video.interacted = true;
			},
			set_video_volume: function (volume) {
				if (this.nodes_video === null) return;

				set_video_volume.call(this, volume, "set", true);
			},
			set_video_paused: function (paused, loop) {
				if (this.nodes_video === null) return;

				var video = this.nodes_video.video;

				// Loop
				if (loop === true || loop === false) {
					video.loop = loop;
				}

				// Pause or play
				if (paused) video.pause();
				else video.play();

				// Interacted
				this.nodes_video.interacted = true;

				// Update buttons
				update_video_play_status.call(this, paused);
			},

			set_show_volume_controls_temp: function (show, duration) {
				if (this.type !== MediaPreview.TYPE_VIDEO) return;

				// Toggle
				var cls1 = "iex_mpreview_vcontrols_table_visible_temp",
					cls2 = "iex_mpreview_vcontrols_volume_container_visible_temp";

				if (show) {
					style.add_class(this.nodes_video.overlay_table, cls1);
					style.add_class(this.nodes_video.volume_container, cls2);

					// Timer
					if (duration !== undefined) {
						if (this.volume_controls_temp_timer !== null) {
							clearTimeout(this.volume_controls_temp_timer);
						}
						this.volume_controls_temp_timer = setTimeout(this.set_show_volume_controls_temp.bind(this, false, 0.0), duration * 1000);
					}
				}
				else {
					// Hide
					style.remove_class(this.nodes_video.overlay_table, cls1);
					style.remove_class(this.nodes_video.volume_container, cls2);

					// Clear timeout
					if (this.volume_controls_temp_timer !== null) {
						clearTimeout(this.volume_controls_temp_timer);
						this.volume_controls_temp_timer = null;
					}
				}
			},

			is_visible: function () {
				return this.cpreview.visible;
			},

			get_inner_rect: function () {
				return style.get_object_rect(this.cpreview.nodes.overflow);
			},
			get_window: function () {
				return this.cpreview.display;
			},

			add_event_listener: function (node, event, callback, capture) {
				node.addEventListener(event, callback, capture);
				this.event_listeners.push([node, event, callback, capture]);
			},
			remove_event_listener: function (node, event, callback, capture, remove) {
				node.removeEventListener(event, callback, capture);
				if (remove) {
					for (var i = 0, j = this.event_listeners.length; i < j; ++i) {
						if (
							this.event_listeners[i][0] === event &&
							this.event_listeners[i][1] === callback &&
							this.event_listeners[i][2] === capture
						) {
							this.event_listeners.splice(i, 1);
							return;
						}
					}
				}
			},

			set_vcontrols_mini_available: function (available) {
				if (this.nodes_video === null) return;

				if (available) style.remove_class(this.nodes_video.overlay_table, "iex_mpreview_vcontrols_table_mini_disabled");
				else style.add_class(this.nodes_video.overlay_table, "iex_mpreview_vcontrols_table_mini_disabled");
			},
			set_vcontrols_mini_visible: function (visible) {
				if (this.nodes_video === null) return;

				if (visible) style.add_class(this.nodes_video.overlay_table, "iex_mpreview_vcontrols_table_mini");
				else style.remove_class(this.nodes_video.overlay_table, "iex_mpreview_vcontrols_table_mini");
			},
			set_vcontrols_borders_rounded: function (rounded) {
				this.style.vcontrols_rounded = rounded;
				if (this.nodes_video === null) return;

				if (rounded) {
					style.remove_class(this.nodes_video.volume_bar, "iex_mpreview_vcontrols_no_border_radius");
					style.remove_class(this.nodes_video.seek_bar, "iex_mpreview_vcontrols_no_border_radius");
				}
				else {
					style.add_class(this.nodes_video.volume_bar, "iex_mpreview_vcontrols_no_border_radius");
					style.add_class(this.nodes_video.seek_bar, "iex_mpreview_vcontrols_no_border_radius");
				}
			},

			clear_stats: function () {
				this.set_stat_zoom(false);
				this.set_stat_status(false);
				this.set_stat_resolution(false);
				this.set_stat_file_size(false);
				this.set_stat_file_name(false);
			},
			set_stat_zoom: function (visible, text, text_fit) {
				if (visible === false) {
					style.remove_class(this.nodes.stat_zoom_container, "iex_mpreview_stat_visible");
					update_stat_sep_visibility.call(this, this.nodes.stat_zoom_container, false);
					this.nodes.stat_zoom.textContent = "";
					this.nodes.stat_zoom_fit.textContent = "";
				}
				else {
					if (visible) {
						style.add_class(this.nodes.stat_zoom_container, "iex_mpreview_stat_visible");
						update_stat_sep_visibility.call(this, this.nodes.stat_zoom_container, true);
					}
					this.nodes.stat_zoom.textContent = text;
					this.nodes.stat_zoom_fit.textContent = text_fit;
				}
			},
			set_stat_status: function (visible, text, type) {
				if (visible === false) {
					style.remove_classes(this.nodes.stat_status, "iex_mpreview_stat_visible iex_mpreview_stat_red");
					update_stat_sep_visibility.call(this, this.nodes.stat_status, false);
					this.nodes.stat_status.textContent = "";
				}
				else {
					if (visible) {
						style.add_class(this.nodes.stat_status, "iex_mpreview_stat_visible");
						update_stat_sep_visibility.call(this, this.nodes.stat_status, true);
					}
					if (type == "error") {
						style.add_class(this.nodes.stat_status, "iex_mpreview_stat_red");
					}
					else {
						style.remove_class(this.nodes.stat_status, "iex_mpreview_stat_red");
					}
					this.nodes.stat_status.textContent = text;
				}
			},
			set_stat_resolution: function (visible, text) {
				if (visible === false) {
					style.remove_class(this.nodes.stat_resolution, "iex_mpreview_stat_visible");
					update_stat_sep_visibility.call(this, this.nodes.stat_resolution, false);
					this.nodes.stat_resolution.textContent = "";
				}
				else {
					if (visible) {
						style.add_class(this.nodes.stat_resolution, "iex_mpreview_stat_visible");
						update_stat_sep_visibility.call(this, this.nodes.stat_resolution, true);
					}
					this.nodes.stat_resolution.textContent = text;
				}
			},
			set_stat_file_size: function (visible, text) {
				if (visible === false) {
					style.remove_class(this.nodes.stat_filesize, "iex_mpreview_stat_visible");
					update_stat_sep_visibility.call(this, this.nodes.stat_filesize, false);
					this.nodes.stat_filesize.textContent = "";
				}
				else {
					if (visible) {
						style.add_class(this.nodes.stat_filesize, "iex_mpreview_stat_visible");
						update_stat_sep_visibility.call(this, this.nodes.stat_filesize, true);
					}
					this.nodes.stat_filesize.textContent = text;
				}
			},
			set_stat_file_name: function (visible, text) {
				if (visible === false) {
					style.remove_class(this.nodes.stat_filename, "iex_mpreview_stat_visible");
					update_stat_sep_visibility.call(this, this.nodes.stat_filename, false);
					this.nodes.stat_filename.textContent = "";
				}
				else {
					if (visible) {
						style.add_class(this.nodes.stat_filename, "iex_mpreview_stat_visible");
						update_stat_sep_visibility.call(this, this.nodes.stat_filename, true);
					}
					this.nodes.stat_filename.textContent = text;
				}
			},

			set_zoom_control_buttons_enabled: function (enabled) {
				if (this.nodes === null) return;

				this.nodes.stats_zoom_controls_enabled = enabled;
			},

			transfer_video_state: function (node) {
				if (node.tagName === "VIDEO" && this.type === MediaPreview.TYPE_VIDEO) {
					// Transfer video state
					var v = this.nodes_video.video,
						muted = v.muted,
						volume = v.volume,
						time = v.currentTime,
						paused = v.paused,
						okay = false,
						fn;

					fn = function () {
						this.muted = muted;
						this.volume = volume;
						this.currentTime = time;
						if (paused) this.pause();
						else this.play();
					};

					if (!node.paused || !isNaN(node.duration)) {
						// Immediate
						try {
							fn.call(node);
							okay = true;
						}
						catch (e) {}
					}
					if (!okay) {
						// Delay until playing
						var play_fn = function (event) {
							try {
								fn.call(this);
							}
							catch (e) {}
							this.removeEventListener("play", play_fn, false);
						};
						node.addEventListener("play", play_fn, false);
					}
				}
			},
			load_video_state: function (state) {
				if (this.type === MediaPreview.TYPE_VIDEO) {
					// Update
					var self = this,
						v = this.nodes_video.video,
						target = v.getAttribute("src") || "",
						event_fn, fn;

					fn = function () {
						// Set state
						this.set_video_muted(state.muted);
						this.set_video_volume(state.volume);
						this.set_video_paused(state.paused);
						this.nodes_video.video.currentTime = state.time;
					};

					if (isNaN(v.currentTime) || v.buffered.length <= 0) {
						// Delay
						self = this;
						event_fn = function () {
							if (target === this.getAttribute("src") || "") {
								fn.call(self);
							}
							this.removeEventListener("loadedmetadata", event_fn, false);
						};

						// Listen
						v.addEventListener("loadedmetadata", event_fn, false);
					}
					else {
						fn.call(this);
					}
				}
			},

			get_size: function () {
				return this.size;
			},
		};



		return MediaPreview;

	})();

	// Content preview base
	var ContentPreview = (function () {

		var ContentPreviewNodes = function () {
			// Main container
			this.container = $.div("iex_cpreview_container iex_cpreview_container_visible");

			// Position/padding container
			this.padding = $.div("iex_cpreview_padding");
			this.container.appendChild(this.padding);

			// Overflow container
			this.overflow = $.div("iex_cpreview_overflow");
			this.padding.appendChild(this.overflow);

			// Offset container
			this.offset = $.div("iex_cpreview_offset");
			this.overflow.appendChild(this.offset);
		};



		var ContentPreview = function () {
			this.nodes = new ContentPreviewNodes();

			this.visible = true;

			this.offset_x = 0;
			this.offset_y = 0;

			this.size = {
				width: 0,
				height: 0,
			};
			this.display = {
				x: 0,
				y: 0,
				width: 0,
				height: 0,
			};

			this.fixed = false;
			this.auto = false;
		};



		ContentPreview.prototype = {
			constructor: ContentPreview,

			set_visible: function (visible) {
				// Add or remove visibility
				this.visible = visible;
				if (visible) style.add_class(this.nodes.container, "iex_cpreview_container_visible");
				else style.remove_class(this.nodes.container, "iex_cpreview_container_visible");
			},
			set_fixed: function (fixed) {
				// No change
				if (this.fixed == fixed) return;

				var container = this.nodes.container,
					doc_offset = style.get_document_offset();

				// Fixed position
				this.fixed = fixed;
				if (this.fixed) {
					this.display.x -= doc_offset.left;
					this.display.y -= doc_offset.top;
					style.add_class(container, "iex_cpreview_container_fixed");
				}
				else {
					this.display.x += doc_offset.left;
					this.display.y += doc_offset.top;
					style.remove_class(container, "iex_cpreview_container_fixed");
				}

				// Position
				container.style.left = this.display.x.toFixed(2) + "px";
				container.style.top = this.display.y.toFixed(2) + "px";
			},
			set_window: function (x, y, width, height, fixed) {
				var container = this.nodes.container,
					overflow = this.nodes.overflow;

				// Fixed position
				if (fixed !== undefined) {
					this.fixed = fixed;
					if (fixed) style.add_class(this.nodes.container, "iex_cpreview_container_fixed");
					else style.remove_class(this.nodes.container, "iex_cpreview_container_fixed");
				}

				// Position
				container.style.left = x.toFixed(2) + "px";
				container.style.top = y.toFixed(2) + "px";

				// Size
				overflow.style.width = width.toFixed(2) + "px";
				overflow.style.height = height.toFixed(2) + "px";

				// Set
				this.display.x = x;
				this.display.y = y;
				this.display.width = width;
				this.display.height = height;
			},
			set_size: function (width, height, offset_x, offset_y) {
				var offset = this.nodes.offset,
					w_diff = width - this.display.width,
					h_diff = height - this.display.height;

				// Bound
				if (w_diff < 0) w_diff = 0;
				if (h_diff < 0) h_diff = 0;

				// Set size
				this.size.width = width;
				this.size.height = height;

				// Set offset
				if (offset_x !== undefined && offset_y !== undefined) {
					this.offset_x = offset_x;
					this.offset_y = offset_y;
				}

				// Set visible size
				offset.style.width = width.toFixed(2) + "px";
				offset.style.height = height.toFixed(2) + "px";
				style.add_class(offset, "iex_cpreview_offset_sized");

				// Set visible offset
				offset.style.left = (w_diff * -this.offset_x).toFixed(2) + "px";
				offset.style.top = (h_diff * -this.offset_y).toFixed(2) + "px";
			},
			set_offset: function (x, y) {
				var offset = this.nodes.offset,
					w_diff = this.size.width - this.display.width,
					h_diff = this.size.height - this.display.height;

				// Bound
				if (w_diff < 0) w_diff = 0;
				if (h_diff < 0) h_diff = 0;

				// Set offset
				this.offset_x = x;
				this.offset_y = y;

				// Set visible offset
				offset.style.left = (w_diff * -x).toFixed(2) + "px";
				offset.style.top = (h_diff * -y).toFixed(2) + "px";
			},
			set_paddings: function (top, right, bottom, left) {
				// Set paddings
				this.nodes.padding.style.top = (-top).toFixed(2) + "px";
				this.nodes.padding.style.left = (-left).toFixed(2) + "px";
				this.nodes.padding.style.padding = top.toFixed(2) + "px " + right.toFixed(2) + "px " + bottom.toFixed(2) + "px " + left.toFixed(2) + "px";
			},
			set_auto_size: function (auto) {
				// No change
				if (this.auto == auto) return;

				var container = this.nodes.container;

				// Fixed position
				this.auto = auto;
				if (this.auto) {
					style.add_class(container, "iex_cpreview_container_auto");
				}
				else {
					style.remove_class(container, "iex_cpreview_container_auto");
				}

				// Position
				this.display.x = 0;
				this.display.y = 0;
			},
			update_auto_size: function () {
				// Get size
				var size = style.get_object_size(this.nodes.container);

				// Set
				this.size.width = size.width;
				this.size.height = size.height;
				this.display.width = size.width;
				this.display.height = size.height;
			},

			clear_size: function () {
				this.nodes.offset.style.width = "";
				this.nodes.offset.style.height = "";

				this.size.width = 0;
				this.size.height = 0;
			},
			clear_paddings: function () {
				// Clear paddings
				this.nodes.padding.style.top = "";
				this.nodes.padding.style.left = "";
				this.nodes.padding.style.padding = "";
			},

			add_to: function (parent) {
				parent.appendChild(this.nodes.container);
			},
			remove: function () {
				var par = this.nodes.container.parentNode;
				if (par) par.removeChild(this.nodes.container);
			},
			add_content: function (node) {
				// Add to offset
				style.add_class(node, "iex_cpreview_content");
				this.nodes.offset.appendChild(node);
			},
			add_overlay: function (node, outside) {
				// Add to overflow or padding
				style.add_class(node, "iex_cpreview_overlay");
				(outside ? this.nodes.padding : this.nodes.overflow).appendChild(node);
			},

		};



		return ContentPreview;

	})();
	// Class for bit streaming to a string
	var BitStream = (function () {

		var BitStream = function () {
			this.value_pre = 0;
			this.value = 0;
			this.bits = 0;
			this.string = "";
			this.pos = 0;
			this.pos_bit = 0;
			this.value_pre_decode = 0;
		};

		BitStream.bits_per_char = 6;
		BitStream.bits_per_char_mask = (1 << BitStream.bits_per_char) - 1;
		BitStream.alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-";
		BitStream.re_alphabet = /^[a-zA-Z0-9\+\-]*/;
		BitStream.alphabet_map = (function (alphabet) {
			var length = alphabet.length,
				map = {},
				i;

			for (i = 0; i < length; ++i) map[alphabet[i]] = i;

			return map;
		})(BitStream.alphabet);

		BitStream.prototype = {
			constructor: BitStream,
			value_to_char: function (v) {
				return BitStream.alphabet[v];
			},
			pos_to_value: function (pos) {
				return (pos >= this.string.length) ? 0 : BitStream.alphabet_map[this.string[pos]];
			},
			encode_value: function (v) {
				return (v ^ ((this.value_pre + this.string.length) % BitStream.alphabet.length));
			},
			decode_value: function (v) {
				return (v ^ ((this.value_pre_decode + this.pos) % BitStream.alphabet.length));
			},
			add: function (value, bits) {
				var b;

				while (bits >= (b = BitStream.bits_per_char - this.bits)) {
					this.value |= (value << this.bits) & BitStream.bits_per_char_mask;
					this.value = this.encode_value(this.value);

					this.string += this.value_to_char(this.value);

					value >>>= b;
					bits -= b;
					this.bits = 0;
					this.value_pre = this.value;
					this.value = 0;
				}

				this.value |= (value & ((1 << bits) - 1)) << this.bits;
				this.bits += bits;
				if (this.bits<0)console.log("value=",value,bits);
			},
			get: function (bits) {
				if (bits === undefined) bits = 1;

				var value = 0,
					shift = 0,
					c, cr, b;

				while (bits >= (b = BitStream.bits_per_char - this.pos_bit)) {
					cr = this.pos_to_value(this.pos);
					c = this.decode_value(cr);
					//c = (cr ^ ((this.value_pre_decode + this.pos) % BitStream.alphabet.length));

					value |= ((c >>> this.pos_bit) & ((1 << b) - 1)) << shift;

					bits -= b;
					shift += b;
					this.pos_bit = 0;
					++this.pos;
					this.value_pre_decode = cr;
				}

				if (bits > 0) {
					//c = (this.pos_to_value(this.pos) ^ ((this.value_pre_decode + this.pos) % BitStream.alphabet.length));
					c = this.decode_value(this.pos_to_value(this.pos));
					value |= ((c >>> this.pos_bit) & ((1 << bits) - 1)) << shift;
					this.pos_bit += bits;
				}

				return value;
			},
			encode: function () {
				var s = this.string;
				if (this.bits > 0) s += this.value_to_char(this.encode_value(this.value));
				return s;
			},
			decode: function (string) {
				this.string = string;
				this.pos = 0;
				this.pos_bit = 0;
				this.value_pre_decode = 0;
			},
			to_encoded_string: function () {
				// Converts the content decoded thus far into a new encoded string
				var s = this.string.substr(0, this.pos),
					c;

				if (this.pos_bit === 0) return s;

				c = this.decode_value(this.pos_to_value(this.pos));
				c &= (1 << this.pos_bit) - 1; // mask off the end
				c = (c ^ ((this.value_pre_decode + this.pos) % BitStream.alphabet.length));
				s += this.value_to_char(c);

				return s;
			},
			remaining: function () {
				if (this.pos >= this.string.length) return 0;
				return (this.string.length - this.pos + 1) * BitStream.bits_per_char - this.pos_bit;
			},
			eof: function () {
				return (this.pos >= this.string.length);
			},
		};

		return BitStream;

	})();

	// Image annotation
	var Annotation = (function () {

		var Annotation = function (parent, settings) {
			this.parent = parent;

			this.selected = false;
			this.index = 0;
			this.font_index = 0;
			this.font_size_index = 8;
			this.font_size = 0;
			this.font_bold = false;
			this.font_italic = false;
			this.text_before = null;
			this.text = "";
			this.text_display = "";
			this.text_formatters = [];
			this.text_align = Annotation.AlignDefault;
			this.text_line_spacing = Annotation.line_spacing.to_index(0);
			this.text_char_spacing = Annotation.char_spacing.to_index(0);
			this.text_decoration = Annotation.TextDecorationNone;
			this.colors = [ 0, 1, 1 ]; // text, alt, background
			this.color_index_selected = 0;
			this.position = [ 32, 32 ];
			this.size = [ 100, 100 ];
			this.nodes = {
				edit: null,
				annotation: null,
			};
			this.events = [];
			this.edit_data = (settings && settings.edit) ? new Annotation.EditData() : null;

			if (settings !== null) {
				this.index = settings.index || 0;
				if ("font_size_index" in settings) {
					this.font_size_index = settings.font_size_index;
				}
				if ("x" in settings) this.position[0] = settings.x;
				if ("y" in settings) this.position[1] = settings.y;
				if ("width" in settings) this.size[0] = settings.width;
				if ("height" in settings) this.size[1] = settings.height;
				if ("text_before" in settings) this.text_before = settings.text_before;
				if ("font" in settings) this.font_index = Math.min(Math.max(0, settings.font), Annotation.fonts.length);
				if ("bold" in settings) this.font_bold = settings.bold;
				if ("italic" in settings) this.font_italic = settings.italic;
			}

			this.font_size = Annotation.font_size_index_to_size(this.font_size_index);

			this.create_nodes((settings && settings.edit) || false);
		};



		Annotation.EditData = function () {
			this.move_events = [];
			this.move_mode = 0;
			this.move_origin = [ 0, 0 ];
			this.move_position = [ 0, 0 ];
			this.move_size = [ 0, 0 ];
			this.double_click_ignore = false;
		};



		Annotation.prototype = {
			constructor: Annotation,

			set_annotation_rect: function (x, y, width, height) {
				this.position[0] = x;
				this.position[1] = y;
				this.size[0] = width;
				this.size[1] = height;

				this.update_annotation_rect();
			},
			update_annotation_rect: function () {
				var n1s = this.nodes.annotation.container.style,
					u = Annotation.scaling_mode_units;

				n1s.left = this.position[0] + u;
				n1s.top = this.position[1] + u;
				n1s.width = this.size[0] + u;
				n1s.height = this.size[1] + u;
			},
			auto_size: function () {
				var a_rect = style.get_object_rect_relative(this.nodes.annotation.container),
					scale = this.parent.get_image_scale(),
					image_size = this.parent.get_image_size(),
					rect, n, x, y, w, h;

				// Size
				n = this.nodes.annotation.text;
				n.style.display = "inline-block";
				n.style.width = "auto";
				rect = style.get_object_rect_relative(n);
				n.style.display = "";
				n.style.width = "";

				// Update
				x = Math.round(this.position[0] + (rect.left - a_rect.left) / scale);
				y = Math.round(this.position[1] + (rect.top - a_rect.top) / scale);
				w = Math.ceil(rect.width / scale);
				h = Math.ceil(rect.height / scale);

				// Limits
				if (x + w > image_size.width) {
					x = image_size.width - w;
				}
				if (y + h > image_size.height) {
					y = image_size.height - h;
				}

				if (x < 0) {
					x = 0;
					if (w > image_size.width) {
						w = image_size.width;
					}
				}
				if (y < 0) {
					y = 0;
					if (h > image_size.height) {
						h = image_size.height;
					}
				}

				// Set
				this.set_annotation_rect(x, y, w, h);
				this.trigger("rect_change");
			},

			set_index: function (index) {
				this.index = index;
				this.nodes.edit.index_text.textContent = "#" + (index + 1);
			},
			set_text_before: function (text) {
				this.text_before = text;
			},
			set_text: function (text, dont_set_input) {
				// Parse
				var tf = new TextFormatter();
				tf.parse(text);

				// Update values
				this.text = tf.raw_text;
				this.text_display = tf.text;
				this.text_formatters = tf.formatters;

				// Update input
				if (!dont_set_input && this.nodes.edit !== null) {
					this.nodes.edit.text.value = this.text;
				}

				// Apply
				this.apply_text();
			},
			set_font: function (index) {
				this.font_index = index;

				var f = Annotation.fonts[index],
					fn = f.css_name + "," + Annotation.fonts[0].css_name;

				if (f.external_name !== null) {
					// Load font
					style.add_font(f.css_name, f.external_name, f.external_settings);
				}

				this.nodes.annotation.content.style.fontFamily = fn;

				if (this.nodes.edit !== null) {
					this.nodes.edit.button_font.style.fontFamily = fn;
				}
			},
			set_font_size: function (index) {
				this.font_size_index = index;
				this.font_size = Annotation.font_size_index_to_size(this.font_size_index);

				this.nodes.annotation.content.style.fontSize = this.font_size + Annotation.scaling_mode_units;

				if (this.nodes.edit !== null) {
					this.nodes.edit.button_font_size_text.textContent = this.font_size;
				}
			},
			set_font_bold: function (bold) {
				this.font_bold = bold;

				if (bold) {
					style.add_class(this.nodes.annotation.text, "iex_annotation_content_bold");

					if (this.nodes.edit !== null) {
						this.nodes.edit.button_font.style.fontWeight = "bold";
					}
				}
				else {
					style.remove_class(this.nodes.annotation.text, "iex_annotation_content_bold");

					if (this.nodes.edit !== null) {
						this.nodes.edit.button_font.style.fontWeight = "";
					}
				}
			},
			set_font_italic: function (italic) {
				this.font_italic = italic;

				if (italic) {
					style.add_class(this.nodes.annotation.text, "iex_annotation_content_italic");

					if (this.nodes.edit !== null) {
						this.nodes.edit.button_font.style.fontStyle = "italic";
					}
				}
				else {
					style.remove_class(this.nodes.annotation.text, "iex_annotation_content_italic");

					if (this.nodes.edit !== null) {
						this.nodes.edit.button_font.style.fontStyle = "";
					}
				}
			},
			set_text_line_spacing: function (spacing) {
				this.text_line_spacing = spacing;

				this.nodes.annotation.content.style.lineHeight = (1 + Annotation.line_spacing.from_index(spacing)) + "em";
			},
			set_text_char_spacing: function (spacing) {
				this.text_char_spacing = spacing;

				this.nodes.annotation.content.style.letterSpacing = Annotation.char_spacing.from_index(spacing) + "em";
			},
			set_text_decoration: function (decoration) {
				this.text_decoration = decoration;

				this.nodes.annotation.text.style.textShadow = this.decoration_to_shadow_style(decoration, "#" + Annotation.colors[this.colors[1]]);

				if (this.text_formatters.length > 0) {
					this.apply_text(); // in case any formatting contains a decoration color modification
				}
			},
			set_align: function (align) {
				var classes_old = this.align_to_classes(this.text_align),
					temp = this.align_to_classes_and_strings(align),
					classes_new = temp[0],
					align_strs = temp[1],
					cl = this.nodes.annotation.container.classList,
					i;

				this.text_align = align;

				for (i = 0; i < classes_old.length; ++i) {
					cl.remove(classes_old[i]);
				}
				for (i = 0; i < classes_new.length; ++i) {
					cl.add(classes_new[i]);
				}

				if (this.nodes.edit !== null) {
					this.nodes.edit.button_align.className = "iex_ae_annotation_button iex_ae_annotation_button_align iex_ae_annotation_button_align_" + align_strs[0] + " iex_ae_annotation_button_valign_" + align_strs[1] + style.theme;
				}
			},
			set_color: function (index, color) {
				this.colors[index] = color;

				var c = "#" + Annotation.colors[color];

				if (this.nodes.edit !== null && index === this.color_index_selected) {
					this.nodes.edit.button_color_bg.style.backgroundColor = c;
				}

				if (index === 0) {
					this.nodes.annotation.text.style.color = c;
					this.nodes.annotation.border1.style.borderColor = c;
				}
				else if (index === 1) {
					if (this.text_decoration !== Annotation.TextDecorationNone) {
						this.set_text_decoration(this.text_decoration);
					}
					else if (this.text_formatters.length > 0) {
						this.apply_text(); // in case any formatting contains a decoration color modification
					}
				}
				else { // if (index === 2) {
					this.nodes.annotation.background.style.backgroundColor = c;
				}
			},
			set_color_index_selected: function (index) {
				this.color_index_selected = index;

				if (this.nodes.edit !== null) {
					this.nodes.edit.button_color_text.textContent =
						(index === 1 ? "alt" :
						(index === 2 ? "bg" :
							"txt"
						));


					this.nodes.edit.button_color_bg.style.backgroundColor = "#" + Annotation.colors[this.colors[this.color_index_selected]];
				}
			},
			set_selected: function (selected, dont_select_text) {
				if (selected === this.selected) return;

				this.selected = selected;

				if (selected) {
					style.add_class(this.nodes.annotation.container, "iex_annotation_selected");

					if (this.nodes.edit !== null) {
						style.add_class(this.nodes.edit.container, "iex_ae_annotation_editor_selected");
						if (!dont_select_text) {
							this.select_text(null, false);
						}
					}
				}
				else {
					style.remove_class(this.nodes.annotation.container, "iex_annotation_selected");

					if (this.nodes.edit !== null) {
						style.remove_class(this.nodes.edit.container, "iex_ae_annotation_editor_selected");
					}
				}
			},

			get_index: function () {
				return this.index;
			},
			get_text: function () {
				return this.text;
			},
			get_text_display: function () {
				return this.text_display;
			},
			get_text_formatters: function () {
				return this.text_formatters;
			},
			get_text_before: function () {
				return this.text_before;
			},
			get_font: function () {
				return this.font_index;
			},
			get_font_size: function () {
				return this.font_size_index;
			},
			get_font_bold: function () {
				return this.font_bold;
			},
			get_font_italic: function () {
				return this.font_italic;
			},
			get_align: function () {
				return this.text_align;
			},
			get_text_decoration: function () {
				return this.text_decoration;
			},
			get_text_line_spacing: function () {
				return this.text_line_spacing;
			},
			get_text_char_spacing: function () {
				return this.text_char_spacing;
			},
			get_color: function (index) {
				return this.colors[index];
			},
			get_color_count: function () {
				return this.colors.length;
			},
			get_color_index_selected: function () {
				return this.color_index_selected;
			},
			get_position: function () {
				return this.position;
			},
			get_size: function () {
				return this.size;
			},
			is_selected: function () {
				return this.selected;
			},

			get_formatter_text: function (text, formatters) {
				return TextFormatter.get_raw(text, formatters);
			},
			escape_text: function (text) {
				return text.trim().replace(/\\/g, "\\\\");
			},
			load_from_state: function (state, text) {
				this.set_annotation_rect(state.rect.x, state.rect.y, state.rect.width, state.rect.height);

				this.set_font(state.font);
				this.set_font_bold(state.bold);
				this.set_font_italic(state.italic);

				this.set_font_size(state.font_size);

				this.set_align(state.align);

				this.set_color(0, state.colors[0]);
				this.set_color(1, state.colors[1]);
				this.set_color(2, state.colors[2]);
				this.set_text_decoration(state.text_decoration);

				this.set_text_line_spacing(state.line_spacing);
				this.set_text_char_spacing(state.char_spacing);

				// Update text
				if (state.text !== null) text = state.text;
				//this.set_text(text);
				this.text_display = text;
				this.text_formatters = state.formatters;
				if (this.nodes.edit !== null) {
					this.text = this.get_formatter_text(this.text_display, this.text_formatters);
					this.nodes.edit.text.value = this.text;
				}
				this.apply_text();
			},

			align_to_classes: function (align) {
				var classes = [],
					s, a;

				a = (align & Annotation.AlignHorizontal);
				if (a === Annotation.AlignJustify) {
					s = "justify";
				}
				else if (a === Annotation.AlignLeft) {
					s = "left";
				}
				else if (a === Annotation.AlignRight) {
					s = "right";
				}
				else { // if (a === Annotation.AlignCenter) {
					s = "center";
				}
				classes.push("iex_annotation_align_" + s);

				a = (align & Annotation.AlignVertical);
				if (a === Annotation.AlignTop) {
					s = "top";
				}
				else if (a === Annotation.AlignBottom) {
					s = "bottom";
				}
				else { // if (a === Annotation.AlignMiddle) {
					s = "middle";
				}
				classes.push("iex_annotation_valign_" + s);

				if ((align & Annotation.AlignHOverflow) !== 0) {
					classes.push("iex_annotation_align_x");
				}
				if ((align & Annotation.AlignVOverflow) !== 0) {
					classes.push("iex_annotation_valign_x");
				}

				return classes;
			},
			align_to_classes_and_strings: function (align) {
				var classes = [],
					strings = [],
					s, a;

				a = (align & Annotation.AlignHorizontal);
				if (a === Annotation.AlignJustify) {
					s = "justify";
				}
				else if (a === Annotation.AlignLeft) {
					s = "left";
				}
				else if (a === Annotation.AlignRight) {
					s = "right";
				}
				else { // if (a === Annotation.AlignCenter) {
					s = "center";
				}
				strings.push(s);
				classes.push("iex_annotation_align_" + s);

				a = (align & Annotation.AlignVertical);
				if (a === Annotation.AlignTop) {
					s = "top";
				}
				else if (a === Annotation.AlignBottom) {
					s = "bottom";
				}
				else { // if (a === Annotation.AlignMiddle) {
					s = "middle";
				}
				strings.push(s);
				classes.push("iex_annotation_valign_" + s);

				if ((align & Annotation.AlignHOverflow) !== 0) {
					classes.push("iex_annotation_align_x");
				}
				if ((align & Annotation.AlignVOverflow) !== 0) {
					classes.push("iex_annotation_valign_x");
				}

				return [ classes , strings ];
			},
			decoration_to_shadow_style: function (decoration, color) {
				var s = "";

				if (decoration === Annotation.TextDecorationShadow) {
					s = "0.0625em 0.0625em 0 " + color;
				}
				else if (decoration === Annotation.TextDecorationShadowBlur) {
					s = "0 0 0.0625em " + color;
					s += "," + s;
				}
				else if (decoration === Annotation.TextDecorationShadowBlurStrong) {
					s = "0 0 0.125em " + color;
					s += "," + s;
					s += "," + s;
				}

				return s;
			},
			apply_text: function () {
				var container = this.nodes.annotation.text,
					pos = 0,
					pos_last = -1,
					bold = false,
					italic = false,
					decoration = -1,
					colors = [ -1, -1, -1 ],
					self = this,
					i, j, f, s, type, i_max;

				var add_node = function (text) {
					if (text.length === 0) return;

					var n = $("span"),
						c = "",
						d;

					n.textContent = text;
					if (bold) {
						c = "iex_annotation_format_bold";
						if (italic) c += " iex_annotation_format_italic";
						n.className = c;
					}
					else if (italic) {
						n.className = "iex_annotation_format_italic";
					}
					if (colors[0] >= 0) {
						n.style.color = "#" + Annotation.colors[colors[0]];
					}
					if (colors[2] >= 0) {
						n.style.backgroundColor = "#" + Annotation.colors[colors[2]];
					}
					if (colors[1] >= 0) {
						if ((d = decoration) >= 0 || (d = self.text_decoration) > 0) {
							c = self.decoration_to_shadow_style(d, "#" + Annotation.colors[colors[1]]);
							if (c.length === 0) c = "none";
							n.style.textShadow = c;
						}
					}
					else if (decoration >= 0) {
						c = self.decoration_to_shadow_style(decoration, "#" + Annotation.colors[self.colors[1]]);
						if (c.length === 0) c = "none";
						n.style.textShadow = c;
					}

					container.appendChild(n);
				};

				// Clear
				container.textContent = "";

				// Add content
				i_max = this.text_formatters.length;
				for (i = 0; i < i_max; ++i) {
					f = this.text_formatters[i];
					type = f[0];

					s = this.text_display.substr(pos, f[1] - pos);
					pos = f[1];

					if (type === Annotation.TextFormattingBold) {
						add_node(s);
						bold = !bold;
					}
					else if (type === Annotation.TextFormattingItalic) {
						add_node( s);
						italic = !italic;
					}
					else if (type === Annotation.TextFormattingNewline) {
						add_node(s);
						container.appendChild($("br"));
						++pos;
						pos_last = pos;
					}
					else if (type === Annotation.TextFormattingHyphen) {
						s += "-";
						add_node(s);
						container.appendChild($("br"));
					}
					else if (type === Annotation.TextFormattingColor) {
						add_node(s);
						j = f[2];
						if (j >= colors.length) {
							for (j = 0; j < colors.length; ++j) colors[j] = -1;
						}
						else {
							colors[j] = f[3];
						}
					}
					else if (type === Annotation.TextFormattingDecoration) {
						add_node(s);
						decoration = f[2];
					}
					else if (type === Annotation.TextFormattingDecorationReset) {
						add_node(s);
						decoration = -1;
					}
					else { // if (type === Annotation.TextFormattingReset) {
						add_node(s);
						for (j = 0; j < colors.length; ++j) colors[j] = -1;
						bold = false;
						italic = false;
						decoration = -1;
					}
				}

				// Last
				s = this.text_display.substr(pos);
				add_node(s);
			},

			destroy: function () {
				this.remove_nodes();

				remove_event_listeners(this.events);
				this.events = [];
			},
			remove_nodes: function () {
				var n;

				// Deselect
				if (this.is_selected()) this.set_selected(false);

				// Stop events
				if (this.edit_data !== null && this.edit_data.move_events.length > 0) {
					this.on_annotation_move_document_mouseup(null);
				}

				// Remove nodes
				if ((n = this.nodes.annotation.container).parentNode !== null) {
					n.parentNode.removeChild(n);
				}
				if (this.nodes.edit !== null) {
					if ((n = this.nodes.edit.container).parentNode !== null) {
						n.parentNode.removeChild(n);
					}
				}
			},
			insert_before: function (annotation) {
				var n = this.nodes.annotation.container,
					n_o = annotation.nodes.annotation.container;

				n_o.parentNode.insertBefore(n, n_o);

				if (this.nodes.edit !== null && annotation.nodes.edit !== null) {
					n = this.nodes.edit.container;
					n_o = annotation.nodes.edit.container;
					n_o.parentNode.insertBefore(n, n_o);
				}
			},
			add_to: function (annotation_container, edit_container) {
				var n = this.nodes.annotation.container;
				annotation_container.appendChild(n);

				if (this.nodes.edit !== null && edit_container !== undefined) {
					n = this.nodes.edit.container;
					edit_container.appendChild(n);
				}
			},

			create_nodes: function (edit) {
				// Edit
				if (edit) {
					this.nodes.edit = {};
					this.create_nodes_edit();
				}

				// Annotation
				this.nodes.annotation = {};
				this.create_nodes_annotation(edit);


				// Setup
				this.update_annotation_rect();
				this.set_align(this.text_align);
				this.set_color(0, this.colors[0]);
				this.set_color(1, this.colors[1]);
				this.set_color(2, this.colors[2]);
				this.set_font(this.font_index);
				this.set_font_size(this.font_size_index);
				this.set_text_line_spacing(this.text_line_spacing);
				this.set_text_char_spacing(this.text_char_spacing);
			},
			create_nodes_annotation: function (edit) {
				var n1, n2, n3, n4, borders, i;

				n1 = $.div("iex_annotation");
				this.nodes.annotation.container = n1;
				if (edit) {
					style.add_class(n1, "iex_annotation_editing");
					add_event_listener(this.events, n1, "mousedown", this.on_annotation_mousedown.bind(this), false);
					add_event_listener(this.events, n1, "mouseover", wrap_mouseenterleave_event(this, this.on_annotation_mouseover), false);
					add_event_listener(this.events, n1, "mouseout", wrap_mouseenterleave_event(this, this.on_annotation_mouseout), false);
				}
				else {
					add_event_listener(this.events, n1, "mousedown", this.on_annotation_mousedown_no_edit.bind(this), false);
				}

				if (Annotation.scaling_mode_px) {
					style.add_class(n1, "iex_annotation_scaling_px");
				}
				if (settings.values.annotations.transparent_until_hover) {
					style.add_class(n1, "iex_annotation_transparent");
				}

				n2 = $.div("iex_annotation_borders");
				n1.appendChild(n2);

				if (edit) {
					borders = [ "top", "bottom", "left", "right", "top_left", "top_right", "bottom_left", "bottom_right" ];
					for (i = 0; i < borders.length; ++i) {
						n3 = $.div("iex_annotation_border iex_annotation_border_" + borders[i]);
						n2.appendChild(n3);
						add_event_listener(this.events, n3, "mousedown", this.on_annotation_resize_mousedown.bind(this, i), false);
					}
				}

				n2 = $.div("iex_annotation_background");
				n1.appendChild(n2);
				this.nodes.annotation.background = n2;
				n2.style.borderColor = "#" + Annotation.colors[this.colors[2]];


				n2 = $.div("iex_annotation_outline");
				n1.appendChild(n2);
				this.nodes.annotation.border1 = n2;
				n2.style.borderColor = "#" + Annotation.colors[this.colors[0]];


				n2 = $.div("iex_annotation_content");
				n1.appendChild(n2);
				if (edit) {
					add_event_listener(this.events, n2, "mousedown", this.on_annotation_move_mousedown.bind(this), false);
					add_event_listener(this.events, n2, "contextmenu", this.on_annotation_contextmenu.bind(this), false);
					add_event_listener(this.events, n2, "dblclick", this.on_annotation_dblclick.bind(this), false);
				}
				this.nodes.annotation.content = n2;

				n3 = $.div("iex_annotation_content2");
				n2.appendChild(n3);

				n4 = $.div("iex_annotation_content3");
				n3.appendChild(n4);
				this.nodes.annotation.text = n4;
			},

			on_annotation_mousedown_no_edit: function (event) {
				//event.preventDefault();
			},
			create_nodes_edit: function () {
				var n1, n2, n3, n4, n5;

				n1 = $.div("iex_ae_annotation_editor");
				this.nodes.edit.container = n1;
				add_event_listener(this.events, n1, "mousedown", this.on_edit_region_mousedown.bind(this), false);


				// Info bar
				n2 = $.div("iex_ae_annotation_editor_top");
				n1.appendChild(n2);

				n3 = $.div("iex_ae_annotation_editor_top_row");
				n2.appendChild(n3);

				n4 = $.div("iex_ae_annotation_editor_top_cell");
				n3.appendChild(n4);

				n5 = $.div("iex_ae_annotation_editor_number");
				n4.appendChild(n5);
				n5.textContent = "#" + (this.index + 1);
				this.nodes.edit.index_text = n5;


				n4 = $.div("iex_ae_annotation_editor_top_cell");
				n3.appendChild(n4);

				n5 = this.create_button_alignment();
				n4.appendChild(n5);
				add_event_listener(this.events, n5, "click", this.on_edit_align_click.bind(this), false);
				add_event_listener(this.events, n5, "mousedown", this.on_edit_button_mousedown.bind(this), false);
				this.nodes.edit.button_align = n5;

				n5 = this.create_button_color();
				n4.appendChild(n5);
				add_event_listener(this.events, n5, "click", this.on_edit_color_click.bind(this), false);
				add_event_listener(this.events, n5, "mousedown", this.on_edit_button_mousedown.bind(this), false);
				this.nodes.edit.button_color = n5;

				n5 = this.create_button("A");
				n4.appendChild(n5);
				add_event_listener(this.events, n5, "click", this.on_edit_font_click.bind(this), false);
				add_event_listener(this.events, n5, "mousedown", this.on_edit_button_mousedown.bind(this), false);
				n5.setAttribute("title", "Font");
				this.nodes.edit.button_font = n5;

				n5 = this.create_button("", "iex_ae_annotation_button_small_text");
				n4.appendChild(n5);
				add_event_listener(this.events, n5, "click", this.on_edit_font_size_click.bind(this), false);
				add_event_listener(this.events, n5, "mousedown", this.on_edit_button_mousedown.bind(this), false);
				n5.setAttribute("title", "Font size");
				this.nodes.edit.button_font_size = n5;
				this.nodes.edit.button_font_size_text = n5.firstChild;

				n5 = this.create_button("\u2191");
				n4.appendChild(n5);
				add_event_listener(this.events, n5, "click", this.on_edit_move_up_click.bind(this), false);
				add_event_listener(this.events, n5, "mousedown", this.on_edit_button_mousedown.bind(this), false);
				n5.setAttribute("title", "Move up");
				this.nodes.edit.button_move_up = n5;

				n5 = this.create_button("\u2193");
				n4.appendChild(n5);
				add_event_listener(this.events, n5, "click", this.on_edit_move_down_click.bind(this), false);
				add_event_listener(this.events, n5, "mousedown", this.on_edit_button_mousedown.bind(this), false);
				n5.setAttribute("title", "Move down");
				this.nodes.edit.button_move_down = n5;

				n5 = this.create_button("\u00D7");
				n4.appendChild(n5);
				add_event_listener(this.events, n5, "click", this.on_edit_remove_click.bind(this), false);
				add_event_listener(this.events, n5, "mousedown", this.on_edit_button_mousedown.bind(this), false);
				n5.setAttribute("title", "Remove");
				this.nodes.edit.button_remove = n5;


				// Input
				n2 = $.div("iex_ae_annotation_editor_bottom");
				n1.appendChild(n2);

				n3 = $.input.text("iex_ae_annotation_editor_input_text");
				n2.appendChild(n3);
				n3.setAttribute("placeholder", "annotation");
				n3.style.width = "100%";
				n3.style.boxSizing = "border-box";
				add_event_listener(this.events, n3, "input", this.on_edit_text_input.bind(this), false);
				add_event_listener(this.events, n3, "change", this.on_edit_text_change.bind(this), false);
				add_event_listener(this.events, n3, "focus", this.on_edit_text_focus.bind(this), false);
				this.nodes.edit.text = n3;
			},
			create_button: function (text, extra_class) {
				var n1, n2, cls;

				cls = "iex_ae_annotation_button";
				if (extra_class) {
					cls += " ";
					cls += extra_class;
				}

				n1 = $.span(cls);
				n2 = $.span("iex_ae_annotation_button_inner");
				n1.appendChild(n2);
				n2.textContent = text;

				return n1;
			},
			create_button_color: function () {
				var n1, n2, n3;

				n1 = $.span("iex_ae_annotation_button iex_ae_annotation_button_small_text");
				n2 = $.span("iex_ae_annotation_button_inner iex_ae_annotation_button_inner_color");
				n1.appendChild(n2);
				n3 = $.span("iex_ae_annotation_button_color");
				n2.appendChild(n3);

				n2 = $.span("iex_ae_annotation_button_inner iex_ae_annotation_button_color_text");
				n2.textContent = "txt";
				n1.appendChild(n2);

				this.nodes.edit.button_color_bg = n3;
				this.nodes.edit.button_color_text = n2;
				n1.setAttribute("title", "Color");

				return n1;
			},
			create_button_alignment: function () {
				var n1, n2;

				n1 = $.span("iex_ae_annotation_button iex_ae_annotation_button_align");
				n2 = $.span("iex_ae_annotation_button_inner");
				n1.appendChild(n2);
				n1.setAttribute("title", "Alignment");

				return n1;
			},

			initialize_move: function (mode, event) {
				this.edit_data.move_mode = mode;
				this.edit_data.move_origin[0] = event.clientX;
				this.edit_data.move_origin[1] = event.clientY;
				this.edit_data.move_position[0] = this.position[0];
				this.edit_data.move_position[1] = this.position[1];
				this.edit_data.move_size[0] = this.size[0];
				this.edit_data.move_size[1] = this.size[1];

				remove_event_listeners(this.edit_data.move_events);
				this.edit_data.move_events = [];
				add_event_listener(this.edit_data.move_events, document, "mousemove", this.on_annotation_move_document_mousemove.bind(this), true);
				add_event_listener(this.edit_data.move_events, document, "mouseup", this.on_annotation_move_document_mouseup.bind(this), true);
			},

			scroll_to_editor: function () {
				var scroll_region = this.nodes.edit.container.parentNode,
					scroll_container = scroll_region.parentNode,
					check = this.nodes.edit.text,
					bound = this.nodes.edit.container,
					r1 = style.get_object_rect_relative(check),
					r2 = style.get_object_rect_relative(scroll_container);

				if (r1.top < r2.top) {
					r1 = style.get_object_rect_relative(bound);
					r2 = style.get_object_rect_relative(scroll_region);
					scroll_container.scrollTop = r1.top - r2.top;
				}
				else if (r1.bottom > r2.bottom) {
					r1 = style.get_object_rect_relative(bound);
					scroll_container.scrollTop = r1.bottom - style.get_object_rect_relative(scroll_region).top - r2.height;
				}
			},
			select_text: function (target, event) {
				this.scroll_to_editor();

				var ret = true,
					active_change = (document.activeElement !== this.nodes.edit.text);

				if (target !== this.nodes.edit.text) {
					if (active_change) {
						select_input(this.nodes.edit.text, this.nodes.edit.text.value.length || 0);
					}
					ret = false;
				}

				if (event && !this.is_selected()) {
					this.trigger("select");
				}
				return ret;
			},

			on_edit_button_mousedown: function (event) {
				return stop_event(event);
			},
			on_edit_align_click: function (event) {
				this.trigger("align");
			},
			on_edit_color_click: function (event) {
				this.trigger("color");
			},
			on_edit_font_click: function (event) {
				this.trigger("font");
			},
			on_edit_font_size_click: function (event) {
				this.trigger("font_size");
			},
			on_edit_move_up_click: function (event) {
				this.trigger("move_up");
			},
			on_edit_move_down_click: function (event) {
				this.trigger("move_down");
			},
			on_edit_remove_click: function (event) {
				this.trigger("remove");
			},
			on_edit_region_mousedown: function (event) {
				if (this.select_text(event.target, true)) return true;
				return true;//stop_event(event);
			},
			on_edit_text_input: function (event) {
				this.set_text(this.nodes.edit.text.value, true);
				this.trigger("text_input");
			},
			on_edit_text_change: function (event) {
				this.set_text(this.nodes.edit.text.value, false);
				this.trigger("text_change");
			},
			on_edit_text_focus: function (event) {
				if (!this.is_selected()) {
					this.trigger("select");
				}
			},

			on_annotation_mousedown: function (event) {
				this.edit_data.double_click_ignore = false;

				if (get_event_mouse_button(event) === 2) return;

				event.stopPropagation();
			},
			on_annotation_mouseover: function (event) {
			},
			on_annotation_mouseout: function (event) {
			},
			on_annotation_contextmenu: function (event) {
				event.stopPropagation();
			},
			on_annotation_resize_mousedown: function (index, event) {
				if (get_event_mouse_button(event) !== 1) return;

				var resize_dir;
				if (index < 4) {
					resize_dir = (1 << index);
				}
				else {
					resize_dir = (index < 6) ? Annotation.ResizeNorth : Annotation.ResizeSouth;
					resize_dir |= ((index % 2) === 0) ? Annotation.ResizeWest : Annotation.ResizeEast;
				}

				// Move
				this.initialize_move(resize_dir, event);
				return stop_event(event);
			},
			on_annotation_move_mousedown: function (event) {
				this.edit_data.double_click_ignore = false;

				var button = get_event_mouse_button(event);
				if (button === 2) return;
				if (button !== 1) return stop_event(event);

				// Move
				this.initialize_move(0, event);
				if (this.select_text(null, true)) return true;
				return stop_event(event);
			},
			on_annotation_dblclick: function (event) {
				if (!this.edit_data.double_click_ignore) {
					this.auto_size();
				}
				this.edit_data.double_click_ignore = false;

				return stop_event(event);
			},

			on_annotation_move_document_mousemove: function (event) {
				this.edit_data.double_click_ignore = true;

				var scale = this.parent.get_image_scale(),
					image_size = this.parent.get_image_size(),
					xo = (event.clientX - this.edit_data.move_origin[0]) / scale,
					yo = (event.clientY - this.edit_data.move_origin[1]) / scale,
					x = this.edit_data.move_position[0],
					y = this.edit_data.move_position[1],
					w = this.edit_data.move_size[0],
					h = this.edit_data.move_size[1];

				if (this.edit_data.move_mode === 0) {
					// Move
					x = Math.round(x + xo);
					y = Math.round(y + yo);

					// Limit
					if (x + w > image_size.width) {
						x = image_size.width - w;
					}
					if (y + h > image_size.height) {
						y = image_size.height - h;
					}

					if (x < 0) x = 0;
					if (y < 0) y = 0;
				}
				else {
					if ((this.edit_data.move_mode & Annotation.ResizeWest) !== 0) {
						// Change x and width
						x = Math.round(x + xo);
						w = Math.round(w + (this.edit_data.move_position[0] - x));
						if (x < 0) {
							w += x;
							x = 0;
						}
						if (w < 0) w = 0;
					}
					else if ((this.edit_data.move_mode & Annotation.ResizeEast) !== 0) {
						// Change width
						w = Math.round(w + xo);
						if (x + w > image_size.width) {
							w = image_size.width - x;
						}
						if (w < 0) w = 0;
					}

					if ((this.edit_data.move_mode & Annotation.ResizeNorth) !== 0) {
						// Change y and height
						y = Math.round(y + yo);
						h = Math.round(h + (this.edit_data.move_position[1] - y));
						if (y < 0) {
							h += y;
							y = 0;
						}
						if (h < 0) h = 0;
					}
					else if ((this.edit_data.move_mode & Annotation.ResizeSouth) !== 0) {
						// Change height
						h = Math.round(h + yo);
						if (y + h > image_size.height) {
							h = image_size.height - y;
						}
						if (h < 0) h = 0;
					}
				}

				// Update
				this.set_annotation_rect(x, y, w, h);
				this.trigger("rect_change");
			},
			on_annotation_move_document_mouseup: function (event) {
				remove_event_listeners(this.edit_data.move_events);
				this.edit_data.move_events = [];
				this.trigger("rect_changed");
			},

			trigger: function (event_name, data) {
				this.parent.on_annotation_event(this, event_name, data);
			},
		};



		Annotation.ResizeNorth = 0x1;
		Annotation.ResizeSouth = 0x2;
		Annotation.ResizeWest = 0x4;
		Annotation.ResizeEast = 0x8;

		Annotation.AlignCenter = 0x0;
		Annotation.AlignLeft = 0x1;
		Annotation.AlignRight = 0x2;
		Annotation.AlignJustify = 0x3;
		Annotation.AlignMiddle = 0x0;
		Annotation.AlignTop = 0x4;
		Annotation.AlignBottom = 0x8;
		Annotation.AlignHOverflow = 0x10;
		Annotation.AlignVOverflow = 0x20;
		Annotation.AlignHorizontal = 0x3;
		Annotation.AlignVertical = 0xC;
		Annotation.AlignDefault = Annotation.AlignCenter | Annotation.AlignMiddle | Annotation.AlignHOverflow | Annotation.AlignVOverflow;

		Annotation.TextDecorationNone = 0x0;
		Annotation.TextDecorationShadow = 0x1;
		Annotation.TextDecorationShadowBlur = 0x2;
		Annotation.TextDecorationShadowBlurStrong = 0x3;
		Annotation.TextDecorationCount = 0x4;

		Annotation.TextFormattingBold = 0x0;
		Annotation.TextFormattingItalic = 0x1;
		Annotation.TextFormattingNewline = 0x2;
		Annotation.TextFormattingHyphen = 0x3;
		Annotation.TextFormattingColor = 0x4;
		Annotation.TextFormattingDecoration = 0x5;
		Annotation.TextFormattingDecorationReset = 0x6;
		Annotation.TextFormattingReset = 0x7;

		Annotation.TextCharSpacingRange = {
			min: -0.5,
			range: 1.5,
			steps: 15,
		};
		Annotation.TextLineSpacingRange = {
			min: 0.0,
			range: 1.5,
			steps: 15,
		};



		Annotation.scaling_mode_px = true;
		Annotation.scaling_mode_units = (Annotation.scaling_mode_px ? "em" : "px");

		Annotation.colors = [ //{
			// dark,  normal,   light
			"000000",           "ffffff", // black and white
			"333333", "808080", "cccccc", // gray
			"5a0707", "e11212", "f3a0a0", // red
			"5a3807", "e18c12", "f3d1a0", // orange
			"544e07", "d4c412", "ede7a0", // yellow
			"2a560c", "6ad91f", "c3efa5", // green
			"0a5648", "19d7b6", "a3efe1", // teal
			"0a4356", "19a8d7", "a3dcef", // lt blue
			"141157", "342cdb", "adaaf0", // blue
			"340a56", "8419d7", "cda3ef", // purple
			"560a43", "d719a8", "efa3dc", // fuchsia
		]; //}
		Annotation.color_shades = 3;

		Annotation.fonts = [{
			name: "Arial",
			css_name: "arial",
			external_name: null,
			external_settings: null,
		}, {
			name: "Wild Words",
			css_name: "iex_wildwords",
			external_name: "ww",
			external_settings: [ "r", "b", "bi" ],
		}];

		Annotation.font_size_settings = {
			base: 8,
			base_step: 1,
			step_duration: 8,
			step_count: 5,
			step_multiplier: 2,
		};

		Annotation.font_size_index_to_size = function (index) {
			var fss = Annotation.font_size_settings;

			if (index < 0) {
				index = 0;
			}
			else if (index >= fss.step_duration * fss.step_count) {
				index = fss.step_duration * fss.step_count - 1;
			}

			var step = Math.floor(index / fss.step_duration),
				step_index = index % fss.step_duration,
				size = fss.base,
				step_size = fss.base_step,
				i;

			for (i = 0; i < step; ++i) {
				size += step_size * fss.step_duration;
				step_size *= fss.step_multiplier;
			}

			size += step_index * step_size;

			return size;
		};
		Annotation.font_size_to_size_info = function (size) {
			var fss = Annotation.font_size_settings,
				base = fss.base,
				step = fss.base_step,
				base_next,
				i, off;

			// Min
			if (size < base) return [ 0 , base ]; // [ index, size ]

			for (i = 0; i < fss.step_count; ++i) {
				base_next = base + step * fss.step_duration;

				if (size < base_next) {
					off = (fss.step_duration - Math.round((base_next - size) / step));
					return [
						i * fss.step_duration + off,
						base + off * step
					];
				}

				step *= fss.step_multiplier;
				base = base_next;
			}

			// Max
			return [ i * fss.step_duration - 1, base - step / fss.step_multiplier ];
		};

		Annotation.char_spacing = {
			from_index: function (index) {
				var index_limit = Annotation.TextCharSpacingRange.steps;

				if (index < 0) {
					index = 0;
				}
				else if (index > index_limit) {
					index = index_limit;
				}

				return Annotation.TextCharSpacingRange.min + Annotation.TextCharSpacingRange.range * (index / index_limit);
			},
			to_index: function (spacing) {
				if (spacing <= Annotation.TextCharSpacingRange.min) return 0;
				if (spacing >= Annotation.TextCharSpacingRange.min + Annotation.TextCharSpacingRange.range) return Annotation.TextCharSpacingRange.steps;

				return Math.round((spacing - Annotation.TextCharSpacingRange.min) / Annotation.TextCharSpacingRange.range * Annotation.TextCharSpacingRange.steps);
			},
		};
		Annotation.line_spacing = {
			from_index: function (index) {
				var index_limit = Annotation.TextLineSpacingRange.steps;

				if (index < 0) {
					index = 0;
				}
				else if (index > index_limit) {
					index = index_limit;
				}

				return Annotation.TextLineSpacingRange.min + Annotation.TextLineSpacingRange.range * (index / index_limit);
			},
			to_index: function (spacing) {
				if (spacing <= Annotation.TextLineSpacingRange.min) return 0;
				if (spacing >= Annotation.TextLineSpacingRange.min + Annotation.TextLineSpacingRange.range) return Annotation.TextLineSpacingRange.steps;

				return Math.round((spacing - Annotation.TextLineSpacingRange.min) / Annotation.TextLineSpacingRange.range * Annotation.TextLineSpacingRange.steps);
			},
		};



		var TextFormatter = function () {
			this.code = "";
			this.before = "";
			this.after = "";

			this.no_spaces = false;
			this.force_update = false;

			this.raw_text = "";
			this.text = "";
			this.formatters = [];

			this.match = null;
			this.match_length = 0;
		};
		TextFormatter.bind = function (fn, args) {
			return function () {
				return fn.apply(this, args);
			};
		};
		TextFormatter.code_simple = function (formatting_code, extra) {
			if (this.before.length > 0 || (this.force_update = (this.after.length > 0))) {
				this.before = " ";
				this.after = "";
				this.text += " ";
			}

			var f = [ formatting_code, this.text.length ];
			if (extra !== undefined) Array.prototype.push.apply(f, extra);
			this.formatters.push(f);

			return true;
		};
		TextFormatter.codes = {
			"\\": function () {
				this.no_spaces = false;

				this.text += this.before;
				this.text += this.code;
				this.text += this.after;

				return true;
			},
			"h": function () {
				this.before = "";
				this.after = "";
				this.formatters.push([ Annotation.TextFormattingHyphen, this.text.length ]);

				return true;
			},
			"n": function () {
				this.before = "";
				this.after = "";
				if (this.text.length > 0 && /^\s/.test(this.text[this.text.length - 1])) {
					// Sequential newlines
					this.text = this.text.substr(0, this.text.length - 1);
				}
				this.formatters.push([ Annotation.TextFormattingNewline, this.text.length ]);
				this.text += " ";

				return true;
			},
			"c": function () {
				var c = 0,
					i, m, p;

				// Invalid check
				if (
					this.after.length > 0 ||
					(m = TextFormatter.re_color.exec(this.raw_text.substr(this.match.index + this.match_length))) === null
				) {
					// Invalid
					return false;
				}

				// Move next
				this.match_length += m[0].length;
				p = this.match.index + this.match_length;

				// Whitespace
				if (this.before.length > 0) this.text += " ";

				// Color index
				if (m[1] !== undefined) {
					c = Math.min(parseInt(m[1], 10), 2);
					if (c > 0) {
						this.code += c;
						this.code += ".";
					}
				}

				// Color
				i = Math.min(parseInt(m[2], 10), Annotation.colors.length - 1);
				this.code += i;
				if (p < this.raw_text.length && TextFormatter.re_number.test(this.raw_text[p])) this.code += ":";

				// Formatting
				this.formatters.push([ Annotation.TextFormattingColor, this.text.length, c, i ]);
				this.force_update = true;
				return true;
			},
			"d": function () {
				var i, m;

				// Invalid check
				if (
					this.after.length > 0 ||
					(m = TextFormatter.re_number.exec(this.raw_text.substr(this.match.index + this.match_length))) === null
				) {
					// Invalid
					return false;
				}

				// Move next
				this.match_length += m[0].length;

				// Whitespace
				if (this.before.length > 0) this.text += " ";

				// Decoration
				i = Math.min(parseInt(m[0], 10), Annotation.TextDecorationCount - 1);
				this.code += i;

				// Formatting
				this.formatters.push([ Annotation.TextFormattingDecoration, this.text.length, i ]);
				this.force_update = true;
				return true;
			},
			"b": TextFormatter.bind(TextFormatter.code_simple, [ Annotation.TextFormattingBold ]),
			"i": TextFormatter.bind(TextFormatter.code_simple, [ Annotation.TextFormattingItalic ]),
			"C": TextFormatter.bind(TextFormatter.code_simple, [ Annotation.TextFormattingColor, [ 3 ] ]),
			"D": TextFormatter.bind(TextFormatter.code_simple, [ Annotation.TextFormattingDecorationReset ]),
			"R": TextFormatter.bind(TextFormatter.code_simple, [ Annotation.TextFormattingReset ]),
		};
		TextFormatter.re_formatter = /(?:(\s*)\\([\w\W])(\s*))/g;
		TextFormatter.re_color = /^(?:([0-9])\.)?([0-9]+):?/;
		TextFormatter.re_number = /^[0-9]/;
		TextFormatter.re_space = /^\s/;
		TextFormatter.MaxTextFormatters = 127; // (2^(2^3 - 1) - 1) where 3 is TagWriter.bits_per.formatter_count_bits

		TextFormatter.get_raw = function (text, formatters) {
			var raw_text = "",
				pos = 0,
				i, f, s, type;

			// Add content
			for (i = 0; i < formatters.length; ++i) {
				f = formatters[i];
				type = f[0];

				s = text.substr(pos, f[1] - pos);
				pos = f[1];

				raw_text += s.replace(/\\/g, "\\\\");
				if (type === Annotation.TextFormattingBold) {
					raw_text += "\\b";
				}
				else if (type === Annotation.TextFormattingItalic) {
					raw_text += "\\i";
				}
				else if (type === Annotation.TextFormattingNewline) {
					if (raw_text.length > 0 && TextFormatter.re_space.test(raw_text[raw_text.length - 1])) {
						raw_text = raw_text.substr(0, raw_text.length - 1); // multiple newlines
					}
					raw_text += "\\n";
					if (text.length > pos && TextFormatter.re_space.test(text[pos])) {
						++pos; // whitespace
					}
				}
				else if (type === Annotation.TextFormattingHyphen) {
					raw_text += "\\h";
				}
				else if (type === Annotation.TextFormattingColor) {
					if (f[2] >= 3) { // colors.length) {
						raw_text += "\\C";
					}
					else {
						raw_text += "\\c";
						if (f[2] > 0) {
							raw_text += f[2];
							raw_text += ".";
						}
						raw_text += f[3];
						if (text.length > pos && TextFormatter.re_number.test(text[pos])) {
							raw_text += ":"; // optional
						}
					}
				}
				else if (type === Annotation.TextFormattingDecoration) {
					raw_text += "\\d1";
				}
				else if (type === Annotation.TextFormattingDecorationReset) {
					raw_text += "\\D";
				}
				else { // if (type === Annotation.TextFormattingReset) {
					raw_text += "\\R";
				}
			}

			// Last
			raw_text += text.substr(pos).replace(/\\/g, "\\\\");
			return raw_text;
		};
		TextFormatter.normalize_text = function (text) {
			return text.trim().replace(/\t\s*| \s+/g, " ");
		};

		TextFormatter.prototype = {
			constructor: TextFormatter,

			parse: function (text) {
				var i = 0,
					fn, match_text;

				this.no_spaces = false;
				this.raw_text = TextFormatter.normalize_text(text);

				TextFormatter.re_formatter.lastIndex = 0;
				while ((this.match = TextFormatter.re_formatter.exec(this.raw_text)) !== null) {
					// Setup
					this.code = this.match[2]; // char
					this.before = this.match[1]; // before
					this.after = this.match[3]; // after

					this.text += this.raw_text.substr(i, this.match.index - i);

					this.match_length = this.match[0].length;


					// Process
					fn = TextFormatter.codes[this.code];
					if (fn === undefined || !fn.call(this)) {
						// Invalid
						this.no_spaces = false;
						this.text += this.match[0];
					}
					else {
						// Don't overflow
						if (this.formatters.length > TextFormatter.MaxTextFormatters) {
							this.formatters.splice(TextFormatter.MaxTextFormatters, this.formatters.length - TextFormatter.MaxTextFormatters);
						}
					}


					// Remove extra spaces between things like " \b \i \b \i " (turns into " \b\i\b\i")
					if (this.no_spaces && this.match.index === i) {
						this.before = "";
						this.after = "";
					}
					this.no_spaces = (this.match[1].length + this.match[3].length > 0);

					// Text update
					match_text = this.before + "\\" + this.code + this.after;
					if (this.force_update || match_text.length !== this.match_length) {
						this.raw_text = this.raw_text.substr(0, this.match.index) + match_text + this.raw_text.substr(this.match.index + this.match_length);
						this.force_update = false;
					}

					// Position update
					i = this.match.index + match_text.length;
					TextFormatter.re_formatter.lastIndex = i;
				}

				// Remainder
				if (i === 0) {
					this.text = this.raw_text;
				}
				else if (i < this.raw_text.length) {
					this.text += this.raw_text.substr(i);
				}
			},
		};

		Annotation.TextFormatter = TextFormatter;



		var State = function () {
			this.rect = {
				x: 0,
				y: 0,
				width: 0,
				height: 0
			};

			this.font = 0;
			this.bold = false;
			this.italic = false;

			this.font_size = 0;

			this.align = 0;

			this.colors = [ 0, 0, 0 ];
			this.text_decoration = 0;

			this.line_spacing = 0;
			this.char_spacing = 0;

			this.formatters = [];

			this.text = null;
		};

		Annotation.State = State;



		return Annotation;

	})();

	// Image annotation control
	var Annotator = (function () {

		// Annotator class
		var Annotator = function () {
			// Get posts
			this.post_queue = null;
			this.post_id_current = -1;
			this.annotation_nodes = {
				container: null,
				overlay: null,
			};
			this.annotation_data = {};
			this.annotations = [];
			this.annotations_active = false;
		};



		// Public functions
		Annotator.prototype = {
			constructor: Annotator,
			start: function (image_hover, file_link) {
				// Get posts
				this.post_queue = Delay.queue(api.get("posts"), on_post_queue_callback.bind(this), 5, 0.125);

				// Bind post acquiring
				api.on("post_add", on_api_post_add.bind(this));
				api.on("post_remove", on_api_post_remove.bind(this));

				// Hover
				image_hover.on("change", on_image_hover_change.bind(this));
				this.on_preview_size_change_bind = null;
				this.on_preview_resize_bind = null;
				this.toggle_hotkey = null;

				// Links
				if (settings.values.annotations.modify_urls) {
					file_link.register_modifier(on_image_link_change.bind(this));
				}
			},
		};



		// Annotation data for a single image
		var AnnotationData = function (data) {
			this.datas = [ data ];
			this.current = 0;
		};



		// Private functions
		var re_annotation_texts = /^(?:|([\w\W]*?)(?:\r\n?|\n))[ \t]*>((?:[^>\r\n][^\r\n]*)?)(\r\n?|\n|$)/;
		var re_annotation = /([ \t]*)(\[iex:a\/([^\]]*)\])\s*$/g;
		var checked_attr = "data-iex-annotation-checked";

		var on_api_post_add = function (post_container) {
			if (post_container.getAttribute(checked_attr) === "true") {
				post_container.setAttribute(checked_attr, "false");
			}

			if (api.post_is_floating_or_embedded(post_container)) {
				// Immediate
				on_post_queue_callback.call(this, post_container);
			}
			else {
				this.post_queue.push(post_container);
			}
		};
		var on_api_post_remove = function (post_container) {
			if (post_container.getAttribute(checked_attr) === "false") {
				this.post_queue.pop(post_container);
			}
		};

		var on_post_queue_callback = function (post_container) {
			var not_processed = !post_container.getAttribute(checked_attr);

			post_container.setAttribute(checked_attr, "true");

			// Process container
			if (not_processed) {
				// Not processed yet
				var tag = null,
					tag_node = null;

				api.post_comment_scan(post_container,
					function (text, pos) {
						re_annotation.lastIndex = pos;
						var m = re_annotation.exec(text),
							i;

						if (m === null) return null;

						i = m.index + m[1].length;

						return [ i , i + m[2].length , m ];
					},
					"a",
					function (node, match) {
						node.className = "iex_annotation_tag";
						tag = match[2][3];
						tag_node = node;
					}
				);

				if (tag !== null) {
					setup_tag.call(this, post_container, tag, tag_node);
				}
			}
			/*else {
				// Already processed
				var tags = api.post_query_selector_all(post_container, "a.iex_annotation_tag"),
					i;
			}*/
		};

		var on_image_hover_change = function (data) {
			var n = data.image_container,
				p_id = (n === null || (n = api.post_get_post_container_from_image_container(n)) === null) ? -1 : api.post_get_id(n);

			if (p_id < 0) {
				if (this.post_id_current >= 0) {
					// Update
					set_current_post.call(this, data.image_hover, data.type, p_id);
				}
			}
			else if (p_id !== this.post_id_current) {
				// Update
				set_current_post.call(this, data.image_hover, data.type, p_id);
			}
		};
		var on_preview_resize = function (image_hover, annotation_container) {
			var mpreview = image_hover.mpreview,
				cpreview = mpreview.cpreview,
				w = mpreview.get_size().width,
				scale = (w > 0 ? (cpreview.size.width / w) : 1.0);

			annotation_container.style.fontSize = scale.toFixed(16) + "px";
		};
		var on_preview_size_change = function (image_hover, annotation_data) {
			var true_size = image_hover.mpreview.get_size(),
				current = annotation_data.datas[annotation_data.current],
				tag_size = current.image;

			if (true_size.width !== tag_size.width || true_size.height !== tag_size.height) {
				// Scale
				var xs = true_size.width / tag_size.width,
					ys = true_size.height / tag_size.height,
					i, r;

				for (i = 0; i < this.annotations.length; ++i) {
					r = current.annotations[i].rect;
					this.annotations[i].set_annotation_rect(
						Math.round(r.x * xs),
						Math.round(r.y * ys),
						Math.round(r.height * xs),
						Math.round(r.width * ys)
					);
				}
			}
		};

		var on_image_link_change = function (node, href) {
			var a = node.getAttribute("data-iex-annotations"),
				post_id, data;

			// Fast mode
			if (a) return href + "#" + a;

			// Get id
			post_id = api.post_get_id_from_node(node);
			data = this.annotation_data[post_id];
			if (data) {
				href += "#";
				href += get_url_extension.call(this, data);
			}
			return href;
		};

		var on_toggle_hotkey = function () {
			var post_id = this.post_id_current,
				annotation_data = this.annotation_data[post_id];

			// Change annotation
			annotation_data.current = ((annotation_data.current + 2) % (annotation_data.datas.length + 1)) - 1;

			// Clear and create
			clear_annotations.call(this, image_hover);
			create_annotations.call(this, image_hover, post_id, annotation_data);
		};

		var setup_tag = function (post_container, tag, tag_node) {
			var target_post_id = api.post_get_id(post_container),
				tr, post_info, file_nodes, post_id, data, texts, i, url_ex, url;

			// Validate tag
			tr = new TagReader();
			if (!tr.read(tag)) {
				return set_tag_error.call(this, tag_node, tr.get_error());
			}

			// Find post reference
			post_info = setup_tag_find_reference_post.call(this, post_container, tr);
			if (post_info[0] === null) {
				return set_tag_error.call(this, tag_node, post_info[2]);
			}

			// Find image
			if ((file_nodes = api.post_get_file_nodes(post_info[0])) === null) {
				// No image
				return set_tag_error.call(this, tag_node, "No image attached to post " + post_info[1]);
			}
			if (file_nodes.link === null || file_nodes.link_thumbnail === null) {
				// Invalid
				return set_tag_error.call(this, tag_node, "Image link and/or thumbnail missing for post " + post_info[1]);
			}

			// Find texts
			texts = setup_tag_find_annotation_texts.call(this, post_container);
			if (texts.length === 0) {
				// No text
				return set_tag_error.call(this, tag_node, "No annotation texts found");
			}

			// Set annotation texts
			if (tr.annotations.length === 0) return; // okay, but no need to do anything

			if (texts.length > tr.annotations.length) {
				texts.splice(tr.annotations.length, texts.length - tr.annotations.length);
			}
			else if (texts.length < tr.annotations.length) {
				tr.annotations.splice(texts.length, tr.annotations.length - texts.length);
			}
			for (i = 0; i < texts.length; ++i) {
				tr.annotations[i].text = texts[i];
			}

			// Set data
			tr.tag = tag;
			post_id = post_info[1];
			if (post_id in this.annotation_data) {
				data = this.annotation_data[post_id];
				data.datas.push(tr);
			}
			else {
				data = new AnnotationData(tr);
				this.annotation_data[post_id] = data;
			}

			// Update links
			url_ex = get_url_extension.call(this, data);
			url = (file_nodes.link.getAttribute("href") || "").replace(/#.*$/, "");

			tag_node.setAttribute("target", "_blank");
			tag_node.setAttribute("href", url + "#!" + url_ex);

			if (settings.values.annotations.modify_urls) {
				update_image_link.call(this, file_nodes.link, url_ex);
				update_image_link.call(this, file_nodes.link_thumbnail, url_ex);
			}

			// Apply annotations if open
			if (this.post_id_current === target_post_id && !this.annotations_active) {
				// Apply annotations
				create_annotations.call(this, image_hover, post_id, data);
			}

			return true;
		};
		var setup_tag_find_annotation_texts = function (post_container) {
			var post_text = api.post_get_comment_text(post_container),
				lines = [],
				m;

			// Find lines
			while ((m = re_annotation_texts.exec(post_text)) !== null) {
				post_text = post_text.substr(m.index + m[0].length);

				lines.push(Annotation.TextFormatter.normalize_text(m[2]));
			}

			// Done
			return lines;
		};
		var setup_tag_find_reference_post = function (post_container, tag_reader) {
			if (tag_reader.image.mode === "post") {
				var post_id = -1,
					comment, links, i, j, n, modval;

				if (
					(comment = api.post_get_comment_container(post_container)) !== null &&
					(links = api.post_get_quotelinks(comment)).length > 0
				) {
					// Find the post number
					if (tag_reader.image.post_number_suffix < 0) {
						for (i = 0; i < links.length; ++i) {
							post_id = api.get_quotelink_target(links[i]);
							n = api.get_post_container_from_id(post_id);
							if (n !== null && api.post_has_file(n)) {
								break;
							}
						}
						if (i >= links.length) {
							// Error
							return [ null, -1, "No post reference with an image found in the comment" ];
						}
					}
					else {
						modval = Math.pow(10, tag_reader.image.post_number_suffix_digits);
						for (i = 0; i < links.length; ++i) {
							j = api.get_quotelink_target(links[i]);
							if ((j % modval) === tag_reader.image.post_number_suffix) {
								post_id = j;
								break;
							}
						}

						if (post_id < 0) {
							// Error
							i = tag_reader.image.post_number_suffix.toString();
							while (i.length < tag_reader.image.post_number_suffix_digits) i = "0" + i;
							return [ null, -1, "No post reference ending in " + i + " found in the comment" ];
						}

						n = api.get_post_container_from_id(post_id);
					}

					// Find the post container from the post number
					if (n === null) {
						// Error
						return [ null, -1, "Post number " + post_id + " could not be found" ];
					}

					return [ n, post_id ];
				}
				else {
					return [ null, -1, "No post references found in the comment" ];
				}
			}

			// Else, assume it's the posted image
			return [ post_container, api.post_get_id(post_container) ];
		};
		var set_tag_error = function (tag_node, error) {
			style.add_class(tag_node, "iex_annotation_tag_error");
			tag_node.setAttribute("title", "Invalid annotation tag: " + error);
			return false;
		};
		var update_image_link = function (node, url_ex) {
			var url = node.getAttribute("href") || "";

			url += (url.indexOf("#") < 0) ? "#!" : "#";
			url += url_ex;

			node.setAttribute("data-iex-annotations", url_ex);
			node.setAttribute("href", url);
		};
		var get_url_extension = function (a_data) {
			var url_ex = "",
				i, j, d;

			for (i = 0; i < a_data.datas.length; ++i) {
				if (i > 0) url_ex += "#";

				d = a_data.datas[i];
				url_ex += "iex:a/" + d.tag;

				d = d.annotations;
				url_ex += "?" + encodeURIComponent(d[0].text);

				for (j = 1; j < d.length; ++j) {
					url_ex += "&";
					url_ex += encodeURIComponent(d[j].text);
				}
			}

			return url_ex;
		};

		var set_current_post = function (image_hover, type, post_id) {
			if (this.annotations_active) {
				clear_annotations.call(this, image_hover);
			}

			this.post_id_current = post_id;

			var d;
			if (type === "image" && (d = this.annotation_data[post_id])) {
				// Check if this post_id has any annotations
				create_annotations.call(this, image_hover, post_id, d);
			}
		};
		var clear_annotations = function (image_hover) {
			this.annotations_active = false;

			if (this.annotation_nodes.overlay !== null) {
				// Remove size event
				image_hover.mpreview.off("resize", this.on_preview_resize_bind);
				image_hover.mpreview.on("size_change", this.on_preview_size_change_bind);
				this.on_preview_resize_bind = null;
				this.on_preview_size_change_bind = null;

				// Remove overlay
				var n = this.annotation_nodes.overlay.parentNode;
				if (n !== null) n.removeChild(this.annotation_nodes.overlay);
				this.annotation_nodes.overlay = null;
				this.annotation_nodes.container = null;

				// Remove annotations
				for (var i = 0; i < this.annotations.length; ++i) {
					this.annotations[i].destroy();
				}
				this.annotations = [];
			}

			// Remove hotkeys
			hotkey_manager.unregister(this.toggle_hotkey);
			this.toggle_hotkey = null;
		};
		var create_annotations = function (image_hover, post_id, annotation_data) {
			var mpreview = image_hover.mpreview,
				cpreview = mpreview.cpreview;

			// Overlay
			this.annotations_active = true;

			if (annotation_data.current >= 0) {
				this.annotation_nodes.overlay = $.div("iex_annotation_overlay");

				this.annotation_nodes.container = $.div("iex_annotation_container");
				this.annotation_nodes.overlay.appendChild(this.annotation_nodes.container);

				this.annotation_nodes.overlay.style.pointerEvents = "none";

				// Create annotations
				create_annotations_from_list.call(this,
					annotation_data.datas[annotation_data.current].annotations,
					this.annotations,
					this.annotation_nodes.container
				);

				// Add
				cpreview.add_content(this.annotation_nodes.overlay);

				// Bind events
				this.on_preview_resize_bind = on_preview_resize.bind(this, image_hover, this.annotation_nodes.container);
				this.on_preview_size_change_bind = on_preview_size_change.bind(this, image_hover, annotation_data);
				mpreview.on("resize", this.on_preview_resize_bind);
				mpreview.on("size_change", this.on_preview_size_change_bind);

				// Trigger size update
				this.on_preview_resize_bind();
				this.on_preview_size_change_bind();
			}

			// Hotkey
			this.toggle_hotkey = hotkey_manager.register(settings.values.annotations.toggle_hotkey, HotkeyManager.MODIFIER_NONE, on_toggle_hotkey.bind(this));
		};

		var create_annotations_from_list = function (list, new_list, target) {
			var i, a;

			for (i = 0; i < list.length; ++i) {
				a = new Annotation(null, null);
				a.load_from_state(list[i], null);
				a.add_to(target, null);
				new_list.push(a);
			}
		};



		// Standalone annotator, for a single image
		var Standalone = function (image_node, annotation_data) {
			// Vars
			this.overlay = $.div("iex_annotation_standalone_overlay");
			this.overlay.style.display = "none";
			this.annotation_data = annotation_data;
			this.image_node = image_node;
			this.annotations = [];
			this.image_size = {
				width: 0,
				height: 0,
				acquired: false,
			};
			this.image_node_rect = null;
			this.secondary_timer = null;
			this.on_secondary_timer_bind = this.on_secondary_timer.bind(this);

			// Image events
			this.image_node.addEventListener("load", this.on_image_load.bind(this), false);
			this.image_node.addEventListener("error", this.on_image_error.bind(this), false);
			this.image_size_poll_interval = setInterval(this.on_image_poll.bind(this), 200);

			// Changes
			window.addEventListener("resize", this.on_window_resize.bind(this), false);
			var ob = new MutationObserver(this.on_image_attribute_change.bind(this));
			ob.observe(this.image_node, { attributes: true });

			// Create annotations
			this.create_annotations();

			// Insert overlay
			var rel = this.image_node.nextSibling;
			if (rel !== null) {
				this.image_node.parentNode.insertBefore(this.overlay, rel);
			}
			else {
				this.image_node.parentNode.appendChild(this.overlay);
			}

			// Start
			this.on_image_poll();
			this.update_position();

			// Hotkey
			this.toggle_hotkey = hotkey_manager.register(settings.values.annotations.toggle_hotkey, HotkeyManager.MODIFIER_NONE, this.toggle.bind(this));
		};
		Standalone.prototype = {
			constructor: Standalone,

			clear_annotations: function () {
				for (var i = 0; i < this.annotations.length; ++i) {
					this.annotations[i].destroy();
				}
				this.annotations = [];
			},
			create_annotations: function () {
				if (this.annotation_data.current < 0) return;

				// Create annotations
				create_annotations_from_list.call(this,
					this.annotation_data.datas[this.annotation_data.current].annotations,
					this.annotations,
					this.overlay
				);
			},
			toggle: function () {
				// Change annotation
				this.annotation_data.current = ((this.annotation_data.current + 2) % (this.annotation_data.datas.length + 1)) - 1;

				// Clear and create
				this.clear_annotations();
				this.create_annotations();
			},

			on_image_load: function () {
				if (!this.image_size.acquired) {
					this.acquire_size();
				}
			},
			on_image_error: function () {
				this.overlay.style.display = "none";
			},
			on_image_poll: function () {
				if (this.image_node.naturalWidth && this.image_node.naturalHeight) {
					this.acquire_size();
				}
			},
			on_window_resize: function () {
				this.update_position();
				if (this.secondary_timer !== null) clearTimeout(this.secondary_timer);
				this.secondary_timer = setTimeout(this.on_secondary_timer_bind, 10);
			},
			on_image_attribute_change: function () {
				this.update_position();
				if (this.secondary_timer !== null) clearTimeout(this.secondary_timer);
				this.secondary_timer = setTimeout(this.on_secondary_timer_bind, 10);
			},
			on_secondary_timer: function () {
				this.secondary_timer = null;
				this.update_position();
			},

			acquire_size: function () {
				this.image_size.width = this.image_node.naturalWidth;
				this.image_size.height = this.image_node.naturalHeight;
				this.image_size.acquired = true;

				this.overlay.style.display = "";

				clearInterval(this.image_size_poll_interval);
				this.image_size_poll_interval = null;

				if (this.image_node_size !== null) {
					this.update_position();
					if (this.secondary_timer !== null) clearTimeout(this.secondary_timer);
					this.secondary_timer = setTimeout(this.on_secondary_timer_bind, 10);
				}
			},
			update_position: function () {
				this.image_node_rect = style.get_object_rect(this.image_node);

				var s = this.overlay.style;
				s.left = (this.image_node_rect.left) + "px";
				s.top = (this.image_node_rect.top) + "px";
				s.width = this.image_node_rect.width + "px";
				s.height = this.image_node_rect.height + "px";

				if (this.image_size.acquired) this.update_size();
			},
			update_size: function () {
				var scale = this.image_node_rect.width / this.image_size.width;
				this.overlay.style.fontSize = scale.toFixed(16) + "px";
			},
		};



		// Annotation tag writer
		var TagWriter = function () {
			this.bit_stream = null;
			this.defaults = null;
			this.bits_per = {
				width: 0,
				height: 0,
				post_number: 0,
				formatter_count: 0,
				formatter_position: 0,
			};
		};

		TagWriter.prototype = {
			constructor: TagWriter,
			write: function (image, annotations, suffix_text) {
				// Setup
				this.bit_stream = new BitStream();
				this.bits_per.width = TagWriter.log2bits(image.size.width);
				this.bits_per.height = TagWriter.log2bits(image.size.height);

				// Header
				this.write_header(image, annotations, suffix_text);

				// Annotations
				if (annotations.length > 0) {
					// Default values
					this.write_annotations_header(annotations);

					// Annotations
					for (var i = 0; i < annotations.length; ++i) {
						this.write_annotation_info(annotations[i]);
					}

					// Clear
					this.defaults = null;
				}

				// CRC
				var crc = crc32(this.bit_stream.encode());
				this.bit_stream.add(crc, 32);

				// Clean
				var s = this.bit_stream.encode();
				this.bit_stream = null;
				return "[iex:a/" + s + "]";
			},
			write_header: function (image, annotations, suffix_text) {
				var bits_per = TagWriter.bits_per;

				this.bit_stream.add(TagWriter.version, bits_per.version); // version
				this.bit_stream.add(annotations.length, bits_per.annotation_count); // number of annotations
				this.bit_stream.add((image.mode === "file" ? 0 : (image.mode === "post" ? 1 : 2)), bits_per.image_mode); // image mode [0=file, 1=post, 2=url]
				if (image.mode === "post") {
					// Find how unique the post number is
					var s = image.extra.toString(),
						refs = TagWriter.get_all_post_references(annotations, suffix_text),
						unique_digits = TagWriter.get_longest_same_end(s, refs);

					if (refs.length <= 1) {
						this.bit_stream.add(0, bits_per.post_number_bits);
					}
					else {
						++unique_digits;
						this.bits_per.post_number = Math.ceil(Math.log(10) / TagWriter.ln2 * unique_digits);

						this.bit_stream.add(this.bits_per.post_number, bits_per.post_number_bits);
						this.bit_stream.add(parseInt(s.substr(s.length - unique_digits), 10), this.bits_per.post_number);
					}
				}
				this.bit_stream.add(this.bits_per.width, bits_per.width_bits); // bits for width
				this.bit_stream.add(this.bits_per.height, bits_per.height_bits); // bits for height
				this.bit_stream.add(image.size.width, this.bits_per.width); // width
				this.bit_stream.add(image.size.height, this.bits_per.height); // height
			},
			write_annotations_header: function (annotations) {
				// Default values
				var bits_per = TagWriter.bits_per,
					i, k, f, v;

				this.defaults = {};
				for (k in TagWriter.defaults_formatters) {
					f = TagWriter.defaults_formatters[k];
					this.defaults[k] = TagWriter.find_most_common(annotations, f[0], f[1]);
				}

				// Write defaults
				this.bit_stream.add(this.defaults.font[0], bits_per.font); // font
				this.bit_stream.add(this.defaults.font[1] ? 1 : 0, bits_per.font_bold); // bold
				this.bit_stream.add(this.defaults.font[2] ? 1 : 0, bits_per.font_italic); // italic

				this.bit_stream.add(this.defaults.font_size, bits_per.font_size); // font size

				this.bit_stream.add(this.defaults.align, bits_per.align); // align

				this.bit_stream.add(this.defaults.color_deco[0], bits_per.color); // color
				this.bit_stream.add(this.defaults.color_deco[1], bits_per.color); // color alt
				this.bit_stream.add(this.defaults.color_deco[2], bits_per.color); // color bg
				this.bit_stream.add(this.defaults.color_deco[3], bits_per.text_decoration); // text decoration

				this.bit_stream.add(this.defaults.spacing[0], bits_per.line_spacing); // line spacing
				this.bit_stream.add(this.defaults.spacing[1], bits_per.char_spacing); // char spacing

				// Bit ranges
				this.bits_per.formatter_count = 0;
				this.bits_per.formatter_position = 0;
				for (i = 0; i < annotations.length; ++i) {
					f = annotations[i].get_text_formatters();
					if (f.length > this.bits_per.formatter_count) this.bits_per.formatter_count = f.length;
					for (k = 0; k < f.length; ++k) {
						v = f[k][1];
						if (v > this.bits_per.formatter_position) this.bits_per.formatter_position = v;
					}
				}
				if (this.bits_per.formatter_count > 0) this.bits_per.formatter_count = TagWriter.log2bits(this.bits_per.formatter_count);
				if (this.bits_per.formatter_position > 0) this.bits_per.formatter_position = TagWriter.log2bits(this.bits_per.formatter_position);

				this.bit_stream.add(this.bits_per.formatter_count, bits_per.formatter_count_bits); // bits per formatter count number
				this.bit_stream.add(this.bits_per.formatter_position, bits_per.formatter_position_bits); // bits per formatter position number
			},
			write_annotation_info: function (annotation) {
				var pos = annotation.get_position(),
					size = annotation.get_size(),
					formatters = annotation.get_text_formatters(),
					bits_per = TagWriter.bits_per,
					i, f;

				// rect
				this.bit_stream.add(pos[0], this.bits_per.width); // x
				this.bit_stream.add(pos[1], this.bits_per.height); // y
				this.bit_stream.add(size[0], this.bits_per.width); // width
				this.bit_stream.add(size[1], this.bits_per.height); // height

				// font/bold/italic
				if (!this.is_default("font", annotation)) {
					this.bit_stream.add(annotation.get_font(), bits_per.font); // font
					this.bit_stream.add(annotation.get_font_bold() ? 1 : 0, bits_per.font_bold); // bold
					this.bit_stream.add(annotation.get_font_italic() ? 1 : 0, bits_per.font_italic); // italic
				}

				// font size
				if (!this.is_default("font_size", annotation)) {
					this.bit_stream.add(annotation.get_font_size(), bits_per.font_size); // font size
				}

				// text align
				if (!this.is_default("align", annotation)) {
					this.bit_stream.add(annotation.get_align(), bits_per.align); // align
				}

				// color / decoration
				if (!this.is_default("color_deco", annotation)) {
					this.bit_stream.add(annotation.get_color(0), bits_per.color); // color
					this.bit_stream.add(annotation.get_color(1), bits_per.color); // color alt
					this.bit_stream.add(annotation.get_color(2), bits_per.color); // color bg
					this.bit_stream.add(annotation.get_text_decoration(), bits_per.text_decoration); // text decoration
				}

				// spacing
				if (!this.is_default("spacing", annotation)) {
					this.bit_stream.add(annotation.get_text_char_spacing(), bits_per.line_spacing); // line spacing
					this.bit_stream.add(annotation.get_text_line_spacing(), bits_per.char_spacing); // char spacing
				}

				// Formatters
				this.bit_stream.add(formatters.length, this.bits_per.formatter_count);
				for (i = 0; i < formatters.length; ++i) {
					f = formatters[i];

					this.bit_stream.add(f[0], bits_per.formatter_type); // formatter
					this.bit_stream.add(f[1], this.bits_per.formatter_position); // position
					if (f[0] === Annotation.TextFormattingColor) {
						this.bit_stream.add(f[2], bits_per.color_index); // color index
						this.bit_stream.add(f[3], bits_per.color); // color
					}
					else if (f[0] === Annotation.TextFormattingDecoration) {
						this.bit_stream.add(f[2], bits_per.text_decoration); // decoration
					}
				}
			},
			is_default: function (key, annotation) {
				var formatter = TagWriter.defaults_formatters[key],
					value = formatter[1](this.defaults[key], formatter[0](annotation));

				this.bit_stream.add(value ? 1 : 0, 1);

				return value;
			},
		};

		TagWriter.get_all_post_references = function (annotations, suffix_text) {
			var posts = [],
				post_map = {},
				re_pattern = />>([1-9][0-9]*)/g,
				i, t;

			for (i = 0; i < annotations.length; ++i) {
				if ((t = annotations[i].get_text_before()) !== null) {
					TagWriter.get_post_references(t, re_pattern, posts, post_map);
				}

				TagWriter.get_post_references(annotations[i].get_text(), re_pattern, posts, post_map);
			}

			if (suffix_text !== null) {
				TagWriter.get_post_references(suffix_text, re_pattern, posts, post_map);
			}

			return posts;
		};
		TagWriter.get_post_references = function (text, re_pattern, posts, post_map) {
			var m;

			while ((m = re_pattern.exec(text)) !== null) {
				m = m[1];
				if (!(m in post_map)) {
					post_map[m] = true;
					posts.push(m);
				}
			}
		};
		TagWriter.get_longest_same_end = function (value, all_values) {
			var length = 0,
				i, j, k, v, j_end;

			for (i = 0; i < all_values.length; ++i) {
				v = all_values[i];
				if (value === v) continue;

				j_end = v.length - Math.min(v.length, value.length);
				j = v.length - 1;
				k = value.length - 1;
				while (j >= j_end && v[j] === value[k]) {
					--j;
					--k;
				}
				j = (v.length - 1) - j;
				if (j > length) length = j;
			}

			return length;
		};
		TagWriter.find_most_common = function (array, formatter, compare_same) {
			var best = [ formatter(array[0]), 1 ],
				values = [ best ],
				i, j, f, v;

			for (i = 1; i < array.length; ++i) {
				f = formatter(array[i]);

				for (j = 0; j < values.length; ++j) {
					v = values[j];
					if (compare_same(f, v[0])) {
						if (++v[1] > best[1]) best = v;
						break;
					}
				}

				if (j >= values.length) {
					values.push([ f , 1 ]);
				}
			}

			return best[0];
		};
		TagWriter.compare_same_arrays = function (a1, a2) {
			for (var i = 0; i < a1.length; ++i) {
				if (a1[i] !== a2[i]) return false;
			}
			return true;
		};
		TagWriter.compare_same_values = function (a1, a2) {
			return (a1 === a2);
		};
		TagWriter.log2bits = function (v) {
			if (v < 1) return 0;
			return Math.floor(Math.log(v) / TagWriter.ln2) + 1;
		};

		TagWriter.ln2 = Math.log(2);
		TagWriter.version = 1;
		TagWriter.version_min = 1;
		TagWriter.bits_per = {
			version: 4,
			annotation_count: 6,
			image_mode: 2,
			post_number_bits: 5,
			width_bits: 4,
			height_bits: 4,
			formatter_count_bits: 3,
			formatter_position_bits: 5,

			font: 2,
			font_size: 5,
			font_bold: 1,
			font_italic: 1,
			text_decoration: 2,
			align: 6,
			color: 5,
			formatter_type: 3,
			color_index: 2,
			line_spacing: 4,
			char_spacing: 4,
		};
		TagWriter.defaults_formatters = {
			font: [
				function (annotation) {
					return [
						annotation.get_font(),
						annotation.get_font_bold(),
						annotation.get_font_italic()
					];
				},
				TagWriter.compare_same_arrays
			],
			color_deco: [
				function (annotation) {
					return [
						annotation.get_color(0),
						annotation.get_color(1),
						annotation.get_color(2),
						annotation.get_text_decoration()
					];
				},
				TagWriter.compare_same_arrays
			],
			font_size: [
				function (annotation) {
					return annotation.get_font_size();
				},
				TagWriter.compare_same_values
			],
			align: [
				function (annotation) {
					return annotation.get_align();
				},
				TagWriter.compare_same_values
			],
			spacing: [
				function (annotation) {
					return [
						annotation.get_text_line_spacing(),
						annotation.get_text_char_spacing()
					];
				},
				TagWriter.compare_same_arrays
			],
		};



		// Annotation tag reader
		var TagReader = function () {
			this.annotation_count = 0;
			this.image = {
				mode: null,
				post_number_suffix: -1,
				post_number_suffix_digits: 0,
				width: 0,
				height: 0,
			};
			this.bits_per = {
				width: 0,
				height: 0,
				formatter_count: 0,
				formatter_position: 0,
			};
			this.annotations = null;

			this.tag = null;
			this.bit_stream = null;
			this.defaults = null;
			this.error_message = null;
		};

		TagReader.prototype = {
			constructor: TagReader,
			read: function (tag, header_validate) {
				this.bit_stream = new BitStream();
				this.bit_stream.decode(BitStream.re_alphabet.exec(tag)[0]);

				// Header
				if (!this.read_header()) return false;
				if (header_validate && !header_validate(this)) return this.error("Invalid header");

				// Annotations
				this.annotations = [];
				if (this.annotation_count > 0) {
					if (this.bit_stream.eof()) return this.error("Invalid stream");

					// Defaults
					if (!this.read_annotations_header()) return false;

					// End of stream
					if (this.bit_stream.eof()) return this.error("Invalid stream");

					// Annotations
					for (var i = 0; true; ) {
						if (!this.read_annotation()) return false;
						if (++i >= this.annotation_count) break;
						if (this.bit_stream.eof()) return this.error("Invalid stream");
					}
				}

				// Get crc
				if (this.bit_stream.remaining() < 32) return this.error("Missing CRC");
				var crc_true = crc32(this.bit_stream.to_encoded_string()),
					crc;

				crc = (this.bit_stream.get(32) >>> 0);
				if (crc_true !== crc) return this.error("CRC invalid");

				// Okay
				return true;
			},
			read_header: function () {
				var bits_per = TagWriter.bits_per,
					v, v2;

				// Version
				v = this.bit_stream.get(bits_per.version);
				if (v < TagWriter.version_min || v > TagWriter.version) return this.error("Invalid version");

				// Number of annotations
				this.annotation_count = this.bit_stream.get(bits_per.annotation_count);

				// Image mode
				v = this.bit_stream.get(bits_per.image_mode);
				if (v === 0) {
					this.image.mode = "file";
				}
				else if (v === 1) {
					// Post
					this.image.mode = "post";
					v = this.bit_stream.get(bits_per.post_number_bits);
					if (v > 0) {
						// Post number suffix
						this.image.post_number_suffix = this.bit_stream.get(v);
						this.image.post_number_suffix_digits = Math.floor(Math.log(v) / Math.log(10));
					}
				}
				else if (v === 2) {
					this.image.mode = "url";
				}
				else if (v > 2) {
					return this.error("Invalid image mode");
				}

				// Resolution
				v = this.bit_stream.get(bits_per.width_bits);
				v2 = this.bit_stream.get(bits_per.height_bits);
				this.bits_per.width = v;
				this.bits_per.height = v2;
				this.image.width = this.bit_stream.get(v);
				this.image.height = this.bit_stream.get(v2);

				return true;
			},
			read_annotations_header: function () {
				var bits_per = TagWriter.bits_per;

				this.defaults = new Annotation.State();

				this.defaults.font = this.bit_stream.get(bits_per.font); // font
				if (this.defaults.font >= Annotation.fonts.length) this.defaults.font = 0;
				this.defaults.bold = (this.bit_stream.get(bits_per.font_bold) === 1); // bold
				this.defaults.italic = (this.bit_stream.get(bits_per.font_italic) === 1); // italic

				this.defaults.font_size = this.bit_stream.get(bits_per.font_size); // font size

				this.defaults.align = this.bit_stream.get(bits_per.align); // align

				this.defaults.colors[0] = this.bit_stream.get(bits_per.color); // color[0]
				this.defaults.colors[1] = this.bit_stream.get(bits_per.color); // color[1]
				this.defaults.colors[2] = this.bit_stream.get(bits_per.color); // color[2]
				this.defaults.text_decoration = this.bit_stream.get(bits_per.text_decoration); // text_decoration

				this.defaults.line_spacing = this.bit_stream.get(bits_per.line_spacing); // line spacing
				this.defaults.char_spacing = this.bit_stream.get(bits_per.char_spacing); // char spacing

				this.bits_per.formatter_count = this.bit_stream.get(bits_per.formatter_count_bits); // bits per formatter count number
				this.bits_per.formatter_position = this.bit_stream.get(bits_per.formatter_position_bits); // bits per formatter position number

				return true;
			},
			read_annotation: function () {
				var bits_per = TagWriter.bits_per,
					annotation = new Annotation.State(),
					formatter_count, i, f;

				// rect
				annotation.rect.x = this.bit_stream.get(this.bits_per.width);
				annotation.rect.y = this.bit_stream.get(this.bits_per.height);
				annotation.rect.width = this.bit_stream.get(this.bits_per.width);
				annotation.rect.height = this.bit_stream.get(this.bits_per.height);
				if (annotation.rect.x > this.image.width) {
					annotation.rect.x = this.image.width;
					annotation.rect.width = 0;
				}
				else if (annotation.rect.x > this.image.width) {
					annotation.rect.width = this.image.width - annotation.rect.x;
				}
				if (annotation.rect.y > this.image.height) {
					annotation.rect.y = this.image.height;
					annotation.rect.height = 0;
				}
				else if (annotation.rect.y > this.image.height) {
					annotation.rect.height = this.image.height - annotation.rect.y;
				}

				// font/bold/italic
				if (this.is_default()) {
					// Default
					annotation.font = this.defaults.font;
					annotation.bold = this.defaults.bold;
					annotation.italic = this.defaults.italic;
				}
				else {
					annotation.font = this.bit_stream.get(bits_per.font);
					annotation.bold = (this.bit_stream.get(bits_per.font_bold) === 1);
					annotation.italic = (this.bit_stream.get(bits_per.font_italic) === 1);

					if (annotation.font >= Annotation.fonts.length) annotation.font = 0;
				}

				// font size
				if (this.is_default()) {
					// Default
					annotation.font_size = this.defaults.font_size;
				}
				else {
					annotation.font_size = this.bit_stream.get(bits_per.font_size);
				}

				// text align
				if (this.is_default()) {
					// Default
					annotation.align = this.defaults.align;
				}
				else {
					annotation.align = this.bit_stream.get(bits_per.align);
				}

				// color / decoration
				if (this.is_default()) {
					// Default
					annotation.colors[0] = this.defaults.colors[0];
					annotation.colors[1] = this.defaults.colors[1];
					annotation.colors[2] = this.defaults.colors[2];
					annotation.text_decoration = this.defaults.text_decoration;
				}
				else {
					annotation.colors[0] = this.bit_stream.get(bits_per.color);
					annotation.colors[1] = this.bit_stream.get(bits_per.color);
					annotation.colors[2] = this.bit_stream.get(bits_per.color);
					annotation.text_decoration = this.bit_stream.get(bits_per.text_decoration);
				}

				// spacing
				if (this.is_default()) {
					// Default
					annotation.line_spacing = this.defaults.line_spacing;
					annotation.char_spacing = this.defaults.char_spacing;
				}
				else {
					annotation.line_spacing = this.bit_stream.get(bits_per.line_spacing);
					annotation.char_spacing = this.bit_stream.get(bits_per.char_spacing);
				}

				// Formatters
				formatter_count = this.bit_stream.get(this.bits_per.formatter_count);
				for (i = 0; i < formatter_count; ++i) {
					f = [];
					f.push(this.bit_stream.get(bits_per.formatter_type));
					f.push(this.bit_stream.get(this.bits_per.formatter_position));
					if (f[0] === Annotation.TextFormattingColor) {
						f.push(this.bit_stream.get(bits_per.color_index));
						f.push(this.bit_stream.get(bits_per.color));
					}
					else if (f[0] === Annotation.TextFormattingDecoration) {
						f.push(this.bit_stream.get(bits_per.text_decoration));
					}
					annotation.formatters.push(f);
				}

				// Done
				this.annotations.push(annotation);
				return true;
			},
			is_default: function () {
				return (this.bit_stream.get(1) === 1);
			},
			error: function (message) {
				this.defaults = null;
				this.annotations = null;
				this.error_message = message;
				return false;
			},
			clear: function () {
				this.defaults = null;
				this.annotations = null;
				this.bit_stream = null;
				this.error_message = null;
			},
			get_error: function () {
				return this.error_message;
			},
		};



		// Static functions
		Annotator.create_image_overlay = function (image_node, annotation_data) {
			new Standalone(image_node, annotation_data);
		};
		Annotator.check_url = function () {
			var hash = window.location.href,
				p = hash.indexOf("#"),
				ret = null,
				re, m, i, tag, lines, tr;

			if (p < 0) return null;
			hash = hash.substr(p);

			re = /#!?iex:a\/([^?#]*)(?:\?([^#]*))?/g;
			while ((m = re.exec(hash)) !== null) {
				tag = m[1];
				if (m[2] === undefined) {
					lines = null;
				}
				else {
					lines = m[2].split("&");
					for (i = 0; i < lines.length; ++i) {
						lines[i] = decodeURIComponent(lines[i]);
					}
				}

				tr = new TagReader();
				if (lines.length !== null && tr.read(tag) && tr.annotations.length > 0) {
					if (ret === null) {
						ret = new AnnotationData(tr);
					}
					else {
						ret.datas.push(tr);
					}

					if (tr.annotations.length > lines.length) {
						tr.annotations.splice(lines.length, tr.annotations.length - lines.length);
					}

					for (i = 0; i < tr.annotations.length; ++i) {
						tr.annotations[i].text = lines[i];
					}
				}
			}

			return ret;
		};
		Annotator.process_tag = function (tag, header_validate) {
			var tr = new TagReader();
			if (tr.read(tag, header_validate)) {
				return tr;
			}
			return null;
		};
		Annotator.generate_tag = function (image, annotations, suffix_text) {
			return (new TagWriter()).write(image, annotations, suffix_text);
		};



		return Annotator;

	})();
	// Quick reply controller
	var QuickReplyController = (function () {

		var QuickReplyController = function () {
			api.on("quick_reply_add", on_quick_reply_add.bind(this));
			api.on("quick_reply_remove", on_quick_reply_remove.bind(this));
			api.on("quick_reply_show", on_quick_reply_show.bind(this));
			api.on("quick_reply_hide", on_quick_reply_hide.bind(this));
			document.addEventListener("QRFile", on_file_4chanx_poll.bind(this), false);
			document.addEventListener("QRPostSuccessful", on_file_post.bind(this), false);

			this.quick_reply_container = null;
			this.quick_reply_events = [];
			this.quick_reply_observers = [];
			this.quick_reply_file_input = null;
			this.quick_reply_comment = null;
			this.quick_reply_comment_text = "";
			this.quick_reply_comment_length = 0;
			this.quick_reply_comment_references = [];
			this.quick_reply_poll_interval = null;
			this.quick_reply_poll_input_timer = null;
			this.nodes = {
				extra_container: null,
				content_container: null,
				enabled_checkbox: null,
				input_file_button: null,
				input_url_button: null,
				input_post_number: null,
				error_message: null,
				fallback_file_input: null,
			};
			this.node_events = [];

			this.enabled = false;
			this.file = null;
			this.file_url = null;
			this.can_directly_use_files = false;
			this.file_poll_error_timeout = null;
			this.editor = null;
			this.force_change_to_file = false;
			this.file_n_submit = null;

			this.mode = "none";
			this.custom_url = null;
			this.post_reference_number = null;
			this.post_reference_number_image = null;
		};



		var on_quick_reply_add = function (node) {
			this.quick_reply_container = node;

			setup_qr_nodes.call(this, node);
			setup_qr_events.call(this, node);

			if (this.editor !== null) {
				this.editor.set_qr_container(node);
			}
		};
		var on_quick_reply_remove = function (node) {
			remove_qr_events.call(this);
			remove_qr_nodes.call(this);

			if (this.editor !== null) {
				this.editor.set_qr_container(null);
			}
		};
		var on_quick_reply_show = function (node) {
			this.set_enabled(settings.values.annotations.editor_always_enable);
			poll_file_4chanx.call(this);

			if (this.quick_reply_comment !== null) {
				if (this.quick_reply_poll_interval !== null) {
					clearInterval(this.quick_reply_poll_interval);
				}

				this.quick_reply_poll_interval = setInterval(on_comment_value_poll.bind(this), 2000);
				on_comment_value_poll.call(this);
			}
		};
		var on_quick_reply_hide = function (node) {
			this.set_enabled(false);

			if (this.quick_reply_poll_interval !== null) {
				clearInterval(this.quick_reply_poll_interval);
				this.quick_reply_poll_interval = null;
			}
			if (this.quick_reply_poll_input_timer !== null) {
				clearTimeout(this.quick_reply_poll_input_timer);
				this.quick_reply_poll_input_timer = null;
			}
			this.quick_reply_comment_length = 0;
			this.quick_reply_comment_text = "";
			update_available_post_references.call(this, []);
		};

		var on_file_post = function (event) {
			this.set_enabled(false);
			this.clear();
		};
		var on_file_4chanx_poll = function (event) {
			var file = null;
			if (event.detail && event.detail instanceof File) {
				file = event.detail;
			}
			change_file.call(this, file, false);

			if (
				file === null &&
				this.quick_reply_file_n_submit !== null &&
				style.has_class(this.quick_reply_file_n_submit, "has-file")
			) {
				setTimeout(on_file_poll_check_failure.bind(this), 10);
			}
			else {
				// Clear poll error timeout
				if (this.file_poll_error_timeout !== null) {
					clearTimeout(this.file_poll_error_timeout);
					this.file_poll_error_timeout = null;
				}
			}
		};
		var on_file_poll_check_failure = function () {
			if (style.has_class(this.quick_reply_file_n_submit, "has-file")) {
				// Clear poll error timeout
				if (this.file_poll_error_timeout === null) {
					on_file_poll_failure.call(this, null);
				}
			}
			else {
				// Clear poll error timeout
				if (this.file_poll_error_timeout !== null) {
					clearTimeout(this.file_poll_error_timeout);
					this.file_poll_error_timeout = null;
				}
			}
		};
		var on_file_poll_failure = function () {
			this.file_poll_error_timeout = null;

			show_error_message.call(this);
		};

		var on_enabled_check_change = function (event) {
			this.set_enabled(this.nodes.enabled_checkbox.checked);
			return stop_event(event);
		};
		var on_input_file_button_click = function (event) {
			// If there's an error, redirect this to the secondary file input
			if (this.mode !== "file" && this.mode !== "none" && this.file !== null) {
				// Switch mode to file
				set_mode.call(this, "file");
			}
			else if (this.quick_reply_file_input !== null && !is_showing_error_message.call(this)) {
				// Force change file
				this.force_change_to_file = true;
				var self = this,
					first = true;
				var click_fn = function (event) {
					if (event.target === self.quick_reply_file_input && first) {
						first = false;
						return;
					}
					self.force_change_to_file = false;
					document.removeEventListener("click", click_fn, true);
				};
				document.addEventListener("click", click_fn, true); // this is to account for clicking "cancel"

				// Activate
				this.quick_reply_file_input.click();
			}
			else {
				// Click custom file
				this.nodes.fallback_file_input.click();
			}
		};
		var on_input_url_button_click = function (event) {
			var v = null;
			try {
				v = prompt("Enter a URL:", this.custom_url || "");
				if (v === null) return;
			}
			catch (e) {
			}

			set_custom_url.call(this, v ? v : null);
		};
		var on_input_post_number_change = function (event) {
			// Must refer to a valid post with a valid image
			var v = this.nodes.input_post_number.options[this.nodes.input_post_number.selectedIndex];
			v = v ? parseInt(v.value, 10) || null : null;

			set_post_reference.call(this, v);
		};

		var on_file_n_submit_attr_change = function (records) {
			var i;

			for (i = 0; i < records.length; ++i) {
				if (records[i].attributeName == "class") {
					// "has-file" class probably changed
					if (style.has_class(records[i].target, "has-file")) {
						poll_file_4chanx_with_errors.call(this);
					}
					else {
						// Set file to null
						change_file.call(this, null, false);
					}
					break;
				}
			}
		};
		var on_qr_file_change = function (node, event) {
			if (this.can_directly_use_files || (node.files && node.files.length > 0)) {
				// Use file directly
				this.can_directly_use_files = true;
				change_file.call(this, (node.files.length > 0 ? node.files[0] : null), true);
			}
			else {
				poll_file_4chanx_with_errors.call(this);
			}
		};
		var on_qr_file_click = function (node, event) {
			if (event.shiftKey && event.which === 1) {
				setTimeout(update_qr_input_file_events.bind(this, this.quick_reply_container, node), 10);
			}
		};
		var on_input_file_fallback_change = function (event) {
			var file = null;
			if (this.nodes.fallback_file_input.files.length > 0) {
				file = this.nodes.fallback_file_input.files[0];
			}

			this.nodes.fallback_file_input.value = null;

			change_file.call(this, file, true);
		};

		var on_comment_change = function (event) {
			if (this.quick_reply_poll_input_timer !== null) {
				clearTimeout(this.quick_reply_poll_input_timer);
				this.quick_reply_poll_input_timer = null;
			}

			comment_value_update.call(this);
			update_editor_annotations.call(this);
		};
		var on_comment_input = function (event) {
			if (this.quick_reply_poll_input_timer !== null) {
				clearTimeout(this.quick_reply_poll_input_timer);
			}

			var self = this;
			this.quick_reply_poll_input_timer = setTimeout(function () {
				self.quick_reply_poll_input_timer = null;
				comment_value_update.call(self);
			}, 200);
		};
		var on_comment_value_poll = function () {
			if (
				this.quick_reply_poll_input_timer === null &&
				this.quick_reply_comment.value.length !== this.quick_reply_comment_length
			) {
				comment_value_update.call(this);
				update_editor_annotations.call(this);
			}
		};

		var update_qr_input_file_events = function (qr_container, old_node) {
			if (old_node !== null) {
				for (var i = 0; i < this.quick_reply_events.length; ++i) {
					if (this.quick_reply_events[i][0] === old_node) {
						this.quick_reply_events.splice(i, 1);
						continue;
					}
				}

				// Nullify
				change_file.call(this, null, true);
			}

			// Find new
			var n;
			if ((n = qr_container.querySelector("input[type=file]")) !== null) {
				// Watch for changes
				this.quick_reply_file_input = n;
				add_event_listener(this.quick_reply_events, n, "change", on_qr_file_change.bind(this, n), false);

				if (n.getAttribute("id") === "qrFile") {
					add_event_listener(this.quick_reply_events, n, "click", on_qr_file_click.bind(this, n), false);
				}
			}
		};

		var poll_file_4chanx = function () {
			document.dispatchEvent(new CustomEvent("QRGetFile", {
				bubbles: true,
				detail: null
			}));
		};
		var poll_file_4chanx_with_errors = function () {
			if (this.file_poll_error_timeout !== null) {
				clearTimeout(this.file_poll_error_timeout);
			}

			this.file_poll_error_timeout = setTimeout(on_file_poll_failure.bind(this), 1000);

			poll_file_4chanx.call(this);
		};

		var comment_value_update = function () {
			if (this.quick_reply_container !== null) {
				this.quick_reply_comment_length = this.quick_reply_comment.value.length;
				this.quick_reply_comment_text = this.quick_reply_comment.value;

				var re_number_matcher = />>([1-9][0-9]*)/g,
					references = [],
					already_used = {},
					number, number_key, image_info, m, pc;

				while ((m = re_number_matcher.exec(this.quick_reply_comment_text)) !== null) {
					number = parseInt(m[1], 10);
					number_key = number.toString();
					if (number_key in already_used) continue;

					if ((pc = api.get_post_container_from_post_number(number)) !== null) {
						already_used[number_key] = true;
						image_info = api.post_get_file_nodes(pc);
						references.push([ number, (image_info !== null && image_info.link !== null) ]);
					}
				}

				update_available_post_references.call(this, references);
			}
			else {
				this.quick_reply_comment_length = 0;
				this.quick_reply_comment_text = "";
				update_available_post_references.call(this, []);
			}
		};

		var change_file = function (file, forced) {
			if (this.file === null) {
				hide_error_message.call(this);

				if (file !== null) {
					set_file.call(this, file);
				}
				// else, both null
			}
			else {
				if (
					forced ||
					file === null ||
					this.file.size != file.size ||
					this.file.type != file.type ||
					this.file.name != file.name ||
					(
						file.lastModifiedDate instanceof Date &&
						this.file.lastModifiedDate instanceof Date &&
						this.file.lastModifiedDate.getTime() != file.lastModifiedDate.getTime()
					)
				) {
					hide_error_message.call(this);
					set_file.call(this, file);
				}
			}
		};

		var setup_qr_nodes = function (qr_container) {
			this.quick_reply_container = qr_container;

			var chan4x = (qr_container.getAttribute("id") == "qr");

			// Setup DOM nodes
			var n1, n2, n3, n4, n5, n6;
			n1 = $.div("iex_quick_reply_extra" + (chan4x ? " iex_quick_reply_extra_4chanx" : ""));
			this.nodes.extra_container = n1;



			n2 = $.div("iex_quick_reply_label_table");
			n1.appendChild(n2);

			n3 = $.div("iex_quick_reply_label_row");
			n2.appendChild(n3);

			n4 = $.div("iex_quick_reply_label_cell");
			n3.appendChild(n4);


			n5 = $.label("iex_quick_reply_label");
			n4.appendChild(n5);

			n6 = $.input.check("iex_quick_reply_label_check");
			n5.appendChild(n6);
			this.nodes.enabled_checkbox = n6;

			n6 = $.span("iex_quick_reply_label_text");
			n6.textContent = "iex image annotations";
			n5.appendChild(n6);


			n4 = $.div("iex_quick_reply_label_cell");
			n3.appendChild(n4);

			n5 = $.span("iex_quick_reply_help_link_container");
			n5.appendChild($.text("["));

			n6 = $.a("iex_quick_reply_help_link");
			n6.setAttribute("href", "https://dnsev.github.io/iex/annotations/help.html");
			n6.setAttribute("rel", "nofollow noreferrer");
			n6.setAttribute("target", "_blank");
			n6.textContent = "Help";
			n5.appendChild(n6);

			n5.appendChild($.text("]"));
			n4.appendChild(n5);



			n2 = $.div("iex_quick_reply_content");
			n1.appendChild(n2);
			this.nodes.content_container = n2;

			n3 = $.div("iex_quick_reply_info");
			n3.textContent = "Select an image to annotate:";
			n2.appendChild(n3);

			n3 = $.div("iex_quick_reply_source_selection");



			n4 = $.span("iex_quick_reply_source_selection_text");
			n4.textContent = "From ";
			n3.appendChild(n4);

			n4 = $.input.button("iex_quick_reply_source_selection_file");
			n4.value = "File";
			this.nodes.input_file_button = n4;
			n3.appendChild(n4);

			n4 = $.span("iex_quick_reply_source_selection_text");
			n4.textContent = " or ";
			n3.appendChild(n4);

			n4 = $.input.button("iex_quick_reply_source_selection_url");
			n4.value = "URL";
			this.nodes.input_url_button = n4;
			n3.appendChild(n4);

			n4 = $.span("iex_quick_reply_source_selection_text");
			n4.textContent = " or reference post ";
			n3.appendChild(n4);

			n4 = $.node("select", "iex_quick_reply_source_selection_post");
			n4.value = "";
			this.nodes.input_post_number = n4;

			n5 = $.node("option", "iex_quick_reply_source_selection_post_option_default");
			n5.value = "";
			n5.textContent = ">>number";
			n4.appendChild(n5);
			n3.appendChild(n4);
			n2.appendChild(n3);



			n3 = $.div("iex_quick_reply_error");
			n3.appendChild($.text("Warning: image not detected properly."));
			n3.appendChild($("br"));
			n3.appendChild($.text("Click \"Image\" to select for editing"));
			n3.setAttribute(
				"title",
				"This typically occurs due to an incompatability between scripts.\n" +
				"For example, appchan-x does not support sharing the post file.\n" +
				"Chrome may have similar issues."
			);
			this.nodes.error_message = n3;
			n2.appendChild(n3);



			n3 = $.input.file("iex_quick_reply_file_input");
			this.nodes.fallback_file_input = n3;
			n2.appendChild(n3);



			// Events
			add_event_listener(this.node_events, this.nodes.enabled_checkbox, "change", on_enabled_check_change.bind(this), false);
			add_event_listener(this.node_events, this.nodes.input_file_button, "click", on_input_file_button_click.bind(this), false);
			add_event_listener(this.node_events, this.nodes.input_url_button, "click", on_input_url_button_click.bind(this), false);
			add_event_listener(this.node_events, this.nodes.input_post_number, "change", on_input_post_number_change.bind(this), false);
			add_event_listener(this.node_events, this.nodes.fallback_file_input, "change", on_input_file_fallback_change.bind(this), false);

			// Add to QR
			qr_container.appendChild(n1);
		};
		var setup_qr_events = function (qr_container) {
			var n, ob;
			if ((n = qr_container.querySelector("#file-n-submit")) !== null) {
				// Watch for class changes
				this.quick_reply_file_n_submit = n;
				ob = new MutationObserver(on_file_n_submit_attr_change.bind(this));
				ob.observe(n, { attributes: true });
				this.quick_reply_observers.push(ob);
			}

			if ((n = qr_container.querySelector("textarea[name=com],textarea[data-name=com]")) !== null) {
				// Watch for changes
				this.quick_reply_comment = n;
				this.quick_reply_comment_length = 0;
				add_event_listener(this.quick_reply_events, n, "change", on_comment_change.bind(this), false);
				add_event_listener(this.quick_reply_events, n, "input", on_comment_input.bind(this), false);
			}

			update_qr_input_file_events.call(this, qr_container, null);
		};

		var remove_qr_nodes = function () {
			this.quick_reply_container = null;

			if (this.nodes.extra_container.parentNode !== null) {
				this.nodes.extra_container.parentNode.removeChild(this.nodes.extra_container);
			}

			for (var k in this.nodes) {
				this.nodes[k] = null;
			}
		};
		var remove_qr_events = function () {
			for (var i = 0; i < this.quick_reply_observers.length; ++i) {
				this.quick_reply_observers[i].disconnect();
			}
			this.quick_reply_observers = [];

			remove_event_listeners(this.quick_reply_events);
			remove_event_listeners(this.node_events);
			this.quick_reply_events = [];
			this.node_events = [];

			this.quick_reply_file_input = null;
			this.quick_reply_file_n_submit = null;

			this.quick_reply_comment = null;
			this.quick_reply_comment_text = "";
			this.quick_reply_comment_length = 0;
			this.quick_reply_comment_references = [];
		};

		var is_showing_error_message = function () {
			return (this.quick_reply_container !== null && style.has_class(this.nodes.error_message, "iex_quick_reply_error_visible"));
		};
		var show_error_message = function () {
			if (this.quick_reply_container !== null) {
				style.add_class(this.nodes.error_message, "iex_quick_reply_error_visible");
			}
		};
		var hide_error_message = function () {
			if (this.quick_reply_container !== null) {
				style.remove_class(this.nodes.error_message, "iex_quick_reply_error_visible");
			}
		};

		var update_editor = function () {
			if (this.mode === "file") {
				this.editor.set_file(this.file, this.file_url, this.mode);
				return;
			}
			else if (this.mode === "post") {
				if (this.post_reference_number_image !== null) {
					this.editor.set_file(null, this.post_reference_number_image, this.mode, this.post_reference_number);
					return;
				}
			}
			else if (this.mode === "url") {
				this.editor.set_file(null, this.custom_url, this.mode);
				return;
			}

			// "none" or invalid
			this.editor.hide();
			this.editor.set_file(null, null, "none");
		};
		var update_editor_annotations = function () {
			if (this.editor !== null) {
				this.editor.load_annotations_from_text(this.quick_reply_comment_text);
			}
		};

		var update_available_post_references = function (references) {
			// Only update this if the references are different
			var i, j;
			if (this.quick_reply_comment_references.length === references.length) {
				if (references.length === 0) return; // same

				var r1, r2;

				i = 0;
				while (true) {
					r1 = this.quick_reply_comment_references[i];
					r2 = references[i];
					if (r1[0] !== r2[0] || r1[1] !== r2[1]) break;

					if (++i >= references.length) return; // same
				}
			}

			// Update
			this.quick_reply_comment_references = references;

			var pn_select = this.nodes.input_post_number,
				selected_index = 0,
				first_selected_index = 0,
				first_selected = null,
				n, n2;

			// Remove
			j = 0;
			for (n = pn_select.firstChild; n !== null; n = n2) {
				n2 = n.nextSibling;
				if (n.value !== "") {
					pn_select.removeChild(n);
				}
				else {
					++j;
				}
			}

			// Add
			for (i = 0; i < references.length; ++i) {
				n = references[i][0];

				if (n === this.post_reference_number) {
					selected_index = i + j;
				}

				n2 = $.node("option", "iex_quick_reply_source_selection_post_option");
				n2.value = n.toString();
				n2.textContent = ">>" + n;
				if (!references[i][1]) {
					n2.disabled = true;
				}
				else if (first_selected === null) {
					first_selected = n2;
					first_selected_index = i;
				}
				pn_select.appendChild(n2);
			}

			// Select
			pn_select.selectedIndex = selected_index;

			// Select based on mode
			if (this.enabled && this.mode === "none" && first_selected !== null) {
				pn_select.selectedIndex = first_selected_index + j;
				set_post_reference.call(this, references[first_selected_index][0]);
			}
			else if (selected_index === 0) {
				// Deselect
				auto_detect_mode.call(this);
			}
		};

		var set_file = function (new_file) {
			if (this.file_url !== null) {
				window.URL.revokeObjectURL(this.file_url);
				this.file_url = null;
			}

			this.file = new_file;

			if (this.file === null) {
				if (this.mode === "file") set_mode.call(this, "none");
			}
			else {
				// Create url
				this.file_url = window.URL.createObjectURL(this.file);

				if ((this.mode === "none" && this.enabled) || this.force_change_to_file) {
					this.force_change_to_file = false;
					set_mode.call(this, "file");
				}
			}
		};
		var set_custom_url = function (url) {
			hide_error_message.call(this);

			if (url === null) {
				this.custom_url = null;
				auto_detect_mode.call(this);
			}
			else {
				this.custom_url = url;
				if (this.enabled) {
					if (!set_mode.call(this, "url")) {
						update_editor.call(this);
					}
				}
			}
		};
		var set_post_reference = function (number) {
			hide_error_message.call(this);

			if (number === null) {
				this.post_reference_number = null;
				auto_detect_mode.call(this);
			}
			else {
				this.post_reference_number = number;

				// Get the image
				var pc, image_info, href;
				if (
					(pc = api.get_post_container_from_post_number(this.post_reference_number)) !== null &&
					(image_info = api.post_get_file_nodes(pc)) !== null &&
					image_info.link !== null &&
					(href = image_info.link.getAttribute("href"))
				) {
					this.post_reference_number_image = href;
				}
				else {
					this.post_reference_number_image = null;
				}

				if (this.enabled) {
					if (!set_mode.call(this, "post")) {
						update_editor.call(this);
					}
				}
			}
		};
		var set_mode = function (mode) {
			if (this.mode === mode) return false;
			this.mode = mode;

			if (mode === "file") {
				style.add_class(this.nodes.input_file_button, "iex_quick_reply_source_selection_file_selected");
			}
			else {
				style.remove_class(this.nodes.input_file_button, "iex_quick_reply_source_selection_file_selected");
			}

			if (mode === "post") {
				style.add_class(this.nodes.input_post_number, "iex_quick_reply_source_selection_post_selected");
			}
			else {
				style.remove_class(this.nodes.input_post_number, "iex_quick_reply_source_selection_post_selected");
				this.post_reference_number = null;
				this.post_reference_number_image = null;

				this.nodes.input_post_number.selectedIndex = 0;
			}

			if (mode === "url") {
				style.add_class(this.nodes.input_url_button, "iex_quick_reply_source_selection_url_selected");
			}
			else {
				style.remove_class(this.nodes.input_url_button, "iex_quick_reply_source_selection_url_selected");
				this.custom_url = null;
			}

			// Editor
			if (mode === "none") {
				// Hide
				if (this.editor !== null) {
					update_editor.call(this);
					this.editor.hide();
				}
			}
			else {
				// Show
				if (this.enabled) {
					if (this.editor === null) {
						this.editor = new AnnotationEditor(this.quick_reply_container, this);
					}
					update_editor.call(this);
					if (!this.editor.is_visible()) {
						this.editor.show();
						update_editor_annotations.call(this);
					}
				}
			}
			return true;
		};
		var auto_detect_mode = function () {
			if (this.file !== null) {
				if (this.mode !== "file") {
					set_mode.call(this, "file");
					return;
				}
			}

			if (this.mode !== "post") {
				var pn_select = this.nodes.input_post_number,
					opts = pn_select.options,
					i, n;

				for (i = 0; i < opts.length; ++i) {
					if (!opts[i].disabled && opts[i].value && (n = parseInt(opts[i].value, 10))) {
						pn_select.selectedIndex = i;
						set_post_reference.call(this, n);
						return;
					}
				}
			}

			set_mode.call(this, "none");
		};



		QuickReplyController.prototype = {
			constructor: QuickReplyController,

			is_enabled: function () {
				return this.enabled;
			},
			set_enabled: function (enabled) {

				if (this.quick_reply_container === null || this.enabled === enabled) {
					if (this.nodes.enabled_checkbox !== null) {
						this.nodes.enabled_checkbox.checked = this.enabled;
					}
					return;
				}

				this.enabled = enabled;

				this.nodes.enabled_checkbox.checked = this.enabled;

				if (this.enabled) {
					style.add_class(this.nodes.extra_container, "iex_quick_reply_extra_enabled");

					poll_file_4chanx.call(this);
					auto_detect_mode.call(this);
				}
				else {
					style.remove_class(this.nodes.extra_container, "iex_quick_reply_extra_enabled");

					if (this.editor !== null) {
						this.editor.hide();
					}
					set_mode.call(this, "none");
				}
			},

			get_text: function () {
				return this.quick_reply_comment_text;
			},
			set_text: function (text) {
				this.quick_reply_comment_length = text.length;
				this.quick_reply_comment_text = text;
				this.quick_reply_comment.value = text;
			},

			clear: function () {
				// Clear statuses
				set_mode.call(this, "none");
			},
		};



		return QuickReplyController;

	})();

	// Image annotation editor
	var AnnotationEditor = (function () {

		var AnnotationEditor = function (qr_container, qr_controller) {
			this.qr_controller = qr_controller;

			this.style_transform = style.get_prefixed_style("transform")[0];
			this.visible = false;

			this.qr_container = null;
			this.qr_resize_timer = null;
			this.qr_resized_during_timer = false;

			this.qr_event_list = [];
			this.move_event_list = [];

			this.minimum_ratio_for_wide = 4.0 / 3.0;

			this.paddings = {
				left: 0,
				top: 0,
				right: 0,
				bottom: 0,
			};

			this.image = {
				url: null,
				mode: null,
				extra: null,
				size: {
					width: 0,
					height: 0,
				},
			};
			this.cpreview = null;
			this.cpreview_zoom = 1;
			this.nodes = {
				container: null,
				image: null,
				image_error: null,
				edit_container: null,
				edit_container_empty: null,
				edit_color_buttons: [],
				edit_color_selected: -1,
				edit_font_selector: null,
				edit_font_size_selector: null,
				edit_font_bold_check: null,
				edit_font_italic_check: null,
				annotation_container: null,
				annotation_scroll_h: null,
				annotation_scroll_v: null,
				annotation_scroll_h_size: null,
				annotation_scroll_v_size: null,
			};
			this.image_events = [];
			this.image_load_poll = null;
			this.preview_scale = 1.0;
			this.preview_move_events = [];
			this.preview_move_speed = [0, 0];
			this.preview_move_origin = [0, 0];
			this.preview_move_offset = [0, 0];
			this.preview_scroll_range = [0, 0];

			this.annotations = [];
			this.annotations_previous = this.annotations;
			this.annotation_text_suffix = null;
			this.annotation_text_suffix_previous = this.annotation_text_suffix;
			this.annotation_selected_index = -1;
			this.update_annotation_sizes_on_image_load = null;

			acquire_paddings.call(this);

			window.addEventListener("resize", on_window_resize.bind(this), false);

			this.set_qr_container(qr_container);
		};



		var re_load_annotations = /^(?:|([\w\W]*?)(?:\r\n?|\n))[ \t]*>((?:[^>\r\n][^\r\n]*)?)(\r\n?|\n|$)/,
			re_load_tag = /^(?:|([\w\W]*?)(?:\r\n?|\n))[ \t]*(\[iex:a\/([^\]]*)\])\s*$/;



		var acquire_paddings = function () {
			var n = $("div"),
				r1, r2;

			n.className = "iex_annotation_editor_outer_paddings";
			n.style.paddingRight = "0";
			n.style.paddingBottom = "0";
			n.style.display = "inline-block";
			n.style.width = "0";
			n.style.height = "0";

			document.body.appendChild(n);
			r1 = style.get_object_size(n);
			n.style.paddingTop = "0";
			n.style.paddingLeft = "0";
			n.style.paddingRight = "";
			n.style.paddingBottom = "";
			r2 = style.get_object_size(n);
			document.body.removeChild(n);

			this.paddings.left = r1.width;
			this.paddings.right = r2.width;
			this.paddings.top = r1.height;
			this.paddings.bottom = r2.height;
		};

		var create_nodes = function () {
			var n1, n2, n3, n4, n5;

			// Create
			n1 = $.div("iex_annotation_editor");
			this.nodes.container = n1;

			n2 = $.div("iex_ae_t1c1");
			n1.appendChild(n2);

			n3 = $.div("iex_ae_t1c2");
			n2.appendChild(n3);

			n4 = $.div("iex_ae_t1c3");
			n3.appendChild(n4);
			n5 = $.div("iex_ae_t1c4");
			n4.appendChild(n5);
			create_nodes_preview.call(this, n5);

			n4 = $.div("iex_ae_t1c3");
			n3.appendChild(n4);
			n5 = $.div("iex_ae_t1c4");
			n4.appendChild(n5);
			create_nodes_editing.call(this, n5);

			// Set size
			document.body.appendChild(n1);
			update_size.call(this);
		};
		var create_nodes_preview = function (container) {
			// Create preview
			var n1, n2, n3, event_bind;
			n1 = $.div("iex_ae_preview_container");
			container.appendChild(n1);


			// Message
			n2 = $.div("iex_ae_preview_message");
			n1.appendChild(n2);
			n3 = $.div("iex_ae_preview_message_inner");
			n2.appendChild(n3);
			this.nodes.image_error = n3;


			// Preview
			this.cpreview = new ContentPreview();
			this.cpreview.set_auto_size(true);
			this.cpreview.add_to(n1);


			// Image
			n2 = $.node("img", "iex_ae_preview_image");
			n2.setAttribute("alt", "");
			n2.setAttribute("title", "");
			this.nodes.image = n2;
			this.cpreview.add_content(n2);


			// Overlay
			n2 = $.div("iex_annotation_overlay");
			this.cpreview.add_content(n2);

			n3 = $.div("iex_annotation_container");
			n2.appendChild(n3);
			this.nodes.annotation_container = n3;


			// Events
			n2.addEventListener("mouseover", wrap_mouseenterleave_event(this, on_container_mouseover), false);
			n2.addEventListener("mouseout", wrap_mouseenterleave_event(this, on_container_mouseout), false);
			n2.addEventListener("mousewheel", (event_bind = on_container_mousewheel.bind(this)), false);
			n2.addEventListener("DOMMouseScroll", event_bind, false);
			n2.addEventListener("mousedown", on_container_mousedown.bind(this), false);
			n2.addEventListener("contextmenu", on_container_contextmenu.bind(this), false);


			// Scroll bars
			n2 = $.div("iex_ae_preview_scroll_h");
			n1.appendChild(n2);
			this.nodes.annotation_scroll_h = n2;

			n3 = $.div("iex_ae_preview_scroll_h_inner");
			n2.appendChild(n3);
			this.nodes.annotation_scroll_h_size = n3;


			n2 = $.div("iex_ae_preview_scroll_v");
			n1.appendChild(n2);
			this.nodes.annotation_scroll_v = n2;

			n3 = $.div("iex_ae_preview_scroll_v_inner");
			n2.appendChild(n3);
			this.nodes.annotation_scroll_v_size = n3;
		};
		var create_nodes_editing = function (container) {
			var n1, n2, n3, n4, n5;

			// Create
			n1 = $.div("iex_ae_t2c0");
			container.appendChild(n1);

			n2 = $.div("iex_ae_t2c1");
			n1.appendChild(n2);

			n3 = $.div("iex_ae_t2c2");
			n2.appendChild(n3);

			n4 = $.div("iex_ae_t2c3");
			n3.appendChild(n4);
			n5 = $.div("iex_ae_t2c4");
			n4.appendChild(n5);
			create_nodes_editing_tools.call(this, n5);

			n4 = $.div("iex_ae_t2c3");
			n3.appendChild(n4);
			n5 = $.div("iex_ae_t2c4");
			n4.appendChild(n5);
			create_nodes_editing_text.call(this, n5);
		};
		var create_nodes_editing_tools = function (container) {
			var n1, n2, n3, n4, n5, n6;

			// Create
			n1 = $.div("iex_ae_t3c0");
			container.appendChild(n1);

			n2 = $.div("iex_ae_t3c1");
			n1.appendChild(n2);

			n3 = $.div("iex_ae_t3c2");
			n2.appendChild(n3);

			n4 = $.div("iex_ae_t3c3");
			n3.appendChild(n4);

			// Top (colors)
			n5 = $.div("iex_ae_t3c4");
			n4.appendChild(n5);
			n6 = $.div("iex_ae_t3c5");
			n5.appendChild(n6);
			create_nodes_editing_tools_top.call(this, n6);

			// Top (colors)
			n5 = $.div("iex_ae_t3c4");
			n4.appendChild(n5);
			n6 = $.div("iex_ae_t3c5");
			n5.appendChild(n6);
			create_nodes_editing_tools_colors.call(this, n6);

			// Middle (fonts)
			n5 = $.div("iex_ae_t3c4");
			n4.appendChild(n5);
			n6 = $.div("iex_ae_t3c5");
			n5.appendChild(n6);
			create_nodes_editing_tools_fonts.call(this, n6);

			// Bottom (align)
			create_nodes_editing_tools_align.call(this, n6);
		};
		var create_nodes_editing_tools_top = function (container) {
			var n1, n2, n3;

			n1 = $.div("iex_ae_t7c0");


			this.nodes.edit_color_index_labels = [];


			n2 = $.span("iex_ae_t7c1 iex_ae_top_label");
			n2.textContent = "Color:";
			n1.appendChild(n2);


			n2 = $.span("iex_ae_t7c1 iex_ae_top_color_link");
			n2.textContent = "Text";
			n1.appendChild(n2);
			n2.addEventListener("click", on_color_index_select.bind(this, 0), false);
			this.nodes.edit_color_index_labels.push(n2);


			n2 = $.span("iex_ae_t7c1 iex_ae_top_color_link");
			n2.appendChild($.text("Deco"));
			n3 = $.span("iex_ae_top_hyphen");
			n3.textContent = "-";
			n3.appendChild($("br"));
			n2.appendChild(n3);
			n2.appendChild($.text("ration"));
			n1.appendChild(n2);
			n2.addEventListener("click", on_color_index_select.bind(this, 1), false);
			this.nodes.edit_color_index_labels.push(n2);


			n2 = $.span("iex_ae_t7c1 iex_ae_top_color_link");
			n2.appendChild($.text("Back"));
			n3 = $.span("iex_ae_top_hyphen");
			n3.textContent = "-";
			n3.appendChild($("br"));
			n2.appendChild(n3);
			n2.appendChild($.text("ground"));
			n1.appendChild(n2);
			n2.addEventListener("click", on_color_index_select.bind(this, 2), false);
			this.nodes.edit_color_index_labels.push(n2);


			container.appendChild(n1);
		};
		var create_nodes_editing_tools_colors = function (container) {
			var shades = Annotation.color_shades,
				i, j, n1, n2, n3, n4, n5;

			// Create
			n1 = $.div("iex_ae_t4c0");
			container.appendChild(n1);

			n2 = $.div("iex_ae_t4c1");
			n1.appendChild(n2);

			n3 = $.div("iex_ae_t4c2");
			n2.appendChild(n3);

			// Black and white
			n4 = $.div("iex_ae_t4c3");
			n3.appendChild(n4);
			n5 = $.div("iex_ae_t4c4");
			n4.appendChild(n5);
			n5.appendChild(create_nodes_color_selector.call(this, 0));
			n5.appendChild(create_nodes_color_selector.call(this, 1));

			for (i = 2; i < Annotation.colors.length; i += shades) {
				n4 = $.div("iex_ae_t4c3");
				n3.appendChild(n4);
				n5 = $.div("iex_ae_t4c4");
				n4.appendChild(n5);
				for (j = 0; j < shades; ++j) {
					n5.appendChild(create_nodes_color_selector.call(this, i + j));
				}
			}

		};
		var create_nodes_color_selector = function (color_index) {
			var n1, n2, n3, cls;
			n1 = $.div("iex_ae_color_selector iex_ae_color_selector_disabled");
			n1.setAttribute("title", "Color index: " + color_index);

			n2 = $.div("iex_ae_color_selector_inner");
			n2.style.backgroundColor = "#" + Annotation.colors[color_index];
			n1.appendChild(n2);

			n3 = $.div("iex_ae_color_selector_border");
			n2.appendChild(n3);

			cls = "iex_ae_color_selector_border2";
			if (color_index === 1) cls += " iex_ae_color_selector_border2_alt";
			n3 = $.div(cls);
			n2.appendChild(n3);

			n1.addEventListener("click", on_color_select.bind(this, color_index), false);

			this.nodes.edit_color_buttons.push(n1);
			return n1;
		};
		var create_nodes_editing_tools_fonts = function (container) {
			var n1, n2, n3, n4, n5;

			n1 = $.div("iex_ae_t5c1");
			container.appendChild(n1);

			n2 = $.div("iex_ae_t5c2");
			n1.appendChild(n2);


			// Column 1
			n3 = $.div("iex_ae_t5c3");
			n2.appendChild(n3);

			n4 = $.div("iex_ae_new_annotation_button");
			n3.appendChild(n4);
			n5 = $.div("iex_ae_new_annotation_button_inner");
			n4.appendChild(n5);
			n5.textContent = "New Note";
			n4.addEventListener("click", on_new_note_click.bind(this), false);


			// Column 2
			n3 = $.div("iex_ae_t5c3");
			n2.appendChild(n3);

			n4 = $.node("select", "iex_ae_selection");
			n4.addEventListener("change", on_font_select.bind(this, n4), false);
			n4.setAttribute("title", "Font to use for the annotation");
			n3.appendChild(n4);
			n4.disabled = true;
			this.nodes.edit_font_selector = n4;
			create_nodes_font_selection.call(this, n4);

			n4 = $.node("select", "iex_ae_selection");
			n4.addEventListener("change", on_font_scale_select.bind(this, n4), false);
			n4.setAttribute("title", "Font size to use for the annotation");
			n3.appendChild(n4);
			n4.disabled = true;
			this.nodes.edit_font_size_selector = n4;
			create_nodes_font_size_selection.call(this, n4);


			// Column 3
			n3 = $.div("iex_ae_t5c3");
			n2.appendChild(n3);

			n4 = $.label("iex_ae_option_label");
			n3.appendChild(n4);
			n5 = $.input.check("iex_ae_option_checkbox iex_ae_option_checkbox_bold");
			n5.addEventListener("change", on_font_bold_change.bind(this, n5), false);
			this.nodes.edit_font_bold_check = n5;
			n5.disabled = true;
			n4.appendChild(n5);
			n5 = $.span("iex_ae_option_label_text");
			n5.textContent = " Bold";
			n4.appendChild(n5);

			n4 = $.label("iex_ae_option_label");
			n3.appendChild(n4);
			n5 = $.input.check("iex_ae_option_checkbox iex_ae_option_checkbox_italic");
			n5.addEventListener("change", on_font_italic_change.bind(this, n5), false);
			this.nodes.edit_font_italic_check = n5;
			n5.disabled = true;
			n4.appendChild(n5);
			n5 = $.span("iex_ae_option_label_text");
			n5.textContent = " Italic";
			n4.appendChild(n5);
		};
		var create_nodes_editing_tools_align = function (container) {
			var n1, n2, n3, n4;

			var add_opt = function (par, text, value) {
				var n1 = $.node("option", "iex_ae_selection_option");
				n1.textContent = text;
				n1.value = value.toString();
				par.appendChild(n1);
				return n1;
			};

			n1 = $.div("iex_ae_t5c1");
			container.appendChild(n1);

			n2 = $.div("iex_ae_t5c2");
			n1.appendChild(n2);


			// Column 1
			n3 = $.div("iex_ae_t5c3 iex_ae_t5c3_small_space");
			n2.appendChild(n3);

			n4 = $.node("select", "iex_ae_selection");
			n4.addEventListener("change", on_halign_select.bind(this, n4), false);
			n4.setAttribute("title", "Horizontal alignment for the annotation text");
			n3.appendChild(n4);
			n4.disabled = true;
			this.nodes.edit_halign_selector = n4;
			add_opt(n4, "Center", Annotation.AlignCenter);
			add_opt(n4, "Left", Annotation.AlignLeft);
			add_opt(n4, "Right", Annotation.AlignRight);
			add_opt(n4, "Justify", Annotation.AlignJustify);

			// Column 2
			n3 = $.div("iex_ae_t5c3 iex_ae_t5c3_small_space");
			n2.appendChild(n3);

			n4 = $.node("select", "iex_ae_selection");
			n4.addEventListener("change", on_valign_select.bind(this, n4), false);
			n4.setAttribute("title", "Vertical alignment for the annotation text");
			n3.appendChild(n4);
			n4.disabled = true;
			this.nodes.edit_valign_selector = n4;
			add_opt(n4, "Middle", Annotation.AlignMiddle);
			add_opt(n4, "Top", Annotation.AlignTop);
			add_opt(n4, "Bottom", Annotation.AlignBottom);

			// Column 3
			n3 = $.div("iex_ae_t5c3 iex_ae_t5c3_small_space");
			n2.appendChild(n3);

			n4 = $.node("select", "iex_ae_selection");
			n4.addEventListener("change", on_text_decoration_select.bind(this, n4), false);
			n4.setAttribute("title", "Text decoration annotation text");
			n3.appendChild(n4);
			n4.disabled = true;
			this.nodes.edit_decoration_selector = n4;
			add_opt(n4, "Normal", Annotation.TextDecorationNone).setAttribute("title", "Decoration index: 0");
			add_opt(n4, "Shadow", Annotation.TextDecorationShadow).setAttribute("title", "Decoration index: 1");
			add_opt(n4, "Blur", Annotation.TextDecorationShadowBlur).setAttribute("title", "Decoration index: 2");
			add_opt(n4, "Strong Blur", Annotation.TextDecorationShadowBlurStrong).setAttribute("title", "Decoration index: 3");
		};
		var create_nodes_font_selection = function (container) {
			var i, n1;

			for (i = 0; i < Annotation.fonts.length; ++i) {
				n1 = $.node("option", "iex_ae_selection_option");
				n1.textContent = Annotation.fonts[i].name;
				n1.value = i.toString();

				container.appendChild(n1);
			}
		};
		var create_nodes_font_size_selection = function (container) {
			var size = Annotation.font_size_settings.base,
				size_incr = Annotation.font_size_settings.base_step,
				i, j, n1;

			for (i = 0; i < Annotation.font_size_settings.step_count; ++i) {
				for (j = 0; j < Annotation.font_size_settings.step_duration; ++j) {
					n1 = $.node("option", "iex_ae_selection_option");
					n1.textContent = "Size: " + size + "px";
					n1.value = (i * Annotation.font_size_settings.step_duration + j).toString();

					container.appendChild(n1);

					size += size_incr;
				}
				size_incr *= Annotation.font_size_settings.step_multiplier;
			}
		};
		var create_nodes_editing_text = function (container) {
			var n1, n2, n3, n4, n5;

			n1 = $.div("iex_ae_t6c0");
			container.appendChild(n1);

			n2 = $.div("iex_ae_t6c1");
			n1.appendChild(n2);

			n3 = $.div("iex_ae_t6c2");
			n2.appendChild(n3);
			this.nodes.edit_container = n3;

			n4 = $.div("iex_ae_add_annotation_container_empty");
			n3.appendChild(n4);
			this.nodes.edit_container_empty = n4;

			n5 = $.a("iex_ae_add_annotation_container_empty_link");
			n4.appendChild(n5);
			n5.textContent = "Add annotation";
			n5.addEventListener("click", on_new_note_empty_click.bind(this), false);

			n4 = $.div("iex_ae_restore_annotations_container_empty");
			n3.appendChild(n4);
			this.nodes.edit_container_empty_restore = n4;

			n5 = $.a("iex_ae_restore_annotations_container_empty_link");
			n4.appendChild(n5);
			n5.textContent = "Restore recent annotations";
			n5.addEventListener("click", on_restore_annotations_click.bind(this), false);

			set_empty_links_visible.call(this, true);
		};
		var remove_nodes = function () {
		};

		var setup_qr_events = function () {
			var movable_sections = this.qr_container.querySelectorAll(".move,.drag,textarea"),
				i;

			// Events
			for (i = 0; i < movable_sections.length; ++i) {
				add_event_listener(this.qr_event_list, movable_sections[i], "mousedown", on_movable_mousedown.bind(this), false);
			}
		};
		var remove_qr_events = function () {
			remove_event_listeners(this.qr_event_list);
			this.qr_event_list = [];
		};

		var update_size = function () {
			var max_rect = style.get_document_rect(),
				doc_left = max_rect.left,
				doc_top = max_rect.top,
				header_rect = api.get_header_rect(),
				qr_rect = style.get_object_rect(this.qr_container),
				container = this.nodes.container,
				x_mid, x, y, w, h;

			// Subtract header
			if (header_rect.top <= max_rect.top && header_rect.bottom > max_rect.top) {
				// It's on the top
				max_rect.top = header_rect.bottom;
			}
			else if (header_rect.bottom >= max_rect.bottom && header_rect.top < max_rect.bottom) {
				// It's on the bottom
				max_rect.bottom = header_rect.top;
			}

			// Subtract size of quick reply if it results in a large enough container
			x_mid = (doc_left + max_rect.right) / 2.0;
			if (qr_rect.left >= x_mid + this.paddings.left) {
				// On the right
				max_rect.right = qr_rect.left;
			}
			else if (qr_rect.right <= x_mid - this.paddings.right) {
				// On the left
				max_rect.left = qr_rect.right;
			}

			// Finalize position
			x = (max_rect.left + this.paddings.left);
			y = (max_rect.top + this.paddings.top);
			w = (max_rect.right - this.paddings.right) - x;
			h = (max_rect.bottom - this.paddings.bottom) - y;
			x -= doc_left;
			y -= doc_top;
			if (w < 16) w = 16;
			if (h < 16) h = 16;

			// Set size
			container.style.left = x + "px";
			container.style.top = y + "px";
			container.style.width = w + "px";
			container.style.height = h + "px";

			// Ratio
			if (w / h >= this.minimum_ratio_for_wide) {
				style.remove_class(container, "iex_annotation_editor_vertical");
			}
			else {
				style.add_class(container, "iex_annotation_editor_vertical");
			}

			// Preview size
			update_preview_size.call(this);
		};
		var update_preview_size = function (offset_x, offset_y) {
			var w, h, size, r;

			// Resize
			this.cpreview.update_auto_size();
			size = this.cpreview.size;

			// Get image size
			w = this.image.size.width;
			h = this.image.size.height;

			// Ratios
			r = w / h;
			if ((size.width / size.height) > r) {
				// fit width
				w = size.width;
				h = w / r;
			}
			else {
				// fit height
				h = size.height;
				w = h * r;
			}

			// Zoom
			w *= this.cpreview_zoom;
			h *= this.cpreview_zoom;

			this.cpreview.set_size(w, h, offset_x, offset_y);

			// Annotation scaling
			this.preview_scale = 1.0;
			if (this.image.size.width > 0) this.preview_scale = (this.cpreview.size.width / this.image.size.width);
			if (Annotation.scaling_mode_px) {
				this.nodes.annotation_container.style.fontSize = this.preview_scale.toFixed(4) + "px";
			}
			else {
				this.nodes.annotation_container.style[this.style_transform] = "scale(" + this.preview_scale.toFixed(4) + ")";
			}

			// Scrollbars
			update_preview_scrollbars.call(this);
			show_preview_scrollbars.call(this);
		};
		var update_preview_scrollbars = function () {
			var size = this.cpreview.size,
				display_size = this.cpreview.display,
				w, h;

			if (size.width > display_size.width + 0.001) {
				style.add_class(this.nodes.annotation_scroll_h, "iex_ae_preview_scroll_displayable");
				w = (size.width - display_size.width) / size.width;
				this.nodes.annotation_scroll_h_size.style.width = ((1 - w) * 100).toFixed(4) + "%";
			}
			else {
				style.remove_class(this.nodes.annotation_scroll_h, "iex_ae_preview_scroll_displayable");
				w = 0;
			}

			if (size.height > display_size.height + 0.001) {
				style.add_class(this.nodes.annotation_scroll_v, "iex_ae_preview_scroll_displayable");
				h = (size.height - display_size.height) / size.height;
				this.nodes.annotation_scroll_v_size.style.height = ((1 - h) * 100).toFixed(4) + "%";
			}
			else {
				style.remove_class(this.nodes.annotation_scroll_v, "iex_ae_preview_scroll_displayable");
				h = 0;
			}

			this.preview_scroll_range[0] = w * 100;
			this.preview_scroll_range[1] = h * 100;

			update_preview_scrollbar_positions.call(this);
		};
		var update_preview_scrollbar_positions = function () {
			this.nodes.annotation_scroll_h_size.style.left = (this.cpreview.offset_x * this.preview_scroll_range[0]).toFixed(4) + "%";
			this.nodes.annotation_scroll_v_size.style.top = (this.cpreview.offset_y * this.preview_scroll_range[1]).toFixed(4) + "%";
		};
		var show_preview_scrollbars = function () {
			style.add_class(this.nodes.annotation_scroll_h, "iex_ae_preview_scroll_visible");
			style.add_class(this.nodes.annotation_scroll_v, "iex_ae_preview_scroll_visible");
		};
		var hide_preview_scrollbars = function () {
			style.remove_class(this.nodes.annotation_scroll_h, "iex_ae_preview_scroll_visible");
			style.remove_class(this.nodes.annotation_scroll_v, "iex_ae_preview_scroll_visible");
		};

		var set_image = function (url, mode, extra) {
			// Events
			clear_image_events.call(this);

			this.image.url = url;
			this.image.mode = mode;
			this.image.extra = extra;
			this.image.size.width = 0;
			this.image.size.height = 0;

			var image = this.nodes.image;

			style.remove_class(image, "iex_ae_preview_image_visible");
			style.remove_class(image, "iex_ae_preview_image_loaded");
			style.remove_class(image, "iex_ae_preview_image_error");
			style.remove_class(this.nodes.annotation_scroll_h, "iex_ae_preview_scroll_visible");
			style.remove_class(this.nodes.annotation_scroll_v, "iex_ae_preview_scroll_visible");
			image.removeAttribute("src");

			if (url === null) {
				// Image not available
				this.nodes.image_error.textContent = "Image not available";
				return;
			}

			this.nodes.image_error.textContent = "";

			add_event_listener(this.image_events, image, "load", on_image_load.bind(this, image), false);
			add_event_listener(this.image_events, image, "error", on_image_error.bind(this, image), false);
			this.image_load_poll = setInterval(on_image_status_poll.bind(this, image), 200);

			update_preview_size.call(this);

			image.setAttribute("src", url);

			update_annotation_full_text.call(this, false);
		};
		var clear_image_events = function () {
			if (this.image_events.length > 0) {
				on_image_abort.call(this, this.nodes.image);

				remove_event_listeners(this.image_events);
				this.image_events = [];

				if (this.image_load_poll !== null) {
					clearInterval(this.image_load_poll);
					this.image_load_poll = null;
				}
			}
		};

		var change_zoom_level = function (new_zoom, update_offset, mx, my) {
			var left, top;

			if (update_offset) {
				var rect = style.get_object_rect_relative(this.cpreview.nodes.container),
					image_rect = style.get_object_rect_relative(this.nodes.image),
					scale = (this.cpreview_zoom / new_zoom),
					w = image_rect.width / scale,
					h = image_rect.height / scale,
					left_max = (w - rect.width) / w,
					top_max = (h - rect.height) / h;

				left = (((mx - rect.left) * (1 - scale)) + (rect.left - image_rect.left)) / image_rect.width;
				top = (((my - rect.top) * (1 - scale)) + (rect.top - image_rect.top)) / image_rect.height;

				if (left_max > 0) {
					if (left < 0) left = 0;
					else if (left > left_max) left = left_max;
					left /= left_max;
				}
				else {
					left = 0;
				}

				if (top_max > 0) {
					if (top < 0) top = 0;
					else if (top > top_max) top = top_max;
					top /= top_max;
				}
				else {
					top = 0;
				}
			}

			this.cpreview_zoom = new_zoom;
			update_preview_size.call(this, left, top);
		};

		var set_empty_links_visible = function (visible) {
			var s1 = "iex_ae_add_annotation_container_empty_visible",
				s2 = "iex_ae_restore_annotations_container_empty_visible";

			if (visible) {
				style.add_class(this.nodes.edit_container_empty, s1);
			}
			else {
				style.remove_class(this.nodes.edit_container_empty, s1);
			}

			if (visible && this.annotations_previous !== this.annotations && this.annotations_previous.length > 0) {
				style.add_class(this.nodes.edit_container_empty_restore, s2);
			}
			else {
				style.remove_class(this.nodes.edit_container_empty_restore, s2);
			}
		};

		var clear_previous_annotations = function () {
			// Clear previous
			if (this.annotations_previous !== this.annotations) {
				for (var i = 0; i < this.annotations_previous.length; ++i) {
					this.annotations_previous[i].destroy();
				}
				this.annotations_previous = this.annotations;
				this.annotation_text_suffix_previous = null;
			}
		};
		var set_no_annotations_selected = function () {
			var ns = this.nodes.edit_color_buttons,
				i;

			i = this.nodes.edit_color_selected;
			if (i >= 0) {
				style.remove_class(ns[i], "iex_ae_color_selector_selected");

				for (i = 0; i < ns.length; ++i) {
					style.add_class(ns[i], "iex_ae_color_selector_disabled");
				}

				this.nodes.edit_color_selected = -1;
			}

			ns = this.nodes.edit_color_index_labels;
			for (i = 0; i < ns.length; ++i) {
				style.remove_class(ns[i], "iex_ae_top_color_link_selected");
			}

			this.nodes.edit_font_selector.disabled = true;
			this.nodes.edit_font_selector.selectedIndex = 0;
			this.nodes.edit_font_size_selector.disabled = true;
			this.nodes.edit_font_size_selector.selectedIndex = 0;
			this.nodes.edit_halign_selector.disabled = true;
			this.nodes.edit_halign_selector.selectedIndex = 0;
			this.nodes.edit_valign_selector.disabled = true;
			this.nodes.edit_valign_selector.selectedIndex = 0;
			this.nodes.edit_decoration_selector.disabled = true;
			this.nodes.edit_decoration_selector.selectedIndex = 0;
			this.nodes.edit_font_bold_check.disabled = true;
			this.nodes.edit_font_bold_check.checked = false;
			this.nodes.edit_font_italic_check.disabled = true;
			this.nodes.edit_font_italic_check.checked = false;

			this.annotation_selected_index = -1;
		};
		var set_annotation_selected = function (annotation_index, select_text) {
			if (annotation_index === this.annotation_selected_index) return;

			// Update selection
			if (this.annotation_selected_index >= 0) {
				this.annotations[this.annotation_selected_index].set_selected(false);
			}
			this.annotation_selected_index = annotation_index;

			update_annotation_color_selected.call(this);
			update_annotation_color_index_selected.call(this);
			update_annotation_align_selected.call(this);
			update_annotation_font_selected.call(this);
			update_annotation_font_size_selected.call(this);
			update_annotation_text_decoration_selected.call(this);
			update_annotation_font_bold_selected.call(this);
			update_annotation_font_italic_selected.call(this);

			this.annotations[this.annotation_selected_index].set_selected(true, !select_text);
		};
		var add_new_annotation = function (text_before, simple_mode) {
			if (this.annotations_previous !== this.annotations) {
				clear_previous_annotations.call(this);
			}

			// Create new
			var index = this.annotations.length,
				sz = get_annotation_default_size.call(this, simple_mode),
				a, m, i;

			if (text_before === undefined) {
				if (this.annotation_text_suffix === null) {
					text_before = null;
					this.annotation_text_suffix = ""; // will add an additional newline before the tag
				}
				else {
					m = /\s*$/.exec(this.annotation_text_suffix)[0];
					i = this.annotation_text_suffix.length - m.length;

					text_before = this.annotation_text_suffix.substr(0, i);
					this.annotation_text_suffix = this.annotation_text_suffix.substr(i).replace(/^(?:\r\n?|\n)/, "");

					if (text_before.length === 0) text_before = null;
				}
			}

			a = new Annotation(this, {
				edit: true,
				index: index,
				font_size_index: sz.font_size_index,
				font: settings.values.annotations.defaults.font,
				bold: settings.values.annotations.defaults.bold,
				italic: settings.values.annotations.defaults.italic,
				x: sz.x,
				y: sz.y,
				width: sz.width,
				height: sz.height,
				text_before: text_before
			});

			this.annotations.push(a);
			a.add_to(this.nodes.annotation_container, this.nodes.edit_container);

			if (!simple_mode) {
				set_empty_links_visible.call(this, false);
				set_annotation_selected.call(this, index, true);

				update_annotation_full_text.call(this, false);
			}

			return a;
		};
		var remove_annotation = function (index, update) {
			var a = this.annotations[index],
				tb = a.get_text_before(),
				tb2, i;

			// Remove
			if (this.annotations.length === 1) {
				this.annotations = [];
				a.remove_nodes();
				this.annotation_text_suffix_previous = this.annotation_text_suffix;
			}
			else {
				this.annotations.splice(index, 1);
				a.destroy();
			}

			// Update indices
			for (i = index; i < this.annotations.length; ++i) {
				this.annotations[i].set_index(i);
			}

			// Update text before
			if (tb !== null) {
				if (index < this.annotations.length) {
					tb2 = this.annotations[index].get_text_before();
					tb2 = (tb2 === null) ? tb : (tb + "\n" + tb2);
					this.annotations[index].set_text_before(tb2);
				}
				else {
					tb2 = this.annotation_text_suffix;
					this.annotation_text_suffix = (tb2 === null) ? tb : (tb + "\n" + tb2);
				}
			}

			// Update selection
			if (this.annotation_selected_index === index) {
				i = this.annotation_selected_index;
				if (i > 0) --i;
				if (i < this.annotations.length) {
					this.annotation_selected_index = -1;
					set_annotation_selected.call(this, i, true);
				}
				else {
					set_no_annotations_selected.call(this);
				}
			}
			else if (this.annotation_selected_index > index) {
				--this.annotation_selected_index;
			}

			// Update text
			if (update) update_annotation_full_text.call(this, false);

			// Show links
			if (this.annotations.length === 0) {
				// Show
				set_empty_links_visible.call(this, true);
			}
		};
		var get_annotation_default_size = function (simple_mode) {
			// New size
			var x = 0,
				y = 0,
				w = 100,
				h = 100,
				font_size, s;

			if (this.image.size.width > 0 && this.image.size.height > 0 && !simple_mode) {
				// Font size
				s = Math.min(this.image.size.width, this.image.size.height) / 32;
				font_size = Annotation.font_size_to_size_info(s);
				w = Math.round(w * font_size[1] / 16);
				h = Math.round(h * font_size[1] / 16);

				// Size
				if (w > this.image.size.width) {
					w = this.image.size.width;
				}
				else {
					s = this.image.size.width / this.cpreview.size.width;
					x = (this.cpreview.size.width - this.cpreview.display.width) * s * this.cpreview.offset_x + (this.cpreview.display.width * s - w) / 2;
					x = Math.round(x);
				}

				if (h > this.image.size.height) {
					h = this.image.size.height;
				}
				else {
					s = this.image.size.height / this.cpreview.size.height;
					y = (this.cpreview.size.height - this.cpreview.display.height) * s * this.cpreview.offset_y + (this.cpreview.display.height * s - h) / 2;
					y = Math.round(y);
				}
			}
			else {
				font_size = Annotation.font_size_to_size_info(16);
			}

			return {
				font_size_index: font_size[0],
				x: x,
				y: y,
				width: w,
				height: h,
			};
		};
		var rebound_annotations = function () {
			if (this.image.size.width <= 0 || this.image.size.height <= 0) return;

			// Update annotation bounds
			var i, a, sz, pos, x, y, w, h, update;

			for (i = 0; i < this.annotations.length; ++i) {
				update = false;
				a = this.annotations[i];
				sz = a.get_size();
				pos = a.get_position();
				x = pos[0];
				y = pos[1];
				w = sz[0];
				h = sz[1];

				if (w > this.image.size.width) {
					x = 0;
					w = this.image.size.width;
					update = true;
				}
				else if (x + w > this.image.size.width) {
					x = this.image.size.width - w;
					update = true;
				}

				if (h > this.image.size.height) {
					y = 0;
					h = this.image.size.height;
					update = true;
				}
				else if (y + h > this.image.size.height) {
					y = this.image.size.height - h;
					update = true;
				}

				if (update) {
					a.set_annotation_rect(x, y, w, h);
				}
			}
		};
		var apply_default_size_to_annotations = function (annotations) {
			var sz = get_annotation_default_size.call(this, false),
				i, a;

			for (i = 0; i < annotations.length; ++i) {
				a = annotations[i];
				a.set_font_size(sz.font_size_index);
				a.set_annotation_rect(sz.x, sz.y, sz.width, sz.height);
			}
		};

		var update_annotation_color_selected = function () {
			var a = this.annotations[this.annotation_selected_index],
				ns = this.nodes.edit_color_buttons,
				i;

			if (this.nodes.edit_color_selected >= 0) {
				style.remove_class(ns[this.nodes.edit_color_selected], "iex_ae_color_selector_selected");
			}
			else {
				for (i = 0; i < ns.length; ++i) {
					style.remove_class(ns[i], "iex_ae_color_selector_disabled");
				}
			}
			this.nodes.edit_color_selected = a.get_color(a.get_color_index_selected());
			style.add_class(ns[this.nodes.edit_color_selected], "iex_ae_color_selector_selected");
		};
		var update_annotation_color_index_selected = function () {
			var sel = this.annotations[this.annotation_selected_index].get_color_index_selected(),
				cls = "iex_ae_top_color_link_selected",
				ns, i;


			ns = this.nodes.edit_color_index_labels;
			for (i = 0; i < ns.length; ++i) {
				if (i === sel) {
					style.add_class(ns[i], cls);
				}
				else {
					style.remove_class(ns[i], cls);
				}
			}
		};
		var update_annotation_align_selected = function () {
			var a = this.annotations[this.annotation_selected_index];

			this.nodes.edit_halign_selector.disabled = false;
			this.nodes.edit_halign_selector.selectedIndex = (a.get_align() & Annotation.AlignHorizontal);
			this.nodes.edit_valign_selector.disabled = false;
			this.nodes.edit_valign_selector.selectedIndex = (a.get_align() & Annotation.AlignVertical) >> 2;
		};
		var update_annotation_font_selected = function () {
			var a = this.annotations[this.annotation_selected_index];

			this.nodes.edit_font_selector.disabled = false;
			this.nodes.edit_font_selector.selectedIndex = a.get_font();
		};
		var update_annotation_font_size_selected = function () {
			var a = this.annotations[this.annotation_selected_index];

			this.nodes.edit_font_size_selector.disabled = false;
			this.nodes.edit_font_size_selector.selectedIndex = a.get_font_size();
		};
		var update_annotation_text_decoration_selected = function () {
			var a = this.annotations[this.annotation_selected_index];

			this.nodes.edit_decoration_selector.disabled = false;
			this.nodes.edit_decoration_selector.selectedIndex = a.get_text_decoration();
		};
		var update_annotation_font_bold_selected = function () {
			var a = this.annotations[this.annotation_selected_index];

			this.nodes.edit_font_bold_check.disabled = false;
			this.nodes.edit_font_bold_check.checked = a.get_font_bold();
		};
		var update_annotation_font_italic_selected = function () {
			var a = this.annotations[this.annotation_selected_index];

			this.nodes.edit_font_italic_check.disabled = false;
			this.nodes.edit_font_italic_check.checked = a.get_font_italic();
		};

		var clear_annotation_lines = function () {
			var i;

			// Clear, move into previous
			if (this.annotations.length > 0) {
				// Remove nodes of every annotation
				set_no_annotations_selected.call(this);
				for (i = 0; i < this.annotations.length; ++i) {
					this.annotations[i].remove_nodes();
				}
				this.annotations = [];
				this.annotation_text_suffix_previous = this.annotation_text_suffix;
			}

			// Show
			set_empty_links_visible.call(this, true);
		};
		var load_annotation_lines = function (lines, lines_before, remaining, tag) {
			if (!this.visible) return;

			var a_len_pre = this.annotations.length,
				sz = null,
				i, j, a, t, can_size;

			this.annotation_text_suffix = remaining;

			if (lines.length === 0) {
				clear_annotation_lines.call(this);
			}
			else {
				// Completely remove previous
				clear_previous_annotations.call(this);

				// Hide
				set_empty_links_visible.call(this, false);

				// Create new annotations
				for (i = 0; i < lines.length; ++i) {
					if (i < a_len_pre) {
						a = this.annotations[i];
						a.set_text_before(lines_before[i]);
					}
					else {
						a = add_new_annotation.call(this, lines_before[i], true);
					}
				}

				// Select
				if (this.annotation_selected_index < 0 || this.annotation_selected_index >= i) {
					set_annotation_selected.call(this, i - 1, true);
				}

				// Remove extras
				while (i < this.annotations.length) {
					remove_annotation.call(this, i, false);
				}

				// Tag and text
				i = 0;
				if (tag !== null && (t = Annotator.process_tag(tag)) !== null) {
					// Load from state
					j = Math.min(this.annotations.length, t.annotations.length);
					for (; i < j; ++i) {
						this.annotations[i].load_from_state(t.annotations[i], lines[i]);
					}
				}

				// Text and default size
				can_size = (this.image.size.width > 0 && this.image.size.height > 0);
				if (!can_size) this.update_annotation_sizes_on_image_load = [];
				for (; i < lines.length; ++i) {
					a = this.annotations[i];
					a.set_text(a.escape_text(lines[i]));
					if (i >= a_len_pre) {
						if (can_size) {
							// Default position
							if (sz === null) sz = get_annotation_default_size.call(this, false);

							a.set_font_size(sz.font_size_index);
							a.set_annotation_rect(sz.x, sz.y, sz.width, sz.height);
						}
						else {
							this.update_annotation_sizes_on_image_load.push(a);
						}
					}
				}

				// Re-bound
				rebound_annotations.call(this);
			}

			// Update
			update_annotation_full_text.call(this, false);
		};
		var update_annotation_full_text = function (omit_tag) {
			if (!omit_tag && (this.image.size.width <= 0 || this.image.size.height <= 0 || !this.visible)) return;

			var text = "",
				i, a, t;

			for (i = 0; i < this.annotations.length; ++i) {
				if (i > 0) text += "\n";
				a = this.annotations[i];
				if ((t = a.get_text_before()) !== null) {
					text += t;
					text += "\n";
				}
				text += ">";
				if (a.text_display.length > 0 && a.text_display[0] === ">") text += " ";
				text += a.text_display;
			}

			if (this.annotation_text_suffix !== null) {
				if (i > 0) text += "\n";
				text += this.annotation_text_suffix;
				++i;
			}

			// Tag
			if (this.annotations.length > 0 && !omit_tag) {
				if (i > 0) text += "\n";
				text += Annotator.generate_tag(this.image, this.annotations, this.annotation_text_suffix);
			}

			// Set text
			this.qr_controller.set_text(text);
		};

		var on_movable_mousedown = function (event) {
			if (this.move_event_list.length > 0) {
				remove_event_listeners(this.move_event_list);
				this.move_event_list = [];
			}
			add_event_listener(this.move_event_list, document, "mousemove", on_movable_document_mousemove.bind(this), true);
			add_event_listener(this.move_event_list, document, "mouseup", on_movable_document_mouseup.bind(this), true);
		};
		var on_movable_document_mousemove = function (event) {
			// Potentially moving
			if (this.qr_resize_timer === null) {
				this.qr_resize_timer = setTimeout(on_resize_delay_timeout.bind(this), 100);
				update_size.call(this);
			}
			else {
				this.qr_resized_during_timer = true;
			}
		};
		var on_movable_document_mouseup = function (event) {
			// Clear events
			if (this.qr_resize_timer !== null) {
				clearTimeout(this.qr_resize_timer);
				this.qr_resize_timer = null;
				this.qr_resized_during_timer = false;
			}

			remove_event_listeners(this.move_event_list);
			this.move_event_list = [];

			update_size.call(this);
		};
		var on_resize_delay_timeout = function () {
			this.qr_resize_timer = null;
			if (this.qr_resized_during_timer) {
				this.qr_resized_during_timer = false;
				update_size.call(this);
			}
		};
		var on_window_resize = function (event) {
			if (this.qr_container !== null) {
				if (this.qr_resize_timer === null) {
					this.qr_resize_timer = setTimeout(on_resize_delay_timeout.bind(this), 100);
					update_size.call(this);
				}
				else {
					this.qr_resized_during_timer = true;
				}
			}
		};

		var on_image_load = function (node, event) {
			if (this.image_load_poll !== null) {
				// Update image size
				this.image.size.width = node.naturalWidth;
				this.image.size.height = node.naturalHeight;
				style.add_class(node, "iex_ae_preview_image_visible");

				update_preview_size.call(this);
				rebound_annotations.call(this);

				if (this.update_annotation_sizes_on_image_load !== null) {
					apply_default_size_to_annotations.call(this, this.update_annotation_sizes_on_image_load);
					this.update_annotation_sizes_on_image_load = null;
				}

				update_annotation_full_text.call(this, false);
			}

			clear_image_events.call(this);

			style.add_class(node, "iex_ae_preview_image_loaded");
		};
		var on_image_error = function (node, event) {
			clear_image_events.call(this);

			style.remove_class(node, "iex_ae_preview_image_visible");
			style.remove_class(node, "iex_ae_preview_image_loaded");
			style.add_class(node, "iex_ae_preview_image_error");

			this.nodes.image_error.textContent = "Image failed to load";
		};
		var on_image_abort = function (node) {
		};
		var on_image_status_poll = function (node) {
			if (node.naturalWidth > 0 && node.naturalHeight > 0) {
				clearInterval(this.image_load_poll);
				this.image_load_poll = null;

				// Update image size
				this.image.size.width = node.naturalWidth;
				this.image.size.height = node.naturalHeight;
				style.add_class(node, "iex_ae_preview_image_visible");

				update_preview_size.call(this);
				rebound_annotations.call(this);

				if (this.update_annotation_sizes_on_image_load !== null) {
					apply_default_size_to_annotations.call(this, this.update_annotation_sizes_on_image_load);
					this.update_annotation_sizes_on_image_load = null;
				}

				update_annotation_full_text.call(this, false);
			}
		};

		var on_container_mouseover = function (event) {
			show_preview_scrollbars.call(this);
		};
		var on_container_mouseout = function (event) {
			hide_preview_scrollbars.call(this);
		};
		var on_container_mousewheel = function (event) {
			if (this.preview_move_events.length === 0) {
				// Get direction
				var delta = (event.wheelDelta || -event.detail || 0),
					new_zoom;

				if (delta > 0) {
					if (this.cpreview_zoom < 4) {
						new_zoom = this.cpreview_zoom + 0.5;
					}
					else {
						new_zoom = this.cpreview_zoom * 2;
					}
					if (new_zoom > 32) new_zoom = 32;
				}
				else {
					if (this.cpreview_zoom <= 4) {
						new_zoom = this.cpreview_zoom - 0.5;
					}
					else {
						new_zoom = this.cpreview_zoom / 2;
					}
					if (new_zoom < 1) new_zoom = 1;
				}

				if (new_zoom !== this.cpreview_zoom) {
					change_zoom_level.call(this, new_zoom, true, event.clientX, event.clientY);
				}
			}

			// Stop
			return stop_event(event);
		};
		var on_container_mousedown = function (event) {
			var button = get_event_mouse_button(event),
				display_size = this.cpreview.display,
				inner_size = this.cpreview.size,
				w, h;

			// Stop
			if (button !== 1 && button !== 2) {
				return stop_event(event);
			}

			// Else, start dragging
			w = (inner_size.width - display_size.width);
			h = (inner_size.height - display_size.height);
			if (w > 0) w = 1.0 / w;
			else w = 0;
			if (h > 0) h = 1.0 / h;
			else h = 0;

			this.preview_move_speed[0] = w;
			this.preview_move_speed[1] = h;
			this.preview_move_origin[0] = event.clientX;
			this.preview_move_origin[1] = event.clientY;
			this.preview_move_offset[0] = this.cpreview.offset_x;
			this.preview_move_offset[1] = this.cpreview.offset_y;
			remove_event_listeners(this.preview_move_events);
			this.preview_move_events = [];
			add_event_listener(this.preview_move_events, document, "mousemove", on_conatiner_move_document_mousemove.bind(this), true);
			add_event_listener(this.preview_move_events, document, "mouseup", on_conatiner_move_document_mouseup.bind(this), true);

			// Stop
			return stop_event(event);
		};
		var on_container_contextmenu = function (event) {
			return stop_event(event);
		};
		var on_conatiner_move_document_mousemove = function (event) {
			var xo, yo;

			xo = this.preview_move_offset[0] + (this.preview_move_origin[0] - event.clientX) * this.preview_move_speed[0];
			yo = this.preview_move_offset[1] + (this.preview_move_origin[1] - event.clientY) * this.preview_move_speed[1];

			if (xo < 0) xo = 0;
			else if (xo > 1) xo = 1;

			if (yo < 0) yo = 0;
			else if (yo > 1) yo = 1;

			this.cpreview.set_offset(xo, yo);

			update_preview_scrollbar_positions.call(this);
		};
		var on_conatiner_move_document_mouseup = function (event) {
			on_conatiner_move_document_mousemove.call(this, event);

			remove_event_listeners(this.preview_move_events);
			this.preview_move_events = [];
		};

		var on_color_index_select = function (index, event) {
			if (this.annotation_selected_index >= 0) {
				var a = this.annotations[this.annotation_selected_index];
				a.set_color_index_selected(index);
				update_annotation_color_index_selected.call(this);
				update_annotation_color_selected.call(this);
			}
		};
		var on_color_select = function (index, event) {
			if (this.annotation_selected_index >= 0) {
				var a = this.annotations[this.annotation_selected_index];
				a.set_color(a.get_color_index_selected(), index);
				update_annotation_color_selected.call(this);
				update_annotation_full_text.call(this, false);
			}
		};
		var on_new_note_empty_click = function (event) {
			on_new_note_click.call(this, event);
		};
		var on_new_note_click = function (event) {
			add_new_annotation.call(this);
		};
		var on_font_select = function (node, event) {
			if (this.annotation_selected_index >= 0) {
				var a = this.annotations[this.annotation_selected_index];
				a.set_font(parseInt(node.value, 10));
				update_annotation_full_text.call(this, false);
			}
		};
		var on_font_scale_select = function (node, event) {
			if (this.annotation_selected_index >= 0) {
				var a = this.annotations[this.annotation_selected_index];
				a.set_font_size(parseInt(node.value, 10));
				update_annotation_full_text.call(this, false);
			}
		};
		var on_font_bold_change = function (node, event) {
			if (this.annotation_selected_index >= 0) {
				var a = this.annotations[this.annotation_selected_index];
				a.set_font_bold(node.checked);
				update_annotation_full_text.call(this, false);
			}
		};
		var on_font_italic_change = function (node, event) {
			if (this.annotation_selected_index >= 0) {
				var a = this.annotations[this.annotation_selected_index];
				a.set_font_italic(node.checked);
				update_annotation_full_text.call(this, false);
			}
		};
		var on_halign_select = function (node, event) {
			if (this.annotation_selected_index >= 0) {
				var a = this.annotations[this.annotation_selected_index],
					align = a.get_align();

				align = (align & ~Annotation.AlignHorizontal) | parseInt(node.value, 10);

				a.set_align(align);
				update_annotation_full_text.call(this, false);
			}
		};
		var on_valign_select = function (node, event) {
			if (this.annotation_selected_index >= 0) {
				var a = this.annotations[this.annotation_selected_index],
					align = a.get_align();

				align = (align & ~Annotation.AlignVertical) | parseInt(node.value, 10);

				a.set_align(align);
				update_annotation_full_text.call(this, false);
			}
		};
		var on_text_decoration_select = function (node, event) {
			if (this.annotation_selected_index >= 0) {
				var a = this.annotations[this.annotation_selected_index];
				a.set_text_decoration(parseInt(node.value, 10));
				update_annotation_full_text.call(this, false);
			}
		};
		var on_restore_annotations_click = function (event) {
			if (this.annotations_previous !== this.annotations && this.annotations_previous.length > 0) {
				// Hide links
				set_empty_links_visible.call(this, false);

				// Clear
				var i;
				for (i = 0; i < this.annotations.length; ++i) {
					this.annotations[i].destroy();
				}

				// Restore
				this.annotations = this.annotations_previous;
				for (i = 0; i < this.annotations.length; ++i) {
					this.annotations[i].add_to(this.nodes.annotation_container, this.nodes.edit_container);
				}
				this.annotation_text_suffix = this.annotation_text_suffix_previous;
				this.annotation_text_suffix_previous = null;

				// Select
				set_annotation_selected.call(this, 0, true);

				// Update
				update_annotation_full_text.call(this, false);
			}
		};

		var on_annotation_event = {
			select: function (annotation) {
				if (!annotation.is_selected()) {
					set_annotation_selected.call(this, annotation.index, false);
				}
			},
			align: function (annotation) {
				var a = annotation.get_align(),
					ha = a & Annotation.AlignHorizontal;

				a &= ~Annotation.AlignHorizontal;

				if (ha === Annotation.AlignCenter) ha = Annotation.AlignRight;
				else if (ha === Annotation.AlignRight) ha = Annotation.AlignLeft;
				else if (ha === Annotation.AlignLeft) ha = Annotation.AlignJustify;
				else ha = Annotation.AlignCenter;

				annotation.set_align(a | ha);
				if (annotation.is_selected()) {
					update_annotation_align_selected.call(this);
				}

				update_annotation_full_text.call(this, false);
			},
			color: function (annotation) {
				annotation.set_color_index_selected((annotation.get_color_index_selected() + 1) % annotation.get_color_count());
				if (annotation.is_selected()) {
					update_annotation_color_index_selected.call(this);
					update_annotation_color_selected.call(this);
				}

				update_annotation_full_text.call(this, false);
			},
			font: function (annotation) {
				if (annotation.get_font_bold()) {
					if (annotation.get_font_italic()) {
						annotation.set_font_bold(false);
					}
					else {
						annotation.set_font_italic(true);
					}
				}
				else {
					if (annotation.get_font_italic()) {
						annotation.set_font_italic(false);
					}
					else {
						annotation.set_font_bold(true);
					}
				}

				if (annotation.is_selected()) {
					update_annotation_font_bold_selected.call(this);
					update_annotation_font_italic_selected.call(this);
				}

				update_annotation_full_text.call(this, false);
			},
			font_size: function (annotation) {
				var size = annotation.get_font_size();
				size = (size + 1) % (Annotation.font_size_settings.step_duration * Annotation.font_size_settings.step_count);

				annotation.set_font_size(size);

				if (annotation.is_selected()) {
					update_annotation_font_size_selected.call(this);
				}

				update_annotation_full_text.call(this, false);
			},
			move_up: function (annotation) {
				if (annotation.index > 0) {
					var annotation_index_old = annotation.index,
						annotation_index_new = annotation_index_old - 1,
						other = this.annotations[annotation_index_new],
						b = annotation.get_text_before();

					annotation.insert_before(other);

					annotation.set_index(annotation_index_new);
					other.set_index(annotation_index_old);
					this.annotations.splice(annotation_index_old, 1);
					this.annotations.splice(annotation_index_new, 0, annotation);

					annotation.set_text_before(other.get_text_before());
					other.set_text_before(b);

					if (this.annotation_selected_index === annotation_index_old) {
						this.annotation_selected_index = annotation_index_new;
					}
					else if (this.annotation_selected_index === annotation_index_new) {
						this.annotation_selected_index = annotation_index_old;
					}

					update_annotation_full_text.call(this, false);
				}
			},
			move_down: function (annotation) {
				var index_max = this.annotations.length - 1,
					other, annotation_index_new, annotation_index_old, b;

				if (annotation.index < index_max) {
					annotation_index_old = annotation.index;
					annotation_index_new = annotation_index_old + 1;
					other = this.annotations[annotation_index_new];
					b = annotation.get_text_before();

					if (annotation_index_old === index_max - 1) {
						annotation.add_to(this.nodes.annotation_container, this.nodes.edit_container);
					}
					else {
						annotation.insert_before(this.annotations[annotation_index_new + 1]);
					}

					annotation.set_index(annotation_index_new);
					other.set_index(annotation_index_old);
					this.annotations.splice(annotation_index_old, 1);
					this.annotations.splice(annotation_index_new, 0, annotation);

					annotation.set_text_before(other.get_text_before());
					other.set_text_before(b);

					if (this.annotation_selected_index === annotation_index_old) {
						this.annotation_selected_index = annotation_index_new;
					}
					else if (this.annotation_selected_index === annotation_index_new) {
						this.annotation_selected_index = annotation_index_old;
					}

					update_annotation_full_text.call(this, false);
				}
			},
			remove: function (annotation) {
				remove_annotation.call(this, annotation.index, true);
			},
			// text_input: function (annotation) {},
			text_change: function (/*annotation*/) {
				update_annotation_full_text.call(this, false);
			},
			rect_changed: function (/*annotation*/) {
				update_annotation_full_text.call(this, false);
			},
		};



		AnnotationEditor.prototype = {
			constructor: AnnotationEditor,

			set_qr_container: function (qr_container) {
				if (this.qr_container !== qr_container) {
					if (this.qr_container !== null) {
						remove_nodes.call(this);
						remove_qr_events.call(this);
					}

					this.qr_container = qr_container;

					if (this.qr_container !== null) {
						create_nodes.call(this);
						setup_qr_events.call(this);
					}
				}
			},
			set_file: function (file, url, mode, extra) {
				// Set image
				set_image.call(this, url, mode, extra);
			},
			show: function () {
				if (this.qr_container === null || this.visible) return;

				this.visible = true;
				style.add_class(this.nodes.container, "iex_annotation_editor_visible");
				update_size.call(this);
				//this.load_annotations_from_text(this.qr_controller.get_text());
			},
			hide: function () {
				if (this.qr_container === null || !this.visible) return;

				this.visible = false;
				style.remove_class(this.nodes.container, "iex_annotation_editor_visible");
				update_annotation_full_text.call(this, true);
				clear_annotation_lines.call(this);
			},
			is_visible: function () {
				return this.visible;
			},
			load_annotations_from_text: function (text) {
				var lines = [],
					lines_before = [],
					tag = null,
					m;

				// Find lines
				while ((m = re_load_annotations.exec(text)) !== null) {
					text = text.substr(m.index + m[0].length);

					lines.push(m[2].trim());
					lines_before.push(m[1] === undefined ? null : m[1]);

					if (m[3].length === 0) {
						text = null;
						break;
					}
				}

				// Find tag
				if (text !== null && (m = re_load_tag.exec(text)) !== null) {
					// Load tag
					text = (m[1] === undefined ? null : m[1]);
					tag = m[3];
				}

				// Load
				load_annotation_lines.call(this, lines, lines_before, text, tag);
			},

			get_image_scale: function () {
				return this.preview_scale;
			},
			get_image_size: function () {
				return this.image.size;
			},
			on_annotation_event: function (annotation, event_name) {
				var fn = on_annotation_event[event_name];
				if (fn !== undefined) {
					fn.call(this, annotation);
				}
			},
		};



		return AnnotationEditor;

	})();

	// Setup objects
	style = new Style();
	api = new API();
	sync = new Sync();
	settings = new Settings();
	hotkey_manager = new HotkeyManager();

	// Execute once page type is detected
	api.on("page_type_detected", function (event) {

		if (event.page_type == "board" || event.page_type == "thread" || event.page_type == "catalog") {
			// Settings
			settings.setup();

			// Insert stylesheet
			style.insert_stylesheet();

			// Create new hover
			hover = new Hover();
			hover.start();

			// Create image hover manager
			image_hover = new ImageHover(hover);
			settings.on_ready(image_hover.start.bind(image_hover));

			// Create image link modifier
			file_link = new FileLink();
			settings.on_ready(file_link.start.bind(file_link));
			settings.on_ready(function () {
				// Check if enabled
				if (settings.values.annotations.enabled) {
					// Insert stylesheet
					style.insert_stylesheet_annotations();

					// Annotator
					var annotator = new Annotator();
					settings.on_ready(annotator.start.bind(annotator, image_hover, file_link));
					if (settings.values.annotations.editor) {
						// Quick reply controller
						new QuickReplyController();
					}
				}
			});
		}
		else if (event.page_type == "image" || event.page_type == "video") {
			// Settings
			settings.setup();

			// File view
			file_view = new FileView();
			file_view.on("image", function (image) {
				// Check if enabled
				if (settings.values.annotations.enabled_standalone) {
					// Check url
					var d = Annotator.check_url();
					if (d !== null) {
						// Insert stylesheet
						style.insert_stylesheet_annotations();
						// Create overlay
						Annotator.create_image_overlay(image, d);
					}
				}
			});
			settings.on_ready(file_view.start.bind(file_view));
		}

	});
	api.setup();



})();