/* dcwebui.js */
//IIFEMODE:;(function() {
	
"use strict";

var SENTINEL_PASSWORD = "************";
var CHAT_SCROLLBACK_LIMIT = 50; // Once over 2x $limit, the first $limit will be trimmed off the list

var $ = (document.querySelectorAll ? 
	function(s) {
		var r = document.querySelectorAll(s);
		return (s[0] === '#' && r.length === 1) ? r[0] : r;
	} : 
	function(s) {
		// i'm not writing a selector engine...
		if (! s.length) return [];
		if (s[0] === '#') {
			return document.getElementById(s.slice(1));
		} else if (s[0] === '.') {
			return document.getElementsByClassName(s.slice(1));
		} else {
			return document.getElementsByTagName(s);
		}
	}
);

var nmdc_escape = function(str) {
	return (
		(''+str).length
			? (''+str).replace(/&/g,'&amp;').replace(/\|/g,'&#124;').replace(/\$/g,'&#36;')
			: ' '
	);
};

var hesc = function(s) {
	var filter = {
		'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&#34;', '\'': '&#39;'
	};
	return s.toString().replace(/[&<>'"]/g, function(s) { return filter[s]; });
};

var fmtBytes = function(b) {
	if (b == 0) {
		return '(nothing)';
	}
	
	var k = 1024;
	var sizes = [' B', ' KiB', ' MiB', ' GiB', ' TiB'];
	var i = Math.floor(Math.log(bytes) / Math.log(k));
	return parseFloat((bytes / Math.pow(k, i)).toFixed(3)) + sizes[i];
};


var urldesc = function(s) {
	return decodeURIComponent(s.replace(/\+/g, " "));
}

var linkify = function(str) {
	// n.b. str is already hesced
	return (str
		.replace(
			/(https?:\/\/[^\s<]+)/g,
			"<a target='_blank' rel=\"noreferrer\" href=\"$1\">$1</a>"
		)
		.replace(
			/magnet:\?.+dn=([^\< ]+)/g,
			function(match, m1) { return "<a href=\"" + match + "\">[MAGNET] " + urldesc(m1) + "</a>"; }
		)
	);
};

var sanitise = function(s) {
	return linkify(hesc(s));
};

var textContent = function($el) {
	if ($el.textContent) return $el.textContent;
	if ($el.innerText) return $el.innerText;
	return "";
};

var negmod = function(l, r) {
	var ret = l % r;
	if (l < 0) {
		return ret + r;
	} else {
		return ret;
	}
};

// @ref https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
var b64 = function(str) {
    return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
        return String.fromCharCode('0x' + p1);
    })).replace(/=/g, '');
}

// https://gist.github.com/eligrey/1276030
var appendInnerHTML = function($el, html) {
	var child = document.createElement("span");
	child.innerHTML = html;
	
	var node;
	while ((node = child.firstChild)) {
		$el.appendChild(node);
	}
};

// http://stackoverflow.com/a/5598797
function getOffsetLeft( elem ) {
    var offsetLeft = 0;
	do {
		if (!isNaN(elem.offsetLeft)) {
			offsetLeft += elem.offsetLeft;
		}
	} while (elem = elem.offsetParent);
	return offsetLeft;
}

function getOffsetTop( elem ) {
    var offsetTop = 0;
	do {
		if (!isNaN(elem.offsetTop)) {
			offsetTop += elem.offsetTop;
		}
	} while (elem = elem.offsetParent);
	return offsetTop;
}

/* */

var date_format = function(d, format) {
	var pad = function(s) {
		return (s < 10) ? '0'+s : ''+s ;
	};
	
	var ret = format;
	ret = ret.replace(/H/g, pad(d.getHours()));
	ret = ret.replace(/i/g, pad(d.getMinutes()));
	ret = ret.replace(/s/g, pad(d.getSeconds()));
	ret = ret.replace(/Y/g, d.getFullYear());
	ret = ret.replace(/m/g, pad(d.getMonth() + 1));
	ret = ret.replace(/d/g, pad(d.getDate()));
	return ret;
};

