'use strict';

const EventEmitter = require('events').EventEmitter;

class Worker extends EventEmitter {
	constructor(id) {
		super();
		this.id = id;
		this.process = {connected: true};
		this.sockets = new Set();
		this.channels = new Map();
		this.subchannels = new Map();
		Sockets.workers.set(this.id, this);
	}

	kill() {}

	send(msg) {
		let cmd = msg.charAt(0);
		let params = msg.substr(1).split('\n');
		switch (cmd) {
		case '!':
			return this.removeSocket(...params);
		case '>':
			return this.sendToSocket(...params);
		case '#':
			return this.sendToChannel(...params);
		case '+':
			return this.addToChannel(...params);
		case '-':
			return this.removeFromChannel(...params);
		case '.':
			return this.moveToSubchannel(...params);
		case ':':
			return this.broadcastToSubchannels(params[0]);
		}
	}

	addSocket(socketid) {
		this.sockets.add(+socketid);
	}

	removeSocket(socketid) {
		socketid = +socketid;
		if (!this.sockets.has(socketid)) {
			throw new Error(`Attempted to disconnect nonexistent socket ${socketid}`);
		}
		this.sockets.delete(socketid);
		this.channels.forEach((channel, channelid) => {
			channel.delete(socketid);
		});
	}

	sendToSocket(socketid, msg) {
		socketid = +socketid;
		if (!this.sockets.has(socketid)) {
			throw new Error(`Attempted to send ${msg} to nonexistent socket ${socketid}`);
		}
	}

	sendToChannel(channelid, msg) {
		if (!this.channels.has(channelid)) {
			throw new Error(`Attempted to send ${msg} to nonexistent channel ${channelid}`);
		}
	}

	addToChannel(channelid, socketid) {
		socketid = +socketid;
		if (!this.channels.has(channelid)) {
			this.channels.set(channelid, new Set([socketid]));
			return;
		}

		let channel = this.channels.get(channelid);
		if (channel.has(socketid)) {
			throw new Error(`Attempted to redundantly add socket ${socketid} to channel ${channelid}`);
		}
		channel.add(socketid);
	}

	removeFromChannel(channelid, socketid) {
		socketid = +socketid;
		if (!this.channels.has(channelid)) {
			throw new Error(`Attempted to remove socket ${socketid} from nonexistent channel ${channelid}`);
		}

		let channel = this.channels.get(channelid);
		if (!channel.has(socketid)) {
			throw new Error(`Attempted to remove nonexistent socket ${socketid} from channel ${channelid}`);
		}

		channel.delete(socketid);
		if (!channel.size) {
			this.channels.delete(channelid);
			this.subchannels.delete(channelid);
		}
	}

	moveToSubchannel(channelid, subchannelid, socketid) {
		socketid = +socketid;
		if (!this.channels.has(channelid)) {
			throw new Error(`Attempted to move socket ${socketid} to subchannel ${subchannelid} of nonexistent channel ${channelid}`);
		}

		if (!this.subchannels.has(channelid)) {
			if (subchannelid === '0') return;
			this.subchannels.set(channelid, new Map([[socketid, subchannelid]]));
			return;
		}

		let subchannel = this.subchannels.get(channelid);
		if (!subchannel.has(socketid)) {
			if (subchannelid !== '0') subchannel.set(socketid, subchannelid);
			return;
		}

		if (subchannelid === '0') {
			subchannel.delete(socketid);
		} else {
			subchannel.set(socketid, subchannelid);
		}
	}

	broadcastToSubchannels(channelid) {
		if (!this.channels.has(channelid)) {
			throw new Error(`Attempted to broadcast to subchannels of nonexistent channel ${channelid}`);
		}
		if (!this.subchannels.has(channelid)) {
			throw new Error(`Attempted to broadcast to nonexistent subchannels of channel ${channelid}`);
		}
	}
}

function createConnection(ip, workerid, socketid) {
	let worker;
	if (workerid) {
		workerid = +workerid;
		worker = Sockets.workers.get(workerid) || new Worker(workerid);
	} else {
		worker = Sockets.workers.get(1) || new Worker(1);
		workerid = worker.id;
	}

	if (!socketid) {
		socketid = 1;
		while (Users.connections.has(`${workerid}-${socketid}`)) {
			socketid++;
		}
	}
	worker.addSocket(socketid);

	let connectionid = `${workerid}-${socketid}`;
	let connection = new Users.Connection(connectionid, worker, socketid, null, ip || '127.0.0.1');
	Users.connections.set(connectionid, connection);

	return connection;
}

function createUser(connection) {
	if (!connection) connection = createConnection();

	let user = new Users.User(connection);
	user.joinRoom('global', connection);
	connection.user = user;

	return user;
}

exports.Connection = createConnection;
exports.User = createUser;
