let { e2ee } = await import(`./e2ee.js?r=${revision}`);
let { ws } = await import(`./ws.js?r=${revision}`);
let { ui } = await import(`./ui.js?r=${revision}`);
let { stream } = await import(`./stream.js?r=${revision}`);
let { listeners } = await import(`./listeners.js?r=${revision}`);

function log(string) {
	console.log(string);
}

export class HNSChat {
	constructor() {
		window.hnschat = this;
		
		try {
			this.varo = new Varo();
		}
		catch {}

		this.mobile = false;

		this.host = window.location.host;
		this.hash = window.location.hash.substring(1);
		this.page;
		this.data;

		this.ui = new ui(this);
		this.ws = new ws(this);
		this.listeners = new listeners(this);
		this.e2ee = new e2ee();

		this.streamURL = "https://media.hns.chat/stream";
		this.stream;

		this.isActive = true;

		this.keys;
		this.session;
		this.domain;
		this.domains;
		this.staked;
		this.conversation;
		this.settings;
		this.messages = [];
		this.seen = {};

		this.users;

		this.channels;
		this.pms;

		this.tab = "channels";

		this.timeFormat = "g:i A";
		this.dateFormat = "F jS, Y";

		this.gotChannels;
		this.gotPms;
		this.gotMentions;

		this.loadingMessages;
		this.replying;
		this.queued = [];

		this.typing;
		this.typingSent;
		this.typingDelay = 2;
		this.typingSendDelay = 1;
		this.lastTyped;
		this.typers = {};

		this.active = [];

		this.avatars = {};

		this.action = "\x01ACTION";

		this.commands = ["me", "shrug", "slap"];

		this.hasBob;

		this.init();
	}

	varoLoaded() {
		if (typeof this.varo !== "undefined") {
			return true;
		}
		return false
	}

	getPage() {
		let pathname = document.location.pathname;
		let match = pathname.match(/\/(?<page>.+?)(?:\/(?<data>.+)|$)/);
		if (match) {
			let groups = match.groups;

			if (groups.page) {
				this.page = groups.page;
			}
			if (groups.data) {
				this.data = groups.data;
			}
		}
		else {
			this.page = "chat";
		}
	}

	async api(data) {
		if (this.session) {
			data.session = this.session;
		}

		let output = new Promise(function(resolve) {
			$.post("/api", JSON.stringify(data), function(response){
				if (response) {
					let json = JSON.parse(response);
					resolve(json);
				}
			});
		});

		return await output;
	}

	time() {
		return Math.floor(Date.now() / 1000);
	}

	handlePush(init=false) {
		if (localStorage.handlePush) {
			try {
				let info = JSON.parse(localStorage.handlePush);
				
				localStorage.setItem("domain", info.domain);
				localStorage.setItem("conversation", info.conversation);

				if (!init) {
					if (this.domain !== info.domain) {
						this.changeDomain(info.domain);
					}
					this.changeConversation(info.conversation);
				}
			}
			catch {}
		}
		localStorage.removeItem("handlePush");
	}

	async init() {
		this.handlePush(true);
		
		this.getPage();
		switch (this.page) {
			case "sync":
				this.loadSync();
				break;

			case "buy":
				break;

			default:
				try {
					this.keys = JSON.parse(localStorage.keys);
				}
				catch {
					this.e2ee.generateKeys().then(r => {
						this.keys = r;
						localStorage.setItem("keys", JSON.stringify(this.keys));
					});
				}

				if (this.hash) {
					localStorage.setItem("hash", this.hash);
					history.pushState("", document.title, window.location.pathname + window.location.search);
				}
				else {
					this.hash = localStorage.hash;
				}

				this.mobile = localStorage.mobile || false;

				this.session = localStorage.session;
				this.domain = localStorage.domain;
				this.conversation = localStorage.conversation;

				try {
					this.settings = JSON.parse(localStorage.settings);
					this.loadSettings();
				}
				catch {
					this.settings = {};
				}

				this.firstLaunch = localStorage.firstLaunch;
				if (!this.firstLaunch) {
					localStorage.setItem("firstLaunch", this.time());
				}

				await this.startSession();
				await this.setPublicKey();
				await this.ws.connect();
				break;
		}

		switch (this.page) {
			case "chat":
				this.stream = new stream(this);
				this.ui.setConversationTab();
				this.ui.setupSync();
				this.ui.setupNotifications();
				break;
		}

		if (typeof bob3 != "undefined") {
			this.hasBob = true;
			this.ui.hasBob();
		}
	}