/* */

var notify = function(title, body) {
	if (!("Notification" in window)) {
		return; // not supported by browser
	}
	
	switch (window.Notification.permission) {
		case "granted": {
			new Notification(title, {
				body: body,
				icon: DCWEBUI_CONF.extern + "/favicon.ico"
			});
		} break;
		
		case "denied": return;
		
		default: {
			// Clarify permission and retry
			Notification.requestPermission(function(permission) {
				notify(title, body);
			});
		} break;
	}
};

/* Tab writers */ 

var write = function(tab) {
	var $tab = $('#inner-'+tab);
	return {
		'cls': function() {
			$tab.innerHTML = '';
			return this;
		},
		'scroll': function() {
			$tab.scrollTop = $tab.scrollHeight;
			return this;
		},
		'raw': function(s) {
			appendInnerHTML($tab, s);
			return this.scroll();
		},
		'c': function(c, s) {
			return this.raw('<span class="'+c+'">'+s+'</span>');
		},
		'time': function() {
			return this.raw('<span class="tx-time">['+ date_format(new Date(), timestamp_formats[timestamp_format_index])+"]</span> ");
		},
		'system': function(s) {
			return this.time().c('tx-sys', sanitise(s)).raw('<br/>');
		},
		'pubnick': function(u) {
			return this.raw('<span class="tx-user" ondblclick="switchToPM(\''+hesc(u)+'\');return false;">&lt;'+hesc(u)+'&gt;</span>');
		},
		'pub': function(u, s) {
			return this.time().
				pubnick(u).raw('&nbsp;').
				c('tx-chat', sanitise(s)).raw('<br/>');
		}
	};
};

var write_system_message_in_all_pm_tabs = function(system_message) {
	for (var k in pm_tabs) {
		writerFor(k).system(system_message);
	}
};

/* Userlist */ 

var switchToPM = function(u) {
	writerFor(u);          // create
	tab_set(pm_tabs[u]);   // switch
	writerFor(u).scroll(); // scroll
};

var userMenu = function(u, ev) {
	
	usermenu.hide();
	usermenu = new MenuList(ev.target);
	usermenu.add("Send private message...", function() {
		switchToPM(u);
	});
	// Usercommands
	for (var i = 0; i < user_usercommands.length; i++) (function(i) {
		var raw = user_usercommands[i].raw;
		usermenu.add(user_usercommands[i].title, function() {
			
			var message = usercommand_process( raw .replace(/%\[nick\]/g, u) );
			sock.emit('raw', {message: message});
		});
	})(i);
	
	// Show
	usermenu.show();
	
	//
	ev.preventDefault();
	return false;
};

