
async function lnsocket_init() {
	const module = await Module()

	function SocketImpl(host) {
		if (!(this instanceof SocketImpl))
			return new SocketImpl(host)

		if (typeof WebSocket !== 'undefined') {
			console.log("WebSocket", typeof WebSocket)
			const ok = host.startsWith("ws://") || host.startsWith("wss://")
			if (!ok)
				throw new Error("host must start with ws:// or wss://")
			const ws = new WebSocket(host)
			ws.ondata = function(fn) {
				ws.onmessage = (v) => {
					const data = v.data.arrayBuffer()
					fn(data)
				}
			}
			return ws
		}

		//
		// we're in nodejs
		//
		const net = require('net')
		let [hostname,port] = host.split(":")
		port = +port || 9735
		const socket = net.createConnection(port, hostname, () => {
			socket.emit("open")
		})
		socket.addEventListener = socket.on.bind(socket)

		if (socket.onmessage)
			throw new Error("socket already has onmessage?")

		socket.ondata = (fn) => {
			socket.on('data', fn)
		}

		socket.close = () => {
			socket.destroy()
		}

		if (socket.send)
			throw new Error("socket already has send?")

		socket.send = function socket_send(data) {
			return new Promise((resolve, reject) => {
				socket.write(data, resolve)
			});
		}

		return socket
	}

	const ACT_ONE_SIZE = 50
	const ACT_TWO_SIZE = 50
	const ACT_THREE_SIZE = 66
	const DEFAULT_TIMEOUT = 15000

	const COMMANDO_REPLY_CONTINUES = 0x594b
	const COMMANDO_REPLY_TERM = 0x594d

	const lnsocket_create = module.cwrap("lnsocket_create", "number")
	const lnsocket_destroy = module.cwrap("lnsocket_destroy", "number")
	const lnsocket_encrypt = module.cwrap("lnsocket_encrypt", "number", ["int", "array", "int", "int"])
	const lnsocket_decrypt = module.cwrap("lnsocket_decrypt", "number", ["int", "array", "int"])
	const lnsocket_decrypt_header = module.cwrap("lnsocket_decrypt_header", "number", ["number", "array"])
	const lnsocket_msgbuf = module.cwrap("lnsocket_msgbuf", "number", ["int"])
	const lnsocket_act_one = module.cwrap("lnsocket_act_one", "number", ["number", "string"])
	const lnsocket_act_two = module.cwrap("lnsocket_act_two", "number", ["number", "array"])
	const lnsocket_print_errors = module.cwrap("lnsocket_print_errors", "int")
	const lnsocket_genkey = module.cwrap("lnsocket_genkey", "int")
	const lnsocket_make_default_initmsg = module.cwrap("lnsocket_make_default_initmsg", "int", ["int", "int"])
	const lnsocket_make_ping_msg = module.cwrap("lnsocket_make_ping_msg", "int", ["int", "int", "int", "int"])
	const commando_make_rpc_msg = module.cwrap("commando_make_rpc_msg", "int", ["string", "string", "string", "number", "int", "int"])

	function concat_u8_arrays(arrays) {
		// sum of individual array lengths
		let totalLength = arrays.reduce((acc, value) =>
			acc + (value.length || value.byteLength)
		, 0);

		if (!arrays.length) return null;

		let result = new Uint8Array(totalLength);

		let length = 0;
		for (let array of arrays) {
			if (array instanceof ArrayBuffer)
				result.set(new Uint8Array(array), length);
			else
				result.set(array, length);

			length += (array.length || array.byteLength);
		}

		return result;
	}

	function parse_msgtype(buf) {
		return buf[0] << 8 | buf[1]
	}

	function wasm_mem(ptr, size) {
		return new Uint8Array(module.HEAPU8.buffer, ptr, size);
	}

	function LNSocket(opts) {
		if (!(this instanceof LNSocket))
			return new LNSocket(opts)

		this.opts = opts || {
			timeout: DEFAULT_TIMEOUT
		}
		this.queue = []
		this.ln = lnsocket_create()
	}

	LNSocket.prototype.queue_recv = function() {
		let self = this
		return new Promise((resolve, reject) => {
			const checker = setInterval(() => {
				const val = self.queue.shift()
				if (val) {
					clearInterval(checker)
					resolve(val)
				} else if (!self.connected) {
					clearInterval(checker)
					reject()
				}
			}, 5);
		})
	}

	LNSocket.prototype.print_errors = function _lnsocket_print_errors() {
		lnsocket_print_errors(this.ln)
	}

	LNSocket.prototype.genkey = function _lnsocket_genkey() {
		lnsocket_genkey(this.ln)
	}

	LNSocket.prototype.act_one_data = function _lnsocket_act_one(node_id) {
		const act_one_ptr = lnsocket_act_one(this.ln, node_id)
		if (act_one_ptr === 0)
			return null
		return wasm_mem(act_one_ptr, ACT_ONE_SIZE)
	}

	LNSocket.prototype.act_two = function _lnsocket_act_two(act2) {
		const act_three_ptr = lnsocket_act_two(this.ln, new Uint8Array(act2))
		if (act_three_ptr === 0) {
			this.print_errors()
			return null
		}
		return wasm_mem(act_three_ptr, ACT_THREE_SIZE)
	}

	LNSocket.prototype.connect = async function lnsocket_connect(node_id, host) {
		await handle_connect(this, node_id, host)

		const act1 = this.act_one_data(node_id)
		this.ws.send(act1)
		const act2 = await this.read_all(ACT_TWO_SIZE)
		if (act2.length != ACT_TWO_SIZE) {
			throw new Error(`expected act2 to be ${ACT_TWO_SIZE} long, got ${act2.length}`)
		}
		const act3 = this.act_two(act2)
		this.ws.send(act3)
	}

	LNSocket.prototype.connect_and_init = async function _connect_and_init(node_id, host) {
		await this.connect(node_id, host)
		await this.perform_init()
	}

	LNSocket.prototype.read_all = async function read_all(n) {
		let count = 0
		let chunks = []
		if (!this.connected)
			throw new Error("read_all: not connected")
		while (true) {
			let res = await this.queue_recv()

			const remaining = n - count

			if (res.byteLength > remaining) {
				chunks.push(res.slice(0, remaining))
				this.queue.unshift(res.slice(remaining))
				break
			} else if (res.byteLength === remaining) {
				chunks.push(res)
				break
			}

			chunks.push(res)
			count += res.byteLength
		}

		return concat_u8_arrays(chunks)
	}

	LNSocket.prototype.read_header = async function read_header() {
		const header = await this.read_all(18)
		if (header.length != 18)
			throw new Error("Failed to read header")
		return lnsocket_decrypt_header(this.ln, header)
	}

	LNSocket.prototype.rpc = async function lnsocket_rpc(opts) {
		const msg = this.make_commando_msg(opts)
		this.write(msg)
		const res = await this.read_all_rpc()
		return JSON.parse(res)
	}

	LNSocket.prototype.recv = async function lnsocket_recv() {
		const msg = await this.read()
		const msgtype = parse_msgtype(msg.slice(0,2))
		const res = [msgtype, msg.slice(2)]
		return res
	}

	LNSocket.prototype.read_all_rpc = async function read_all_rpc() {
		let chunks = []
		while (true) {
			const [typ, msg] = await this.recv()
			switch (typ) {
			case COMMANDO_REPLY_TERM:
				chunks.push(msg.slice(8))
				return new TextDecoder().decode(concat_u8_arrays(chunks));
			case COMMANDO_REPLY_CONTINUES:
				chunks.push(msg.slice(8))
				break
			default:
				console.log("got unknown type", typ)
				continue
			}
		}
	}

	LNSocket.prototype.make_commando_msg = function _lnsocket_make_commando_msg(opts) {
		const buflen = 4096
		let len = 0;
		const buf = module._malloc(buflen);
		module.HEAPU8.set(Uint8Array, buf);

		const params = JSON.stringify(opts.params||{})
		if (!(len = commando_make_rpc_msg(opts.method, params, opts.rune,
			0, buf, buflen))) {
			throw new Error("couldn't make commando msg");
		}

		const dat = wasm_mem(buf, len)
		module._free(buf);
		return dat
	}

	LNSocket.prototype.make_ping_msg = function _lnsocket_make_ping_msg(num_pong_bytes=1, ignored_bytes=1)  {
		const buflen = 32
		let len = 0;
		const buf = module._malloc(buflen);
		module.HEAPU8.set(Uint8Array, buf);

		if (!(len = lnsocket_make_ping_msg(buf, buflen, num_pong_bytes, ignored_bytes)))
			throw new Error("couldn't make ping msg");

		const dat = wasm_mem(buf, len)
		module._free(buf);
		return dat
	}

	LNSocket.prototype.encrypt = function _lnsocket_encrypt(dat) {
		const len = lnsocket_encrypt(this.ln, dat, dat.length)
		if (len === 0) {
			this.print_errors()
			throw new Error("encrypt error")
		}
		const enc = wasm_mem(lnsocket_msgbuf(this.ln), len)
		return enc
	}

	LNSocket.prototype.decrypt = function _lnsocket_decrypt(dat) {
		const len = lnsocket_decrypt(this.ln, dat, dat.length)
		if (len === 0) {
			this.print_errors()
			throw new Error("decrypt error")
		}
		return wasm_mem(lnsocket_msgbuf(this.ln), len)
	}

	LNSocket.prototype.write = function _lnsocket_write(dat) {
		this.ws.send(this.encrypt(dat))
	}

	LNSocket.prototype.read = async function _lnsocket_read() {
		const size = await this.read_header()
		const enc = await this.read_all(size+16)
		return this.decrypt(enc)
	}

	LNSocket.prototype.make_default_initmsg = function _lnsocket_make_default_initmsg() {
		const buflen = 1024
		let len = 0;
		const buf = module._malloc(buflen);
		module.HEAPU8.set(Uint8Array, buf);

		if (!(len = lnsocket_make_default_initmsg(buf, buflen)))
			throw new Error("couldn't make initmsg");

		const dat = wasm_mem(buf, len)
		module._free(buf);
		return dat
	}

	LNSocket.prototype.perform_init = async function lnsocket_connect() {
		await this.read()
		const our_init = this.make_default_initmsg()
		this.write(our_init)
	}

	LNSocket.prototype.ping_pong = async function lnsocket_ping_pong() {
		const pingmsg = this.make_ping_msg()
		this.write(pingmsg)
		return await this.read()
	}

	LNSocket.prototype.disconnect = function lnsocket_disconnect() {
		if (this.connected === true && this.ws) {
			this.ws.close()
			return true
		}
		return false
	}

	LNSocket.prototype.destroy = function _lnsocket_destroy() {
		this.disconnect()
		lnsocket_destroy(this.ln)
	}

	function handle_connect(ln, node_id, host) {
		const ws = new SocketImpl(host)
		return new Promise((resolve, reject) => {
			const timeout = ln.opts.timeout || DEFAULT_TIMEOUT
			const timer = setTimeout(reject, timeout);

			ws.ondata((v) => {
				ln.queue.push(v)
			});

			ws.addEventListener('open', function(ev) {
				ln.ws = ws
				ln.connected = true
				clearTimeout(timer)
				resolve(ws)
			});

			ws.addEventListener('close', function(ev) {
				ln.connected = false
			});
		})
	}

	return LNSocket
}

Module.init = Module.lnsocket_init = lnsocket_init
