<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>tomsg webclient</title>
<script>
var PROTOCOL_VERSION=3;

// Note: message id's are considered strings in this code. This is to not have to deal with full 64-bit integers.

var sock=null,negotiated_version=false,username=null;
var messagecache=new Map();  // msgid => [user,message]
var roomlist=[":console"];
var currentroom=":console";
var currentreply=null;  // [msgid, table row] or null
var roomlogs=new Map([[":console",[]]]);  // see drawRoomEntry for entry types

var commandlist=[
	{cmd:"register",usage:"/register <username> <password>"},
	{cmd:"login",usage:"/login <username> <password>"},
	{cmd:"createroom",usage:"/createroom"},
	{cmd:"invite",usage:"/invite <username>  (adds to current room)"},
	{cmd:"leave",usage:"/leave  (leaves the current room)"},
	{cmd:"shrug",usage:"/shrug  (sends '¯\\_(ツ)_/¯')"},
	{cmd:"help",usage:"/help  (show list of commands)"}
];

var uniqid=(function(){
	var id=1;
	return function uniqid(){
		return id++;
	};
})();

function now(){
	return new Date().getTime();
}

var net_callbacks={};

function net_send(msg,cb){
	var id=uniqid()+"";
	sock.send(id+" "+msg);
	console.log("Sent '"+id+" "+msg+"'");
	net_callbacks[id]=cb;
}

function fancySplit(text,freespace){
	var obj={
		word: [],
		rest: [],
	};
	var cursor=0,idx;
	if(freespace){
		while(cursor<text.length&&text[cursor]==" ")cursor++;
	}
	while(cursor<text.length){
		idx=text.indexOf(" ",cursor);
		if(idx==-1)idx=text.length;
		obj.word.push(text.slice(cursor,idx));
		obj.rest.push(text.slice(cursor));
		if(idx==text.length)break;
		cursor=idx+1;
		if(freespace){
			while(cursor<text.length&&text[cursor]==" ")cursor++;
		} else if(cursor==text.length){
			obj.word.push("");
			obj.rest.push("");
		}
	}
	return obj;
}

function leftPad(str,num,chr){
	str=str+"";
	if(str.length>=num)return str;
	var pad="";
	while(pad.length<num-str.length)pad+=chr;
	return pad+str;
}

function formatTime(date){
	return leftPad(date.getHours(),2,"0")+":"+
	       leftPad(date.getMinutes(),2,"0")+":"+
	       leftPad(date.getSeconds(),2,"0");
}

function net_historyCollectionCallback(id,list,count,cb, item,err){
	if(err){
		cb(null,err);
	} else {
		list.push(item);
		if(list.length==count){
			cb(list);
		} else {
			net_callbacks[id]=net_historyCollectionCallback.bind(this,id,list,count,cb);
		}
	}
}

function resetState() {
	if(sock)sock.close();
	negotiated_version=false;
	username=null;
	messagecache.clear();
	net_callbacks={};
	cancelReply();
}