	loadSettings() {
		Object.keys(this.settings).forEach((setting, k) => {
			let value = this.settings[setting];
			switch (setting) {
				case "chatDisplayMode":
					this.ui.chatDisplayMode(value);
					break;

				default:
					this.ui.root.style.setProperty(`--${setting}`, value);
					break;
			}
		});
	}

	loadSync() {
		let hash = this.hash;
		let db64 = this.db64(hash);
		let json = JSON.parse(db64);
		this.session = json.session;
		localStorage.setItem("session", this.session);

		if (json.settings) {
			this.settings = json.settings;
			localStorage.setItem("settings", JSON.stringify(this.settings));
		}

		let data = {
			action: "getPublicKey"
		}
		this.api(data).then(r => {
			if (r.success) {
				let pubkey = JSON.parse(r.pubkey);
				this.e2ee.importKey(pubkey.x, pubkey.y, json.privkey).then(privkey => {
					this.keys = {
						privateKeyJwk: privkey,
						publicKeyJwk: pubkey
					};

					localStorage.setItem("keys", JSON.stringify(this.keys));
					this.ui.openURL("/");
				});
			}
		});
	}

	db64(str) {
		str = (str + '==='.slice((str.length + 3) % 4)).replace(/-/g, '+').replace(/_/g, '/');
		return atob(str);
	}

	b64(str) {
		str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
		return btoa(str);
	}

	syncLink() {
		let data = {
			session: this.session,
			privkey: this.keys.privateKeyJwk.d,
			settings: this.settings
		}

		let json = JSON.stringify(data);
		let encoded = this.b64(json);
		let link = "https://"+this.host+"/sync#"+encoded;

		return link;
	}

	regex(pattern, string) {
		return [...string.matchAll(pattern)];
	}

	rtrim(str, chr) {
	  let rgxtrim = (!chr) ? new RegExp('\\s+$') : new RegExp(chr+'+$');
	  return str.replace(rgxtrim, '');
	}

	replaceRange(s, start, end, substitute) {
		let before = s.substr(0, start);
		let after = s.substr(end, (s.length -end));

		return before+substitute+after;
	}

	sorted(array, by) {
		return array.sort((a, b) => a[by].localeCompare(b[by]));
	}

	isKey(e, key) {
		if (this.key(e) == key) {
			return true
		}
		return false;
	}

	key(e) {
		return e.which || e.keyCode;
	}

	keyName(e) {
		return e.key;
	}

	setPublicKey() {
		let data = {
			action: "setPublicKey",
			pubkey: JSON.stringify(this.keys.publicKeyJwk)
		};

		return this.api(data);
	}

	async startSession() {
		if (this.session) {
			return;
		}

		let data = {
			"action": "startSession"
		};

		return this.api(data).then(r => {
			this.session = r.session;
			localStorage.setItem("session", r.session);
		});
	}

	sendDomain() {
		if (!this.domain) {
			if (this.domains.length) {
				this.domain = this.domains[0].id;
				localStorage.setItem("domain", this.domain);
			}
			else {
				this.ui.openURL("/id");
				return;
			}
		}
		this.ws.send(`DOMAIN ${this.domain}`);
	}

	message(data) {
		let message = data.toString();
		let parsed = message.match(/(?<command>[A-Z]+)(\s(?<body>.+))?/);
		this.handle(parsed.groups);
	}