var userlist = {
	'add': function(u) {
		if (this.has(u)) return;
		
		var userlists = $(".userlist");
		for (var l = 0, e = userlists.length; l !== e; ++l) {
			var userlist = userlists[l];
			
			var to_add = document.createElement('li');
			to_add.className = "user-" + b64(u);
			to_add.innerHTML = hesc(u);
			
			to_add.onclick = function() { switchToPM(u); };
			to_add.oncontextmenu = function(ev) { return userMenu(u, ev); };
			
			var users = userlist.children;
			var cmp   = hesc(u).toUpperCase();
			
			var found = false;
			for (var i = 0; i < users.length; i++) {
				if ((''+users[i].innerHTML).toUpperCase() > cmp) {
					userlist.insertBefore(to_add, users[i]);
					found = true;
					break;
				}
			}
			if (! found) {
				userlist.appendChild(to_add);
			}
		}
		return this;
	},
	'del': function(u) {
		var userlists = $(".userlist");
		for (var l = 0, e = userlists.length; l !== e; ++l) {
			if (! userlists[l].children) continue;
			var userlist = userlists[l];
				
			var users = userlist.children;
			var cmp   = hesc(u).toUpperCase();
			for (var i = 0; i < users.length; i++) {
				if ((''+users[i].innerHTML).toUpperCase() === cmp) {
					userlist.removeChild(users[i]);
					break;
				}
			}
		}
		return this;
	},
	'clear': function() {
		var userlists = $(".userlist");
		for (var i in userlists) {
			if (! userlists[i].children) continue;
			var userlist = userlists[i];
			
			var users = userlist.children;
			for (var j = users.length; j --> 0;) {
				userlist.removeChild(users[j]);
			}
		}
		return this;
	},
	'names': function() {
		var userlist = $(".userlist")[0].children;
		var ret = [];
		for (var i = 0, e = userlist.length; i < e; ++i) {
			ret.push( textContent(userlist[i]) );
		}
		return ret;
	},
	'has': function(u) {
		return $(".user-" + b64(u)).length !== 0; /* there are two - large and non-large */
	},
	'count': function() {
		return $(".userlist")[0].children.length;
	},
	'setInfo': function(nick, props) {
		var baseClass = "user-" + b64(nick);
		var $el = $("." + baseClass);
		var prop_str = [];
		if (props.Description.length > 0) {
			prop_str.push(props.Description);
		}
		if (props.Email.length > 0) {
			prop_str.push(props.Email);
		}
		if (props.ClientTag.length > 0) {
			prop_str.push(props.ClientTag + " " + props.ClientVersion);
		}
		prop_str.push("Sharing " + fmtBytes(props.ShareSize));
		
		for (var i = 0; i < $el.length; ++i) {
			$el[i].title = prop_str.join("\n");
			
			if (props.IsOperator) {
				$el[i].className = baseClass + " user-is-operator";
			} else {
				$el[i].className = baseClass; // remove op flag
			}
		}
	}
};

var submit = function() {
	var str = $("#chatbox").value;
	if (! str.length) return;

	if (hub_state === STATE_READY_FOR_LOGIN) {
		transition(1); // disables #chatbox
		
		hub_last_nick = str.split(":", 2)[0];
		
		var hub_pass = "";
		if (str.length > hub_last_nick.length) {
			hub_pass = str.substr(hub_last_nick.length + 1);
		}
		if (hub_pass === SENTINEL_PASSWORD) {
			// Probably not a real password. Attempt to load a better one from the saved state
			var cache = persistence_get("login");
			if (cache.indexOf(":") != -1) {
				hub_pass = cache.substr(cache.indexOf(":") + 1);
			}
		}
	
		persistence_set("login", hub_pass.length > 0 ? hub_last_nick+":"+hub_pass : hub_last_nick);
		
		sock.emit('hello', {'nick' : hub_last_nick, 'pass' : hub_pass});
		write("tab-main").system("Connecting...");
		
	} else if (hub_state === STATE_ACTIVE) {
		if (pm_target !== false) {
			sock.emit('priv', {'user': pm_target, 'message': str});
			writerFor(pm_target).pub(hub_last_nick, str );
		} else {
			sock.emit('pub', {'message' : str});
		}
		
		chat_scrollback.push( str );
		chat_scrollback_index = -1;
		if (chat_scrollback.length > (2*CHAT_SCROLLBACK_LIMIT)) {
			chat_scrollback = chat_scrollback.slice(CHAT_SCROLLBACK_LIMIT);
		}
		
	} else {
		write("tab-main").system("Invalid internal state.");
	}
	
	$("#chatbox").value = '';
};

/* page visibility */

var pagevis_currently_visible = true;

var pagevis_setup = function(fnActive, fnInactive) {
	
	var h, vc; 
	if (typeof document.hidden !== "undefined") {
		h = "hidden";
		vc = "visibilitychange";
	} else if (typeof document.msHidden !== "undefined") {
		h = "msHidden";
		vc = "msvisibilitychange";
	} else if (typeof document.webkitHidden !== "undefined") {
		h = "webkitHidden";
		vc = "webkitvisibilitychange";
	}

	if (typeof document[h] === "undefined") {
		// Browser doesn't support Page Visibility API, so behave as if the page is always visible
		pagevis_currently_visible = true
		
	} else {		
		document.addEventListener(vc, function() {
			if (document[h]) {
				pagevis_currently_visible = false;
				fnInactive();
			} else {
				pagevis_currently_visible = true;
				fnActive();
			}
		});
	}
}