function reconnect(){
	resetState();

	var url;
	if(location.hostname!="")url="wss://"+location.hostname;
	else url="ws://localhost";
	sock=new WebSocket(url+":29546");
	updateStatus();
	updateRoomList();
	sock.addEventListener("message",function(msg){
		var spl=fancySplit(msg.data,false);
		console.log(msg.data);
		var id=spl.word[0],type=spl.word[1];
		if(id=="_push"){
			if(type=="message"){
				var r=spl.word[2],u=spl.word[3],t=new Date(+spl.word[4]/1000);
				var msgid=spl.word[5],replyid=spl.word[6];
				var text=spl.rest[7];
				messagecache.set(msgid,[u,text]);
				addRoomEntry(r,"message",[u,t,msgid,replyid,text]);
			} else if(type=="invite"){
				var r=spl.word[2],inviter=spl.word[3];
				roomlist.push(r);
				roomlogs.set(r,[]);
				if(inviter==username)addRoomEntry(r,"notice",[now(),"You created this room in another session"]);
				else addRoomEntry(r,"notice",[now(),"User '"+inviter+"' invited you to this room"]);
				fetchRoomHistory(r);
				updateRoomList();
			} else if(type=="join"){
				var r=spl.word[2],u=spl.word[3];
				addRoomEntry(r,"notice",[now(),"User '"+u+"' joined this room"]);
			} else if(type=="leave"){
				var r=spl.word[2],u=spl.word[3];
				if(u==username)uiLeaveRoom(r);
				else addRoomEntry(r,"notice",[now(),"User '"+u+"' left the room"]);
			} else if(type=="ping"){
				// Do nothing
			} else if(type=="online"){
				// TODO: implement nicklist and do something with this
			} else {
				alert("Unknown push message type '"+type+"'!");
			}
		} else if(net_callbacks[id]){
			var fn=net_callbacks[id];
			delete net_callbacks[id];
			var obj;
			if(type=="ok")fn(true);
			else if(type=="error")fn(null,spl.rest[2]);
			else if(type=="number")fn(spl.rest[2]);  // Note: no int parse!
			else if(type=="name")fn(spl.rest[2]);
			else if(type=="list")fn(spl.word.slice(3));
			else if(type=="history"){
				var count=+spl.word[2];
				net_callbacks[id]=net_historyCollectionCallback.bind(this,id,[],count,fn);
			} else if(type=="history_message")fn([spl.word[4],+spl.word[5]/1000,spl.word[6],spl.word[7],spl.rest[8]]);
			else if(type=="message")fn([spl.word[3],+spl.word[4]/1000,spl.word[5],spl.word[6],spl.rest[7]]);
			else alert("Unknown server response message type '"+type+"'!");
		} else {
			alert("No callback for server message id '"+id+"'!");
		}
	});
	sock.addEventListener("open",function(){
		updateStatus();
		net_send("version "+PROTOCOL_VERSION,function(ok,err){
			if(err){
				var msg="Server version incompatible (we need protocol version "+
							PROTOCOL_VERSION+"): "+err;
				addRoomEntry(":console","error",[now(),msg]);
			} else {
				negotiated_version=true;
				updateStatus();
			}
		});
	});
	sock.addEventListener("close",function(ev){
		updateStatus();
		if(ev.code!=1000){
			setTimeout(function(){
				reconnect();
			},1000+Math.random()*1000);
		}
	});
}

function fetchRoomList(){
	net_send("list_rooms",function(list,err){
		if(err){
			addRoomEntry(":console","error",[now(),"Unable to fetch room list: "+err]);
		} else {
			var roomid;
			for(roomid in roomlogs.keys()){
				if(roomid!=":console")roomlogs.delete(roomid);
			}
			roomlist=[":console"];
			var i;
			for(i=0;i<list.length;i++){
				roomlist.push(list[i]);
				roomlogs.set(list[i],[]);
				fetchRoomHistory(list[i]);
			}
			updateRoomList();
		}
	});
}

function fetchRoomHistory(roomid){
	net_send("history "+roomid+" 10",function(list,err){
		if(err){
			addRoomEntry(roomid,"error",[now(),"Unable to fetch history: "+err]);
		} else {
			var i;
			for(i=0;i<list.length;i++){
				addRoomEntry(roomid,"message",list[i]);
			}
		}
	});
}

function getMessage(msgid,callback){
	if(messagecache.has(msgid)){
		callback(null,messagecache.get(msgid));
		return;
	}

	net_send("get_message "+msgid,function(item,err){
		if(err)callback(err,null);
		else callback(null,item);
	});
}

function updateStatus(){
	var str=null;
	if(!sock||sock.readyState>=2){
		str="not connected";
	} else if(sock.readyState==0){
		str="connecting...";
	} else if(!negotiated_version){
		str="versioning...";
	} else if(username){
		str="u: "+username;
	} else {
		str="connected";
	}
	var div=document.getElementById("status");
	if(!div.firstChild)div.appendChild(document.createTextNode(""));
	div.firstChild.nodeValue=str;
}

function updateRoomList(){
	var rldiv=document.getElementById("roomlist");
	var ch=rldiv.children,len=ch.length;
	var i;
	for(i=len-1;i>=0;i--)rldiv.removeChild(ch[i]);

	var div;
	for(i=0;i<roomlist.length;i++){
		div=document.createElement("div");
		div.classList.add("roomlistitem");
		if(roomlist[i]==currentroom)div.classList.add("selected");
		div.addEventListener("click",function(roomid){
			currentroom=roomid;
			updateRoomList();
			drawRoom(currentroom);
			document.getElementById("roominput").focus();
		}.bind(this,roomlist[i]));
		div.appendChild(document.createTextNode(roomlist[i]));
		rldiv.appendChild(div);
	}
}

function uiLeaveRoom(roomid){
	roomlist.splice(roomlist.indexOf(roomid),1);
	roomlogs.delete(roomid);
	var isCurrent=currentroom==roomid;
	if(isCurrent)currentroom=":console";
	updateRoomList();
	if(isCurrent)drawRoom(currentroom);
}