	async handle(parsed) {
		let command = parsed.command;
		let body = parsed.body;
		let push = false;

		if (body) {
			try {
				body = JSON.parse(body);

				for (var k in body) {
					try {
						body[k] = JSON.parse(body[k]);
					}
					catch {}

					for (var i in body[k]) {
						try {
							body[k][i] = JSON.parse(body[k][i]);
						}
						catch {}
					}
				}
			}
			catch {}
		}

		switch (command) {
			case "SUCCESS":
				switch (body.type) {
					case "ADDDOMAIN":
					case "ADDSLD":
						this.ui.enableButton(body.type);
						this.ui.handleSuccess(body);
						break;

					case "DELETEDOMAIN":
						this.ui.removeDomain(body.id);
						break;

					case "VERIFYDOMAIN":
						this.ui.handleSuccess(body);
						break;

					case "GETADDRESS":
						this.ui.paymentResponse(body);
						break;
				}
				break;

			case "ERROR":
				switch (body.type) {
					case "ADDDOMAIN":
						this.ui.errorResponse(body);
						break;

					case "ADDSLD":
						this.ui.enableButton(body.type);
						this.ui.errorResponse(body);
						break;

					case "MESSAGES":
						this.ui.setUserList();
						this.ui.messagesLoading(false);
						this.ui.setGatedView(body);
						this.ui.markEmptyIfNeeded();
						this.loadingMessages = false;
						break;

					case "DOMAIN":
						this.domain = null;
						this.sendDomain();
						break;

					case "PM":
						if (body.id) {
							let otherUser = this.otherUserFromPM(body.id);
							let queuedMessage = this.queuedMessage(otherUser.domain);
							if (queuedMessage) {
								this.ui.changeConversation(body.id);
								this.ui.close();
							}
						}
						else {
							this.ui.errorResponse(body);
						}
						break;

					case "GETADDRESS":
						this.ui.paymentResponse(body);
						break;
				}
				break;

			case "IDENTIFIED":
				if (this.page !== "sync") {
					if (body.seen) {
						this.seen = body.seen;
					}
					this.ws.send("DOMAINS");
				}
				break;

			case "DOMAINS":
				this.domains = body;
				this.ui.domains(this.domains);

				switch (this.page) {
					case "chat":
						this.sendDomain();
						break;

					case "id":
					case "invite":
						this.ws.send(`STAKED`);
						break;
				}
				break;

			case "STAKED":
				this.staked = body;
				this.ui.stakedDomains(body);
				break;

			case "DOMAIN":
				this.ui.updateDomainSelect();
				this.ws.send("USERS");
				break;

			case "USERS":
				$.each(body, (k, user) => {
					body[k].domain = body[k].domain.toString();
				});
				this.users = body;
				this.ws.send(`PING`);
				this.ws.send(`CHANNELS`);
				this.ws.send(`PMS`);
				break;

			case "USER":
				this.users = this.users.filter(u => {
					return u.id != body.id;
				});

				body.domain = body.domain.toString();
				this.users.push(body);

				if (body.id !== this.domain) {
					let pm = this.pmWithUser(body.id);
					if (pm) {
						this.makeSecret(pm);
					}
				}

				this.ui.setUserList();
				this.ui.updateConversations();
				break;

			case "CHANNELS":
				this.ui.clear("channels");
				this.channels = body;
				if (this.channels.length) {
					let sorted = this.channels.sort((a, b) => {
						if (("activity" in a) && !("activity" in b)) {
							return 1;
						}
						if (("activity" in b) && !("activity" in a)) {
							return -1;
						}
						if (!("activity" in a) && !("activity" in b)) {
							return 0;
						}
						return a.activity - b.activity;
					});
					$.each(sorted.reverse(), (k, channel) => {
						this.ui.conversation("channels", channel);
					});
				}
				this.ui.updateConversations();
				this.gotChannels = true;
				this.ws.send(`MENTIONS`);
				this.ready(true);
				break;

			case "CHANNEL":
				this.channels.push(body);
				this.ui.conversation("channels", body);
				this.ui.updateConversations();
				break;

			case "PMS":
				this.ui.clear("pms");
				this.pms = body;
				if (this.pms.length) {
					let sorted = this.pms.sort((a, b) => {
						return b.activity - a.activity;
					});
					$.each(sorted, (k, conversation) => {
						this.makeSecret(conversation).then((key) => {
							this.ui.conversation("pms", conversation);

							if (k == this.pms.length - 1) {
								this.ui.updateConversations();
								this.gotPms = true;
								this.ready(true);
							}
						});
					});
				}
				else {
					this.ui.updateConversations();
					this.gotPms = true;
					this.ready(true);
				}
				break;

			case "PM":
				this.pms.push(body);
				this.makeSecret(body).then((key) => {
					this.ui.conversation("pms", body);

					let otherUser = this.otherUserFromPM(body.id);
					let queuedMessage = this.queuedMessage(otherUser.domain);
					if (queuedMessage) {
						this.ui.changeConversation(body.id);
						this.ui.close();
					}
				});
				break;

			case "MESSAGES":
				this.ui.setUserList();
				this.ui.insertMessages(body, true).then(() => {
					this.ui.messagesLoading(false);

					if (body.at) {
						this.ui.scrollToMessage(body.at);
					}
					else if (body.after) {
						this.ui.backInPresent(body);
					}

					if (!this.isChannel(this.conversation)) {
						let otherUser = this.otherUserFromPM(this.conversation);
						let queuedMessage = this.queuedMessage(otherUser.domain);
						if (queuedMessage) {
							this.queued = this.queued.filter(q => {
								return q.domain !== queuedMessage.domain;
							});
							this.sendMessage(this.conversation, queuedMessage.message);
						}
					}
				});
				break;

			case "MESSAGE":
			case "NOTICE":
				delete this.typers[body.user];

				let c;
				if (this.isChannel(body.conversation)) {
					c = this.channelForID(body.conversation);
				}
				else {
					c = this.pmForID(body.conversation);
				}
				c.activity = body.time;

				this.ui.updateConversations();
				this.ui.moveConversationToTop(body.conversation);

				await this.decryptMessageIfNeeded(body.conversation, body).then(decrypted => {
					body.message = decrypted[0];
					if (body.p_message) {
						body.p_message = decrypted[1];
					}

					if (typeof body.message == "object") {
						if (body.message.message) {
							body.message.message = body.message.message.toString();
						}
						body.message = JSON.stringify(body.message);
					}
					if (typeof body.p_message == "object") {
						if (body.p_message.message) {
							body.p_message.message = body.p_message.message.toString();
						}
						body.p_message = JSON.stringify(body.p_message);
					}

					body.message = body.message.toString();
					if (body.p_message) {
						body.p_message = body.p_message.toString();
					}

					if (!this.ui.inThePast && body.conversation == this.conversation) {
						let data = {
							messages: [body]
						}
						this.ui.insertMessages(data);
						this.seen[this.conversation] = this.time();
						
						if (!this.isActive) {
							if (this.isChannel(body.conversation)) {
								if (this.mentionsMe(body.message)) {
									push = "mention";
								}
								else if (this.replyingToMe(body)) {
									push = "reply";
								}
							}
							else {
								push = "pm";
							}
						}
					}
					else {
						if (this.isChannel(body.conversation)) {
							if (this.mentionsMe(body.message)) {
								this.ui.markMention(body.conversation, true);
								push = "mention";
							}
							else if (this.replyingToMe(body)) {
								push = "reply";
							}
						}
						else {
							push = "pm";
						}
					}

					if (push) {
						if (body.user !== this.domain) {
							let name;

							switch (push) {
								case "pm":
									name = this.ui.toUnicode(this.userForID(body.user).domain);
									break;

								case "mention":
								case "reply":
									name = `${this.ui.toUnicode(this.userForID(body.user).domain)} - #${this.ui.toUnicode(c.name)}`;
									break;
							}

							let subtitle = this.ui.messageSummary(decrypted[0]);
							subtitle = this.ui.stripHTML(this.ui.replaceIds(subtitle)); 
							this.ui.sendNotification(name, subtitle, body.conversation);
						}
					}
				});
				break;

			case "DELETEMESSAGE":
				this.ui.deleteMessage(body.id);
				break;

			case "REACT":
				if (body.conversation == this.conversation) {
					let message = this.messages.filter(m => {
						return m.id == body.message;
					})[0];

					try {
						let json = JSON.parse(message.reactions);

						if (!Object.keys(json).includes(body.reaction)) {
							json[body.reaction] = [];
						}

						if (json[body.reaction].includes(body.user)) {
							let x = json[body.reaction].indexOf(body.user);
							delete json[body.reaction].splice(x, 1);

							if (!Object.keys(json[body.reaction]).length) {
								delete json[body.reaction];
							}
						}
						else {
							json[body.reaction].push(body.user);
						}

						message.reactions = JSON.stringify(json);
						this.ui.updateReactions(body.message);
						this.seen[this.conversation] = this.time();
					}
					catch {}
				}
				break;

			case "TYPING":
				this.typers[body.from] = {
					to: body.to,
					time: this.time()
				}
				break;

			case "MENTIONS":
				this.ui.updateMentions(body);
				this.gotMentions = true;
				this.ready(true);
				break;

			case "PONG":
				this.updateActiveUsers(body.active);
				//this.ui.checkVersion(body.version);
				break;

			case "PINMESSAGE":
				this.channelForID(body.conversation).pinned = body.id;
				if (this.conversation == body.conversation) {
					this.ui.setPinnedMessage();
				}
				break;

			case "STARTVIDEO":
				this.ui.muteAll();
				this.channelForID(body.conversation).video = true;
				this.channelForID(body.conversation).videoUsers = body.users;
				this.ui.showVideoIfNeeded();
				break;

			case "INVITEVIDEO":
				this.channelForID(body.conversation).videoSpeakers = body.speakers;
				this.ui.showVideoIfNeeded();
				break;

			case "JOINVIDEO":
				if (body.users) {
					this.channelForID(body.conversation).videoUsers = body.users;
				}
				if (body.watchers) {
					this.channelForID(body.conversation).videoWatchers = body.watchers;
				}
				if (body.speakers) {
					this.channelForID(body.conversation).videoSpeakers = body.speakers;
				}
				this.ui.showVideoIfNeeded();
				break;

			case "VIEWVIDEO":
				if (body.users) {
					this.channelForID(body.conversation).videoUsers = body.users;
				}
				if (body.watchers) {
					this.channelForID(body.conversation).videoWatchers = body.watchers;
					if (body.watchers.includes(this.domain)) {
						this.channelForID(body.conversation).watching = true;
					}
				}
				this.ui.showVideoIfNeeded();
				break;

			case "LEAVEVIDEO":
				if (body.users) {
					this.channelForID(body.conversation).videoUsers = body.users;
				}
				if (body.watchers) {
					this.channelForID(body.conversation).videoWatchers = body.watchers;
				}
				if (body.speakers) {
					this.channelForID(body.conversation).videoSpeakers = body.speakers;
				}
				this.ui.showVideoIfNeeded();
				break;

			case "ENDVIDEO":
				this.endVideo(body.conversation);
				break;

			case "MUTEVIDEO":
			case "MUTEAUDIO":
				let toggle;
				let user = this.channelForID(body.conversation).videoUsers[body.user];
				switch (command) {
					case "MUTEVIDEO":
						toggle = "toggleVideo";
						user.video = !user.video;
						break;

					case "MUTEAUDIO":
						toggle = "toggleAudio";
						user.audio = !user.audio;
						break;
				}
				this.ui.updateVideoUsers();
				if (body.user == this.domain) {
					this.ui.setMute(toggle, -1);
				}
				break;

			case "CONNECTED":
				if (this.userForID(body)) {
					this.userForID(body).active = true;
					this.ui.updateUserList();
					this.ui.updateConversations();
				}
				break;

			case "DISCONNECTED":
				if (this.userForID(body)) {
					this.userForID(body).active = false;
					this.ui.updateUserList();
					this.ui.updateConversations();
				}
				break;
		}
	}

