//this is to share the websocket across tabs / windows
//note safari and android chrome has no support for this
import storage from 'kv-storage-polyfill';
import { create_deferred } from './util';
let request_ID = 0;
let ready = create_deferred();
let closing = create_deferred();
let promises = new Map();
let user = null;
let stores;
let w;
async function create_ws(otp) {
	const token = user?.token || await storage.get('token');
	const carts = await storage.get('carts');
	let shit = [];
	if(token) {shit.push('token=' + token)};
	if(otp) {shit.push('otp=' + otp)};
	if((token || otp) && Array.isArray(carts)) {shit.push(...carts.map(c => 'cart=' + c))};
	let ws = new WebSocket(`ws${location.protocol === 'https:' ? 's' : ''}://${import.meta.env.VITE_websocket || location.hostname}${shit.length ? '/?' + shit.join('&') : ''}`);
	ws.onclose = function(event) {
		//https://stackoverflow.com/a/28396165 always 1006...
		//according to the spec, if onerror is fired so will onclose. therefore, don't need to listen to onerror.
		ready.reject(event.code);//if ready is already resolved, this won't do anything
		closing.resolve();
		for(let p of promises.values()) {
			p[1]('websocket closed');
		}
		promises.clear();
		request_ID = 0;
		ready = create_deferred();
		for(let i = 0; i < ports.length; ++i) {
			ports[i].postMessage({what: 'disconnected'});
		}
		//reconnect on next req(). for notifications, have explicit reconnect button that will refetch the entire set via req()
	}
	ws.onopen = function(event) {
		if(shit.length === 0) {
			ready.resolve();
			for(let i = 0; i < ports.length; ++i) {//this is just to resolve the promise on initial load
				ports[i].postMessage({
					what: "user",
					how: "replace",
					data: null
				});
			}
		}
		closing = create_deferred();
	}
	ws.onmessage = function(event) {
		let d = JSON.parse(event.data);
		let p;
		if(p = promises.get(d.response_ID)) {
			p[0](d.data);
		} else {
			let ps = hashbrown.get(d.what);
			if(d.what === 'user') {
				switch(d.how) {
					case 'replace':
						ready.resolve();
						if((user = d.data) === null) {
							storage.delete('token');
							console.log(d.why);
						} else {
							if(d.data.token) {//we know this is from otp
								if(ports.length !== 1) {
									console.error("unpossible");
								} else {
									ports[0].postMessage('save');
									(save = create_deferred()).then(() => storage.set('token', token), () => {});
								}
							}
							storage.delete('carts');
						}
						break;
					case 'update':
						if(user) {
							Object.assign(user, d.data);
						} else {
							console.log('shieeet', d);
						}
						break;
				}
			} else if(d.what === 'store') {
				switch(d.how) {
					case 'replace':
						stores = d.data;
						break;
					case 'add':
						stores.push(d.data);
						break;
					case 'remove':
						stores.splice(stores.findIndex(s => s.id === d.data), 1);
						break;
					case 'update':
						//stores.find(s => s.id === d.data.id) = d.data;
						break;
				}
			}
			if(ps) {
				for(const port of ps) {
					port.postMessage(d);
				}
			} else {
				console.log('no one listening :(', d)
			}
		}
	}
	return ws;
}
let hashbrown = new Map();
self.hb = hashbrown;
let save;
const ports = [];
onconnect = async function(e) {
	const idx = ports.push(e.ports[0]) - 1;
	ports[idx].onmessage = async function(e) {
		if(typeof e.data === 'boolean') {
			if(e.data) {
				save.resolve();
			} else {
				save.reject();
			}
		}
		if(e.data === 'closing') {
			ports.splice(ports.indexOf(this), 1);
			if(ports.length) {//if 0, websocket connection and sharedworker die anyways right?
				for(const [event, set_ports] of hashbrown) {
					set_ports.delete(this);
					if(set_ports.size === 0) {
						hashbrown.delete(event);
						const rr = request_ID;
						w.send(JSON.stringify({what: 'unsubscribe', parameters: {what: event}, request_ID}));
						let data = await new Promise((resolve, reject) => promises.set(request_ID++, [resolve, reject])).finally(() => promises.delete(rr));
						if(data === false) {
							console.error(`failed to unsubscribe to ${event} probably because never subscribed to begin with`);
						} else if(data === undefined) {
							console.error('unpossible');
						} else {
							console.log('successed?')
						}
					}
				}
			}
		} else if(e.data.indexOf('first') === 0) {
			let otp = e.data.substring(5);
			if(w === undefined || w.readyState > 1) {
				if(w?.readyState > 1) {
					await closing;
				}
				w = await create_ws(otp);
			} else if(otp && user === null) {
				try {
					await ready;
				} catch(e) {
					console.error('otp', e);
					return;
				}
				const rr = request_ID;
				const params = {
					what: 'login_otp',
					parameters: {
						carts: await storage.get('carts'),
						otp
					},
					request_ID
				}
				try {
					w.send(JSON.stringify(params));
				} catch(e) {
					console.log(e);
					console.log(params);
					return;
				}
				let data = await new Promise((resolve, reject) => promises.set(request_ID++, [resolve, reject])).finally(() => promises.delete(rr));
				if(typeof data === 'string') {
					for(let i = 0; i < ports.length; ++i) {
						ports[i].postMessage({
							what: "user",
							how: "replace",
							data: null,
							why: data
						});
					}
				} else {
					const {token, ...noTokenUser} = user = data;
					for(let i = 0; i < ports.length; ++i) {
						ports[i].postMessage({
							what: "user",
							how: "replace",
							data: noTokenUser
						});
					}
					this.postMessage('save');
					(save = create_deferred()).then(() => storage.set('token', token), () => {});
					storage.delete('carts');
				}
			}
		} else if(e.data.indexOf('o') === 0) {
			const [action, event] = e.data.split(',');
			if(action === 'on') {
				if(hashbrown.has(event)) {
					hashbrown.get(event).add(this);
				} else {
					hashbrown.set(event, new Set([this]));
				}
			} else if(action === 'off') {
				if(event === 'all') {
					for(const [event, set_ports] of hashbrown) {
						set_ports.delete(this);
						if(set_ports.size === 0) {
							hashbrown.delete(event);
							const rr = request_ID;
							w.send(JSON.stringify({what: 'unsubscribe', parameters: {what: event}, request_ID}));
							let data = await new Promise((resolve, reject) => promises.set(request_ID++, [resolve, reject])).finally(() => promises.delete(rr));
							if(data === false) {
								console.error(`failed to unsubscribe to ${event} probably because never subscribed to begin with`);
							} else if(data === undefined) {
								console.error('unpossible');
							} else {
								console.log('successed?')
							}
						}
					}
				} else if(hashbrown.has(event)) {
					let s = hashbrown.get(event);
					s.delete(this);
					if(s.size === 0) {
						hashbrown.delete(event);
						const rr = request_ID;
						w.send(JSON.stringify({what: 'unsubscribe', parameters: {what: event}, request_ID}));
						let data = await new Promise((resolve, reject) => promises.set(request_ID++, [resolve, reject])).finally(() => promises.delete(rr));
						if(data === false) {
							console.error(`failed to unsubscribe to ${event} probably because never subscribed to begin with`);
						} else if(data === undefined) {
							console.error('unpossible');
						} else {
							console.log('successed?')
						}
					}
				} else {
					this.postMessage("you weren't even listening to this event to begin with...");
				}
			}
			//on off map. if all references are gone, can unsubscribe from server.
		} else {
			let {what, r, parameters} = JSON.parse(e.data);
			if(w.readyState > 1) {
				await closing;
				w = await create_ws();
			}
			try {
				await ready;
			} catch(e) {
				this.postMessage({r, failure: e});
				return;
			}
			let remember_me;
			if(parameters) {
				({remember_me, ...parameters} = parameters);
			}
			const rr = request_ID;
			try {
				w.send(JSON.stringify({what, parameters, request_ID}));
			} catch(e) {
				console.log(e);
				console.log({what, parameters, request_ID});
				return;
			}
			let data = await new Promise((resolve, reject) => promises.set(request_ID++, [resolve, reject])).finally(() => promises.delete(rr));
			this.postMessage({r, data});
			if(typeof remember_me === 'boolean' && typeof data === 'object') {
				if(remember_me) {
					storage.set('token', data.token);
				}
				if(what === 'register') {//only token returned
					user = Object.assign(data, {email: parameters.email, god: false, acl: null});
				} else if(what === 'login') {//token, god, acl
					user = Object.assign(data, {email: parameters.email});
				} else if(what === 'login_otp') {//token, email, god, acl
					user = data;
				}
				const {token, ...noTokenUser} = user;
				for(let i = 0; i < ports.length; ++i) {
					ports[i].postMessage({
						what: "user",
						how: "replace",
						data: noTokenUser
					});
				}
			} else if(what === 'logout' || data === 'unauthenticated' || what === 'unregister') {
				user = null;
				storage.delete('token');
				for(let i = 0; i < ports.length; ++i) {
					ports[i].postMessage({what: "user", how: 'replace', data: null});
				}
			} else if(data === 'unauthorized') {
				if(user) {
					user.god = false;
					for(let i = 0; i < ports.length; ++i) {
						ports[i].postMessage({what: "user", how: 'update', data: {god: false}});
					}
				} else {
					console.log('nani????');
				}
			} else if(data === undefined && what === 'update_profile') {
				if(parameters.email) {
					user.email = parameters.email;
					for(let i = 0; i < ports.length; ++i) {
						ports[i].postMessage({what: "user", how: 'update', data: {email: user.email}});
					}
				}
			}
		}
	}
	if(await Promise.race([ready, "promises suck because can't synchronously inspect status"]) === undefined) {
		if(stores) {
			ports[idx].postMessage({
				what: "store",
				how: "replace",
				data: stores
			});
		}
		if(user) {
			const {token, ...noTokenUser} = user;
			ports[idx].postMessage({
				what: "user",
				how: "replace",
				data: noTokenUser
			});
		} else {
			ports[idx].postMessage({
				what: "user",
				how: "replace",
				data: null
			});
		}
	}
}