function drawRoom(roomid){
	var tbody=document.getElementById("roomlog");
	var ch=tbody.children,len=ch.length;
	var i;
	for(i=len-1;i>=0;i--)tbody.removeChild(ch[i]);

	var roomtitle=document.getElementById("roomtitle");
	if(!roomtitle.firstChild)roomtitle.appendChild(document.createTextNode(roomid));
	else roomtitle.firstChild.nodeValue=roomid;

	var tr,td;
	var items=roomlogs.get(roomid);
	if(!items){
		alert("drawRoom on nonexistent roomid '"+roomid+"'!");
		return;
	}
	for(i=Math.max(0,items.length-20);i<items.length;i++){
		drawRoomEntry(items[i][0],items[i][1]);
	}
}

function drawRoomEntry(type,args){
	var tbody=document.getElementById("roomlog");
	var tr=document.createElement("tr");
	var td0=document.createElement("td");
	var td1=document.createElement("td");
	var td2=document.createElement("td");
	var node0=document.createTextNode("");
	var node1=document.createTextNode("");
	var node2=document.createTextNode("");
	td0.appendChild(node0);
	td1.appendChild(node1);
	td2.appendChild(node2);
	tr.appendChild(td0);
	tr.appendChild(td1);
	tr.appendChild(td2);
	tbody.appendChild(tr);
	switch(type){
		case "message":  // [username, timestamp_ms, msgid, replyid, text]
			tr.classList.add("message");
			node0.nodeValue=formatTime(new Date(args[1]));
			node1.nodeValue="<"+args[0]+">";
			node2.nodeValue=args[4];
			if(args[3]!="-1"){
				var replyNode=document.createElement("span");
				replyNode.classList.add("reply");
				replyNode.classList.add("pending");
				getMessage(args[3],function(err,item){
					replyNode.classList.remove("pending");
					var text;
					if(err){
						replyNode.classList.add("failed");
						text="Failed to fetch original message!";
					} else {
						text="> <"+item[0]+"> "+item[4];
					}
					replyNode.appendChild(document.createTextNode(text));
				});
				td2.insertBefore(replyNode,node2);
				td2.insertBefore(document.createElement("br"),node2);
			}

			tr.addEventListener("click",function(){
				if(currentreply&&currentreply[0]==args[2])cancelReply();
				else startReply(args[2],tr);
			});
			break;

		case "error":  // [timestamp_ms, text]
			tr.classList.add("error");
			node0.nodeValue=formatTime(new Date(args[0]));
			node1.nodeValue="--";
			node2.nodeValue=args[1];
			break;

		case "notice":  // [timestamp_ms, text]
			tr.classList.add("notice");
			node0.nodeValue=formatTime(new Date(args[0]));
			node1.nodeValue="--";
			node2.nodeValue=args[1];
			break;

		default:
			alert("drawRoomEntry on unknown type '"+type+"'!");
			break;
	}
	tr.scrollIntoView();
}

function addRoomEntry(roomid,type,args){
	if(!roomlogs.get(roomid)){
		alert("addRoomEntry on nonexistent roomid '"+roomid+"'!");
		return;
	}
	roomlogs.get(roomid).push([type,args]);
	if(roomid==currentroom){
		drawRoomEntry(type,args);
	}
}

function cancelReply(){
	if(!currentreply)return;
	currentreply[1].classList.remove("replying");
	document.getElementById("roominput_td").classList.remove("replying");
	currentreply=null;
}

function startReply(msgid,tr){
	cancelReply();
	currentreply=[msgid,tr];
	tr.classList.add("replying");
	document.getElementById("roominput_td").classList.add("replying");
	document.getElementById("roominput").focus();
}

function showUsage(roomid,cmd){
	for(var i=0;i<commandlist.length;i++){
		if(commandlist[i].cmd==cmd){
			addRoomEntry(roomid,"error",[now(),"Usage: "+commandlist[i].usage]);
			return;
		}
	}
	throw new Error("No usage registered for command '"+cnd+"'!");
}