	endAllVideo() {
		try {
			this.stream.close();
			$.each(this.channels, (k, c) => {
				this.endVideo(c.id);
			});
		}
		catch {};
	}

	endVideo(conversation) {
		this.channelForID(conversation).videoUsers = {};
		this.channelForID(conversation).videoWatchers = [];
		this.channelForID(conversation).videoSpeakers = [];
		this.channelForID(conversation).video = false;
		this.channelForID(conversation).watching = false;
		this.ui.showVideoIfNeeded();
	}

	currentVideoUsers() {
		return this.channelForID(this.conversation).videoUsers;
	}

	updateActiveUsers(active) {
		if (this.users) {
			let current = this.active;
			this.active = active;

			current.forEach(u => {
				if (!active.includes(u)) {
					let user = this.userForID(u);
					user.active = false;
				}
			});

			active.forEach(u => {
				let user = this.userForID(u);
				user.active = true;
			});

			this.ui.updateUserList();
			this.ui.updateConversations();
		}
	}

	otherUserFromPM(id) {
		let pm = this.pmForID(id);
		let otherUserID = this.otherUser(pm.users);
		let otherUser = this.userForID(otherUserID);
		return otherUser;
	}

	queuedMessage(domain) {
		return this.queued.filter(q => {
			return q.domain == domain;
		})[0];
	}