/* tabs */

/**
 * Switch active tab
 * 
 * @param {String} tab  Full tab ID (e.g. tab-main, tab-users, tab-ext-???)
 */
var tab_set = function(tab) {
	
	var tabs = $(".tabpane");
	for (var i in tabs) {
		try {
			tabs[i].style.display = (tabs[i].id === tab ? 'block' : 'none');
		} catch (e) {};
	}
	var tabitems = $(".tabitem");
	for (var i in tabitems) {
		try {
			// Update UNREAD/SELECTED flags for the target
			var was_unread = (tabitems[i].className.indexOf("unread") !== -1);
			var is_target = (tabitems[i].getAttribute('data-tab') === tab);
			
			var is_still_unread = (was_unread && !is_target);
			
			tabitems[i].className = "tabitem" + (is_target ? ' selected' : '') + (is_still_unread ? ' unread' : '');
		} catch (e) {};
	}
	
	pm_target = false;
	for (var i in pm_tabs) {
		if (pm_tabs[i] === tab) {
			pm_target = i;
			break;
		}
	}
	
	if (tab == "tab-main" && pagevis_currently_visible) {
		mainchat_unread_count = 0;
	}
	
	updateTitle();
	
	write(tab).scroll();
	$("#chatbox").focus();
	last_tab = tab;
	
};

var tab_new = function(id, name) {
	appendInnerHTML($("#bar"),
		' <div class="tabitem" data-tab="tab-ext-'+id+'" id="tabitem-tab-ext-'+id+'">'+
			'<span class="tab-label">'+
				hesc(name)+
			'</span> '+
			'<a class="tab-closer" data-tab="tab-ext-'+id+'">&times;</a>'+
		'</div> '
	);
	appendInnerHTML($("#extratabs"),
		' <div class="tabpane content placement-mid" id="tab-ext-'+id+'" style="display:none;">'+
			'<div class="content-inner" id="inner-tab-ext-'+id+'"></div>'+
		'</div>'
	);
	tab_addHandlers();
	return "tab-ext-"+id;
};

var tab_free = function(id) {
	if (id === "tab-main") return;

	// remove tab item and body
	var el = $("#tabitem-"+id);
	el.parentNode.removeChild(el);
	
	var el = $("#"+id);
	el.parentNode.removeChild(el);
	
	// clear from PM tabs
	for (var i in pm_tabs) {
		if (pm_tabs[i] === id) {
			// pm_tabs[i] = false;
			delete(pm_tabs[i]);
		}
	}
	
	// maybe clear the 'new pm' warning
	updateTitle();
	
	// don't leave us with no tab displayed
	if (last_tab === id) {
		tab_set("tab-main");
	}
};

var tab_addHandlers = function() {
	var tabitems = $(".tabitem");
	for (var i = 0; i < tabitems.length; i++) {
		if (! tabitems[i]) continue;
		
		tabitems[i].onclick = function(ev) {
			tab_set( this.getAttribute('data-tab') );
			
			// 360nobubble
			if (ev.stopPropagation) {
				ev.stopPropagation();
			} else {
				ev.cancelBubble = true; // oldIE
			}
			return false;
		};
	}
	
	var tabclosers = $(".tab-closer");
	for (var i = 0; i < tabclosers.length; i++) {
		if (! tabclosers[i]) continue;
		
		tabclosers[i].onclick = function(ev) {
			tab_free( this.getAttribute('data-tab') );
			
			// 360nobubble
			if (ev.stopPropagation) {
				ev.stopPropagation();
			} else {
				ev.cancelBubble = true; // oldIE
			}
			return false;
		};
	}
};

/* */

var maybeWriterFor = function(username) {
	if (! username in pm_tabs || ! pm_tabs[username]) {
		return null;
	}
	
	return write(pm_tabs[username]);
};