function executeCommand(roomid,text){
	var spl=fancySplit(text,true);
	if(spl.word.length==0)return;
	var cmd=spl.word[0];
	var creds=null,othername;
	switch(cmd){
		case "register":
		case "login":
			if(spl.word.length<3){
				showUsage(roomid,cmd);
				break;
			}
			creds=[spl.word[1],spl.rest[2]];
			net_send(cmd+" "+creds[0]+" "+creds[1],function(ok,err){
				if(err){
					addRoomEntry(roomid,"error",[now(),"Unable to "+cmd+": "+err]);
				} else if(cmd=="register"){
					addRoomEntry(roomid,"notice",[now(),"Successfully registered user '"+creds[0]+"'"]);
				} else {
					username=creds[0];
					addRoomEntry(roomid,"notice",[now(),"Logged in as user '"+creds[0]+"'"]);
					updateStatus();
					fetchRoomList();
				}
			});
			break;

		case "createroom":
			if(spl.word.length!=1){
				showUsage(roomid,cmd);
				break;
			}
			if(!username){
				addRoomEntry(roomid,"error",[now(),"createroom: not logged in"]);
				break;
			}
			net_send("create_room",function(name,err){
				if(err){
					addRoomEntry(roomid,"error",[now(),"Unable to create a room: "+err]);
				} else {
					addRoomEntry(roomid,"notice",[now(),"Successfully created room '"+name+"'"]);
					fetchRoomList();
				}
			});
			break;

		case "invite":
			if(spl.word.length<2){
				showUsage(roomid,cmd);
				break;
			}
			if(!username){
				addRoomEntry(roomid,"error",[now(),"invite: not logged in"]);
				break;
			}
			if(!currentroom||currentroom[0]==":"){
				addRoomEntry(roomid,"error",[now(),"invite: cannot invite into room '"+currentroom+"'"]);
				break;
			}
			othername=spl.rest[1];
			net_send("invite "+currentroom+" "+othername,function(ok,err){
				if(err){
					addRoomEntry(roomid,"error",[now(),"Unable to invite '"+othername+"': "+err]);
				} else {
					addRoomEntry(roomid,"notice",[now(),"Successfully invited '"+othername+"'"]);
				}
			});
			break;

		case "leave":
			if(!username){
				addRoomEntry(roomid,"error",[now(),"invite: not logged in"]);
				break;
			}
			var roomtoleave=currentroom;
			if(!roomtoleave||roomtoleave[0]==":"){
				addRoomEntry(roomid,"error",[now(),"invite: cannot leave room '"+roomtoleave+"'"]);
				break;
			}
			net_send("leave_room "+roomtoleave,function(ok,err){
				if(err){
					addRoomEntry(roomid,"error",[now(),"Unable to leave this room: "+err]);
				} else {
					uiLeaveRoom(roomtoleave);
				}
			});
			break;

		case "shrug":
			sendMessage(roomid,"¯\\_(ツ)_/¯");
			break;

		case "help":
			for(var i=0;i<commandlist.length;i++){
				addRoomEntry(roomid,"notice",[now(),commandlist[i].usage]);
			}
			break;

		default:
			addRoomEntry(roomid,"error",[now(),"Unknown command '"+cmd+"'"]);
			break;
	}
}

function sendMessage(roomid,text){
	if(roomid[0]==":"){
		addRoomEntry(roomid,"error",[now(),"Cannot send a message here"]);
		return;
	}

	var sentAs=username,msg=text.replace(/\n/g,"");
	var replyid=currentreply?currentreply[0]:"-1";
	cancelReply();

	net_send("send "+roomid+" "+replyid+" "+msg,function(msgid,err){
		if(msgid!=null){
			msgid=msgid.toString();
			addRoomEntry(roomid,"message",[sentAs,new Date().getTime(),msgid,replyid,msg]);
			return;
		}
		addRoomEntry(roomid,"error",[now(),"Unable to send message: "+err]);
	});
}

function doSend(){
	var inputelem=document.getElementById("roominput");
	var text=inputelem.value;
	inputelem.value="";

	if(text.length==0)return;
	if(text.length>=2&&text[0]=="/"&&text[1]=="/"){
		sendMessage(currentroom,text.slice(1));
	} else if(text[0]=="/"){
		executeCommand(currentroom,text.slice(1));
	} else {
		sendMessage(currentroom,text);
	}
}

function doKeypress(ev){
	if(ev.keyCode==10||ev.keyCode==13)doSend();
}

window.addEventListener("load",function(){
	reconnect();
	drawRoom(currentroom);
	document.getElementById("roominput").focus();
});
</script>
<style>
html, body{
	margin:0;
	width:100%;
	height:100%;
}
body{
	background-color:#04040c;
	color:#eee;
	font-family:mononoki,meslo,monaco,monospace;
	font-size:11pt;
}
table{
	border-collapse:collapse;
}
#window{
	width:100%;
	height:100%;
}