	ready(bool) {
		if (bool) {
			if (this.gotChannels && this.gotPms && this.gotMentions) {
				this.ui.changeMe(this.domain);
				this.ui.setLoading(false);
				this.ui.handleHash();
				this.changeConversation(this.conversation);
				this.changedConversation();
			}
		}
		else {
			this.gotChannels = false;
			this.gotPms = false;
			this.gotMentions = false;
			this.ui.setLoading(true);
		}
	}

	isChannel(name) {
		if (name) {
			if (name.toString().length == 8) {
				return true;
			}
		}
		return false;
	}

	channelForID(id) {
		return this.channels.filter(c => {
			return c.id == id;
		})[0];
	}

	channelForName(name) {
		return this.channels.filter(c => {
			return c.name == name;
		})[0];
	}

	stakedForName(name) {
		return this.staked.filter(c => {
			return c.name == name;
		})[0];
	}

	pmForID(id) {
		return this.pms.filter(c => {
			return c.id == id;
		})[0];
	}

	pmWithUser(id) {
		return this.pms.filter(c => {
			return c.users.includes(id);
		})[0];
	}

	otherUser(users) {
		return users.filter(u => {
			return u !== this.domain;
		})[0];
	}

	userForID(id) {
		if (this.users) {
			return this.users.filter(u => {
				return u.id == id;
			})[0];
		}
		return false;
	}