var writerFor = function(username) {
	var ret = maybeWriterFor(username);	
	if (ret !== null) {
		return ret; // found
	}
	
	// create new
	var tabid = tab_new(next_tabid++, username);
	pm_tabs[username] = tabid;
	return write(tabid);
};

/* */

var tabcomplete_state = '';

var tabcompletion_start = function(direction) {
	
	var cursor = $("#chatbox").value.replace(/^.*\s([^\s]+)$/, '$1');
	
	if (tabcomplete_state === '') {
		// new tab completion
		tabcomplete_state = cursor;
	}
	
	// Find all users who start with tabcomplete_state and retrieve the first
	//    one after cursor
	
	var users = userlist.names();
	var match = [];
	
	for (var i = 0, e = users.length; i !== e; ++i) {
		if (users[i].toUpperCase().indexOf(tabcomplete_state.toUpperCase()) === 0) {
			match.push(users[i]);
		}
	}
	
	if (match.length === 0) {
		// no matches
		return;
	}
	
	// Is cursor in the list?
	var cpos = -1;
	for (var i = 0, e = match.length; i !== e; ++i) {
		if (match[i] === cursor) {
			cpos = i; break;
		}
	}
	
	var targetName = match[negmod(i + direction, match.length)];
	
	// Replace in textbox
	
	var chatprefix = $("#chatbox").value.substr(0, $("#chatbox").value.length - cursor.length);
	
	$("#chatbox").value = chatprefix + targetName;
	$("#chatbox").focus();
};

var tabcompletion_inactive = function() {
	tabcomplete_state = '';
};

/* */ 

var MenuList = function(el) {
	this.el = el;
	
	this.div = document.createElement("div");
	this.div.classList.add("menu");
	this.div.style.position = "absolute";
	
	this.ul = document.createElement("ul");
	this.div.appendChild(this.ul);
	
	document.body.appendChild(this.div);
	
};
MenuList.prototype.clear = function() {
	while (this.ul.children.length) {
		this.ul.children[0].parentNode.removeChild( this.ul.children[0] );
	}
};
MenuList.prototype.add = function(txt, cb) {
	var li = document.createElement("li");
	li.innerHTML = txt;
	li.onclick = cb;
	this.ul.appendChild(li);
};
MenuList.prototype.show = function() {
	this.div.style.display = "block";
	
	this.div.style.top  = (getOffsetTop(this.el) + this.el.clientHeight)+"px";
	this.div.style.left = (getOffsetLeft(this.el) - 200 + this.el.clientWidth)+"px";
};
MenuList.prototype.hide = function() {
	this.div.style.display = "none";
};
MenuList.prototype.toggle = function() {
	// ES5 strict mode sets `this` to undefined
	(this.div.style.display === "block" ? this.hide : this.show).call(this);
};

/* */ 

var menu = new MenuList($("#menubutton"));

menu.reset = function() {
	this.clear();
	this.add(joinparts_getstr(),  toggle_joinparts);
	this.add(desktop_notifications_fmtstr(), desktop_notifications_toggle);
	this.add(warnonclose_fmtstr(), warnonclose_toggle);
	this.add(timestamp_display(), timestamp_toggle);
};

var usermenu = new MenuList(document.body);

/**
 * Process all substitutions in a usercommand. May prompt the user for input.
 * 
 * @param {String} str
 * @returns {String}
 */
var usercommand_process = function(str) {
	var message = str
		.replace(/%\[mynick\]/g, hub_last_nick)
	;
	var match = null;
	for(;;) {
		match = message.match(/%\[line:([^\]]*)\]/);
		if (match === null) break;

		var res = prompt(match[1]);
		if (res === null) return; //cancelled

		message =
			message.substr(0, match.index) +
			nmdc_escape(res) +
			message.substr(match.index + match[0].length)
		;
	}
	
	return message;
};

var user_usercommands = [];

/**
 * Called when a $UserCommand is recieved from the server. Register it in all
 *  relevant context menus.
 * 
 * @param {Object} data   Has properties type, context, title, raw
 * @returns {undefined}
 */