.invisible{
	display:none;
}

/* SIDEBAR */
#sidebar{
	width:150px;
	border-right:1px #668 solid;
	vertical-align:top;
}

#branding{
	background-color:#223;
	padding:4px;
	height:22px;
	text-align:center;
	font-size:13pt;
}
#status{
	padding:2px;
	margin:8px 0px 10px 0px;
}
.roomlistitem{
	margin:2px 0px;
	padding:3px 2px;
	background-color:#223;
	cursor:pointer;
}
.roomlistitem:hover{
	background-color:#282833;
}
.roomlistitem.selected{
	background-color:#335;
	font-weight:bold;
}
.roomlistitem:hover{
	background-color:#445;
}

/* ROOM VIEW */
#room{
	width:100%;
	height:100%;
}
#room_td{
	padding:0;
}
#roomtitle{
	height:22px;
	padding:4px;
	background-color:#335;
	font-weight:bold;
}

#roomlog_td{
	vertical-align:top;
}
#roomlog_table{
	width:100%;
}
#roomlog{
	/* The scrolling-table hack */
	display:block;
	overflow-y:auto;
	height:calc(100vh - 63px);  /* TODO: figure out whether 63 is ALWAYS correct */
}
#roomlog > tr{
	vertical-align:top;
}
#roomlog > tr > td:first-child{
	width:80px;
	color:#666;
}
#roomlog > tr > td:nth-child(2){
	width:120px;
	padding-right:20px;
	text-align:right;
}
#roomlog > tr > td:last-child{
	white-space:pre-wrap;
}
#roomlog > tr.message{
	color:inherit;
	cursor:pointer;
}
#roomlog > tr.message:hover{
	background-color:#202030;
}
#roomlog > tr.replying{
	background-color:#090 !important;
}
#roomlog > tr.error{
	color:#f44;
}
#roomlog > tr.notice{
	color:#a8f;
}
#roomlog > tr > td > span.reply{
	color:#8f8;
	font-style:italic;
}
#roomlog > tr > td > span.reply.pending:after{
	content:"(...)";
}
#roomlog > tr > td > span.reply.failed{
	color:#f22;
	font-style:normal;
}

#roombar_tr{
	height:30px;
	border-top:1px #668 solid;
}
#roominput_td{
	position:relative;
	vertical-align:top;
	padding:0px 1px 0px 10px;
}
#roominput_td.replying{
	background-color:#090;
}
#roominput{
	position:absolute;
	background-color:rgba(0,0,0,0);
	padding:0;
	border-width:0;
	width:calc(100% - 11px);
	height:100%;
	color:white;
	font-family:inherit;
	font-size:inherit;
}
#roomsend_td{
	position:relative;
	width:40px;
	vertical-align:top;
	padding:0;
}
#roomsend{
	position:absolute;
	width:100%;
	height:100%;
	background-color:#335;
	border-width:0px;
	color:#ccc;
	font-family:inherit;
	font-size:inherit;
	cursor:pointer;
}
#roomsend:hover{
	background-color:#445;
}
</style>
</head>
<body>
<table id="window"><tbody><tr>
	<td id="sidebar">
		<div id="branding">TOMSG</div>
		<div id="status">&nbsp;</div>
		<div id="roomlist"></div>
	</td>
	<td id="room_td">
		<table id="room"><tbody>
			<tr id="roomtitle_tr"><td id="roomtitle" colspan="2"></td></tr>
			<tr id="roomlog_tr"><td id="roomlog_td" colspan="2">
				<table id="roomlog_table"><tbody id="roomlog"></tbody></table>
			</td></tr>
			<tr id="roombar_tr">
				<td id="roominput_td"><input type="text" id="roominput" onkeypress="doKeypress(event)"></td>
				<td id="roomsend_td">
					<button id="roomsend" onclick="doSend()">
<svg width="27px" height="21px" version="1.1" viewBox="10 15 100 85" style="margin-top: 6px; margin-bottom: -2px">
 <g fill="#fff">
  <path transform="translate(-.61382 -.30051)" d="m14.614 20.301 76 30-72-5z"/>
  <path d="m14 80 76-30-72 5z"/>
 </g>
</svg>
					</button>
				</td>
			</tr>
		</tbody></table>
	</td>
</tr></tbody></table>
</body>
</html>