	userForName(name, active=false) {
		return this.users.filter(u => {
			return (u.domain == name || this.ui.toUnicode(u.domain) == name) && !u.locked && !u.deleted;
		})[0];
	}

	usersForConversation(id) {
		if (this.channels) {
			let conversation = this.channels.filter(c => {
				return c.id == id;
			})[0];

			let users = this.users.filter(u => {
				return !u.locked;
			});
			if (!conversation.public) {
				users = this.users.filter(u => {
					return !u.locked && u.tld == conversation.name || u.id == "n2sTWh5EZGI8xMQr";
				});
			}

			return users;
		}
		return [];
	}

	changeConversation(id) {
		let current = this.conversation;
		let change = id;

		if (!this.pmForID(id) && !this.channelForID(id)) {
			id = false;
		}

		if (!id) {
			change = this.channels[0].id;
		}

		if (current !== change) {
			this.conversation = change;
			localStorage.setItem("conversation", this.conversation);
			this.changedConversation();
		}
	}

	changedConversation() {
		this.ui.closeMenusIfNeeded();

		this.messages = [];
		this.ui.clear("input");
		this.ui.clear("messages");
		this.ui.setInThePast(false);
		this.ui.messagesLoading(true);
		this.ui.emptyUserList();
		this.ui.searchUsers(false);
		this.ui.clearSelection();
		this.ui.updateTypingView();
		this.getMessages();

		if (this.isChannel(this.conversation)) {
			this.tab = "channels";
		}
		else {
			this.tab = "pms";
		}

		this.ui.setConversationTab();

		this.ui.setActiveConversation();
		this.ui.markUnread(this.conversation, false);
		this.ui.markMention(this.conversation, false);
		this.ui.updateInputBar();

		this.ui.showVideoIfNeeded();

		this.seen[this.conversation] = this.time();
		this.ws.send(`CHANGEDCONVERSATION ${this.conversation}`);
	}