var process_usercommand = function(data) {
	switch(data.type) {
		case 0: { /* USERCOMMAND_TYPE_SEPARATOR */
			// ignore 
		} break;
		
		case 1: /* USERCOMMAND_TYPE_RAW */
		case 2:/* USERCOMMAND_TYPE_NICKLIMITED */
		{
			if (data.context & 1) { /* USERCOMMAND_CONTEXT_HUB */ 
				menu.add(data.title, function() {
					sock.emit('raw', {message: usercommand_process(data.raw)});
				});
			}
			
			if (data.context & 2) { /* USERCOMMAND_CONTEXT_USER */ 
				user_usercommands.push(data);
			}
		} break;
		
		case 255: { /* USERCOMMAND_TYPE_CLEARALL */
			if (data.context & 1) {
				menu.reset();
			}
			
			if (data.context & 2) {
				user_usercommands = []; // clear
			}
		} break;
	}
};

var joinparts_getstr = function() {
	return (show_joins ? "&#9745;" : "&#9744;") + " Show Joins/Parts";
};

var toggle_joinparts = function(ev) {
	var $el = ev.target || ev.srcElement;
	show_joins = ! show_joins;
	persistence_set("show_joins", show_joins);
	$el.innerHTML = joinparts_getstr();
};

/* */

var updateTitle = function() {
	var prefix = "";
	var unrTabs = $(".unread");
	if (unrTabs.length === 1 && unrTabs[0].getAttribute('data-tab') == "tab-main") {
		prefix = "[" + mainchat_unread_count + " NEW] "
	} else if (unrTabs.length > 0) {
		prefix = "[NEW PM] "
	}
	
	document.title = prefix + hub_hubname + "("+userlist.count()+") "
};

var sock = {};
var hub_state     = 0; // [disconnected, sent-nick, connected]
var hub_last_nick = '';
var hub_hubname   = DCWEBUI_CONF.title;

var pm_tabs = {}; // nick => tabid
var next_tabid = 1;
var pm_target = false;

var last_tab = "tab-main";

var show_joins = false;

var have_cleared_once = false;

var STATE_DISCONNECTED    = -1;
var STATE_READY_FOR_LOGIN = 0;
var STATE_CONNECTING      = 1;
var STATE_ACTIVE          = 2;

var chat_scrollback = [];
var chat_scrollback_index = -1;

var timestamp_formats = [ "H:i",     "H:i:s",   "Y-m-d H:i:s" ];
var timestamp_names =   [ "Minutes", "Seconds", "Full"        ];
var timestamp_format_index = 0;

var mainchat_unread_count = 0;

var should_warn_on_close = false;

var timestamp_display = function() {
	return "Timestamp format: " + timestamp_names[timestamp_format_index];
};

var timestamp_toggle = function(ev) {
	var $el = ev.target || ev.srcElement;	
	timestamp_format_index = (timestamp_format_index + 1) % timestamp_formats.length;
	persistence_set("timestamps", timestamp_format_index);	
	$el.innerHTML = timestamp_display();
};

var warnonclose_fmtstr = function() {
	return (should_warn_on_close ? "&#9745;" : "&#9744;") + " Confirm closing window";
};

var warnonclose_toggle = function(ev) {
	var $el = ev.target || ev.srcElement;	
	should_warn_on_close = !should_warn_on_close;
	persistence_set("warnonclose", should_warn_on_close);
	$el.innerHTML = warnonclose_fmtstr();
};

var desktop_notifications_enabled = false;

var desktop_notifications_fmtstr = function() {
	return (desktop_notifications_enabled ? "&#9745;" : "&#9744;") + " Show desktop popups";
};

var desktop_notifications_toggle = function(ev) {
	var $el = ev.target || ev.srcElement;	
	desktop_notifications_enabled = !desktop_notifications_enabled;
	persistence_set("notifications", desktop_notifications_enabled);
	
	if (desktop_notifications_enabled) {
		desktop_notifications_onEnable();
	}
	
	persistence_set("popups", desktop_notifications_enabled);
	
	$el.innerHTML = desktop_notifications_fmtstr();
};