	getMessage(id) {
		let data = {
			action: "getMessage",
			domain: this.domain,
			id: id
		};

		return this.api(data);
	}

	getMessages(options={}) {
		if (this.loadingMessages) {
			return;
		}

		this.loadingMessages = true;
		
		let data = {
			conversation: this.conversation,
		};
		let merged = {...data,...options};

		this.ws.send(`MESSAGES ${JSON.stringify(merged)}`);
	}

	async makeSecret(pm) {
		let output = new Promise(resolve => {
			let otherUser = this.otherUserFromPM(pm.id);
			let otherKey = otherUser.pubkey;

			this.e2ee.deriveKey(otherKey, this.keys.privateKeyJwk).then(key => {
				otherUser.sharedkey = key;
				resolve(key);
			});
		});

		return await output;
	}

	async decryptedBody(conversation, message) {
		let output = new Promise(resolve => {
			let pm = this.pmForID(conversation);
			let user = this.userForID(this.otherUser(pm.users));

			this.e2ee.decryptMessage(message, user.sharedkey, conversation).then(decrypted => {
				resolve(decrypted.trim());
			});
		});

		return await output;
	}

	async decryptMessageIfNeeded(conversation, message) {
		let body = new Promise(resolve => {
			if (this.isChannel(conversation)) {
				resolve(message.message);
			}
			else {
				this.decryptedBody(conversation, message.message).then(decrypted => {
					resolve(decrypted);
				});
			}
		});

		let replyBody = new Promise(resolve => {
			if (message.p_message) {
				if (this.isChannel(conversation)) {
					resolve(message.p_message);
				}
				else {
					this.decryptedBody(conversation, message.p_message).then(decrypted => {
						resolve(decrypted);
					});
				}
			}
			else {
				resolve();
			}
		});

		let prepared = await Promise.all([body, replyBody]);
		return prepared;
	}

	async encryptedBody(conversation, message) {
		let output = new Promise(resolve => {
			let pm = this.pmForID(conversation);
			let user = this.userForID(this.otherUser(pm.users));

			this.e2ee.encryptMessage(message, user.sharedkey, conversation).then(encrypted => {
				resolve(encrypted.trim());
			});
		});

		return await output;
	}

	async encryptMessageIfNeeded(conversation, message) {
		let output = new Promise(resolve => {
			if (this.isChannel(conversation)) {
				resolve(message);
			}
			else {
				this.encryptedBody(conversation, message).then(encrypted => {
					resolve(encrypted);
				});
			}
		});

		return await output;
	}

	changeDomain(domain) {
		this.domain = domain;
		localStorage.setItem("domain", domain);

		if (this.page == "chat") {
			this.ready(false);
			this.ws.send(`DOMAIN ${domain}`);
		}
	}

	usersInMessage(message) {
		let matches = this.regex(/\@(?<name>[^ \x00]+?)\//gm, message);
		return matches;
	}

	channelsInMessage(message) {
		let matches = this.regex(/\#(?<name>[^ \x00]+?)(?:\s|$)/gm, message);
		return matches;
	}

	replaceCompletions(text) {
		let output = text;

		while (this.usersInMessage(output).length) {
			let users = this.usersInMessage(output);
			let result = users[0];

			let name = result.groups.name;
			let start = result.index;
			let end = (start + name.length + 1 + 1);
			
			let match = this.users.filter(u => {
				return this.ui.toUnicode(u.domain) == name && !u.locked;
			});

			let replace;
			if (match.length) {
				let id = match[0].id;
				replace = `@${id}`;
			}
			else {
				replace = `@\x00${name}/`;
			}
			output = this.replaceRange(output, start, end, replace);
		}

		while (this.channelsInMessage(output).length) {
			let channels = this.channelsInMessage(output);
			let result = channels[0];

			let name = result.groups.name;
			let start = result.index;
			let end = (start + name.length + 1);
			
			let match = this.channels.filter(c => {
				return this.ui.toUnicode(c.name) == name;
			});

			let replace;
			if (match.length) {
				let id = match[0].id;
				replace = `@${id}`;
			}
			else {
				replace = `#\x00${name}`;
			}
			output = this.replaceRange(output, start, end, replace);
		}

		return output;
	}

	sendTyping() {
		let input = $("textarea#message").val();

		if (!this.typing || !input || (input && input[0] == "/")) {
			return;
		}

		let send = true;
		if (this.typingSent) {
			let diff = this.time() - this.typingSent;

			if (diff < this.typingSendDelay) {
				send = false;
			}
		}

		if (send) {
			this.typingSent = this.time();

			let data = {
				from: this.domain,
				to: this.conversation
			}

			this.ws.send(`TYPING ${JSON.stringify(data)}`);
		}
	}

	updateTypingStatus() {
		if (this.lastTyped) {
			if ((this.time() - this.lastTyped) > this.typingDelay) {
				this.typing = false;
				this.lastTyped = false;
			}
			else if ($("textarea#message").is(":focus") && $("textarea#message").val() !== "") {
				this.typing = true;
			}
		}
	}

	sendMessage(conversation, message) {
		if (!message.trim().length) {
			return;
		}

		this.encryptMessageIfNeeded(conversation, message).then(encrypted => {
			let data = {
				conversation: conversation,
				message: encrypted
			}

			if (this.replying) {
				data.replying = this.replying.message;
			}

			this.typing = false;
			this.ws.send(`MESSAGE ${JSON.stringify(data)}`);

			this.replying = null;
			this.ui.updateReplying();
		});
	}

	async sendPayment(address, amount) {
		const wallet = await bob3.connect();

		if (!amount) {
			return { message: "Please enter an amount." };
		}

		try {
			const send = await wallet.send(address, amount);
			return send;
		}
		catch (error) {
			return error;
		}
	}

	async upload(data, attachment) {
		let output = new Promise(resolve => {
			$.ajax({
		        url: `${window.location.origin}/upload`,
		        type: "POST",
		        data: data,
		        cache: false,
		        contentType: false,
		        processData: false,
		        beforeSend: (e) => {},
		        xhr: () => {
		            let p = $.ajaxSettings.xhr();
		            p.upload.onprogress = () => {}
					return p;
				},
				success: (response) => {
					let json = JSON.parse(response);

					resolve(json);
				}
		    });
		});

		return await output;
	}

	deleteAttachment(id) {
		let data = {
			action: "deleteAttachment",
			id: id
		};

		return this.api(data);
	}

	mentionsMe(message) {
		if (message.includes(`@${this.domain}`)) {
			return true;
		}
		return false;
	}

	replyingToMe(message) {
		if (message.p_user && message.p_user == this.domain) {
			return true;
		}
		return false;
	}
}

new HNSChat();