var desktop_notifications_onEnable = function() {
	notify(hub_hubname, "Desktop popups enabled");
}

var scrollback_move = function(delta) {
	if (chat_scrollback.length === 0) {
		return; // no effect
	}
	
	if (chat_scrollback_index === -1) {
		chat_scrollback_index = chat_scrollback.length - 1; // always starts at most recent message
	} else {
		chat_scrollback_index += delta;
		
		// clamp
		if (chat_scrollback_index === -1) {
			chat_scrollback_index = 0;
		} else if (chat_scrollback_index === chat_scrollback.length) {
			chat_scrollback_index = chat_scrollback.length - 1;
		}
	}
	
	$("#chatbox").value = chat_scrollback[chat_scrollback_index];
};

/* */

var persistence_set = function(key, value) {
	if (window.localStorage) {
		window.localStorage[key] = JSON.stringify(value);
	}
};

var persistence_get = function(key, fallback) {
	try {
		return JSON.parse( window.localStorage[key] );
	} catch (ex) {
		return fallback;
	}
};

var transition = function(new_state) {
	hub_state = new_state;
	
	switch(new_state) {
		case STATE_DISCONNECTED: {
			userlist.clear();		
			$("#chatbox").disabled = true;
			$("#chatbox").value = ''; // clear
		} break;
		
		case STATE_READY_FOR_LOGIN: {
			userlist.clear();
			$("#chatbox").spellcheck = false;
			$("#chatbox").disabled = false;
			$("#chatbox").value = ''; // clear
		} break;
		
		case STATE_CONNECTING: {
			$("#chatbox").disabled = true;
		} break;
		
		case STATE_ACTIVE: {
			$("#chatbox").disabled = false;
			$("#chatbox").spellcheck = true;
		} break;
	}
};

var tab_is_visible = function(tabref) {
	return (last_tab === tabref && pagevis_currently_visible);
}

var tab_mark_unread = function(tabref) {
	if ($("#tabitem-"+tabref).className.indexOf('unread') === -1) {
		$("#tabitem-"+tabref).className += " unread";
		updateTitle();
	}
}

window.onload = function() {
	write("tab-main").system("Communicating with server...");
	
	show_joins = persistence_get("show_joins", false);
	
	document.title = DCWEBUI_CONF.title;
	
	// HTML event handlers
	
	$("#form-none").onsubmit = function(ev) {
		submit();
		
		// don't submit form
		ev.preventDefault();
		return false;
	};
	
	$("#chatbox").onkeydown = function(ev) {
		if (ev.keyCode === 9 /* Tab */) {
			tabcompletion_start( ev.shiftKey ? -1 : 1 );
			ev.preventDefault();
			return false;
				
		} else if (ev.keyCode == 38 /* ArrowUp */ && ev.ctrlKey) {
			scrollback_move(-1);
			ev.preventDefault();
			return false;
			
		} else if (ev.keyCode == 40 /* ArrowDown */ && ev.ctrlKey) {
			scrollback_move(1);
			ev.preventDefault();
			return false;
			
		} else {
			tabcompletion_inactive();
			chat_scrollback_index = -1; // clear
		}
	};
	
	window.onresize = function() {
		if (last_tab === "tab-users" && document.documentElement.clientWidth >= 590) {
			tab_set("tab-main");
		}
		
		menu.hide();
		usermenu.hide();
	};
	
	$("#menubutton").onclick = function(ev) {
		menu.toggle();
		ev.preventDefault();
		ev.stopPropagation();
		return false;
	};
	
	window.onclick = function() {
		menu.hide();
		usermenu.hide();
	};
	
	window.onbeforeunload = function (e) {
		if (should_warn_on_close && hub_state === STATE_ACTIVE) {
			// n.b. recent Firefox / Chrome don't display the custom message here.
			var confirmationMessage = "Still connected to the hub \"" + hub_hubname + "\".\nAre you sure you want to close?";
			e.returnValue = confirmationMessage;
			return confirmationMessage;
		}
		// else: ignore
	};
	
	timestamp_format_index = persistence_get("timestamps", 0);
	
	should_warn_on_close = persistence_get("warnonclose");
	
	desktop_notifications_enabled = persistence_get("popups");
	if (desktop_notifications_enabled) {
		// prompt for permissions
		desktop_notifications_onEnable();
	}
	
	menu.reset();
	
	tab_addHandlers();
	
	pagevis_setup(
		function() {
			// We've just become active
			// If the foreground is a PM window with UNREAD, mark it as read
			tab_set(last_tab);
		},
		function() {
			// We've just become inactive
			// TODO: marker lines
		}
	);
	
	// Hacks for WiiU user-agent
	
	if (navigator && navigator.userAgent && !!navigator.userAgent.match("Nintendo WiiU")) {
		document.body.className += " navigator-wiiu";
	}
	
	// Socket event handlers
	
	sock = io.connect(DCWEBUI_CONF.extern);
	sock.on('cls', function() {
		transition(STATE_READY_FOR_LOGIN);
		
		var pre_login = persistence_get("login", "");
		if (pre_login.indexOf(":") !== -1) {
			pre_login = pre_login.substr(0, pre_login.indexOf(":")) + ":" + SENTINEL_PASSWORD;
		}
		$("#chatbox").value = pre_login;
		
		if (have_cleared_once) {
			// re-log-in automatically			
			write("tab-main").system("Automatically reconnecting as \"" + pre_login + "\"...");
			submit();
		}
		
		if (!have_cleared_once) {
			write("tab-main").cls();
			userlist.clear();
			have_cleared_once = true; // don't re-clear, keep history
		}
	});
	sock.on('hubname', function(s) {
		write("tab-main").system("Now talking on "+s);
		hub_hubname = s;
		updateTitle();
	});
	sock.on('sys', function(data) {
		write("tab-main").system(data);
	});
	sock.on('raw', function(data) {
		write("tab-main").raw(data);
	});
	sock.on('pub', function(data) {
		write("tab-main").pub( data.user, data.message);
		
		if (! tab_is_visible("tab-main")) {
			tab_mark_unread("tab-main");
			mainchat_unread_count += 1;
			updateTitle();
		}
	});
	sock.on('priv', function(data) {
		writerFor(data.user).pub(data.user, data.message);
		if (! tab_is_visible(pm_tabs[data.user])) {
			// Got PM, but tab isn't focused
			
			tab_mark_unread( pm_tabs[data.user] );
			
			if (desktop_notifications_enabled) {
				notify("Message from " + data.user, data.message);
			}
		}
	});
	sock.on('hello', function() {
		transition(STATE_ACTIVE);
		write_system_message_in_all_pm_tabs("Reconnected.");
	});
	sock.on('part', function(u) {
		userlist.del(u.user);
		if (show_joins) {
			write("tab-main").system("*** Parts: "+u.user); // sanitised
		}
		
		var targetTab = maybeWriterFor(u.user);
		if (targetTab !== null) {
			targetTab.system("*** Parts: "+u.user);
		}
		
		updateTitle();
	});
	sock.on('join', function(u) {
		userlist.add(u.user);
		if (show_joins) {
			write("tab-main").system("*** Joins: "+u.user);
		}
		
		var targetTab = maybeWriterFor(u.user);
		if (targetTab !== null) {
			targetTab.system("*** Joins: "+u.user);
		}
		
		updateTitle();
	});
	sock.on('info', function(u) {
		try {
			var props = JSON.parse(u.message);
			userlist.setInfo(u.user, props);
		} catch (ex) {}
	});
	sock.on('close', function() {
		transition(STATE_DISCONNECTED);
		write("tab-main").system("Connection closed by remote host.");
		write_system_message_in_all_pm_tabs("Disconnected.");
	});
	sock.on('disconnect', function() {
		transition(STATE_DISCONNECTED);
		write("tab-main").system("Lost connection to the server.");
		write_system_message_in_all_pm_tabs("Disconnected.");
	});
	sock.on('usercommand', function(data) {
		process_usercommand(data);
	});
};

//IIFEMODE:})();
