namespace ns_model
{
	
	/**
	 * adds a listener for a certain incident
	 */
	export function listen
	(
		model: type_model,
		incident: string,
		handler: (details?: any)=>void
	): void
	{
		if (! model.listeners.hasOwnProperty(incident))
		{
			model.listeners[incident] = [];
		}
		else
		{
			// do nothing
		}
		model.listeners[incident].push(handler);
	}


	/**
	 * sends a notification to all listeners for a certain incident
	 */
	function notify
	(
		model: type_model,
		incident: string,
		details: any = null
	): void
	{
		if (model.listeners.hasOwnProperty(incident))
		{
			for (const handler of model.listeners[incident])
			{
				handler(details);
			}
		}
	}


	/**
	 * sets the state
	 */
	export function set_state
	(
		model: type_model,
		state: enum_state
	): void
	{
		model.state = state;
		notify(model, "state_changed");
	}


	/**
	 * sets the active spot (channel or query)
	 */
	export function set_active
	(
		model: type_model,
		spot: type_spot
	): void
	{
		model.active = spot;
		notify(model, "spots_changed");
		notify(model, "entries_changed");
		notify(model, "users_changed");
	}


	/**
	 * updates the model according to a list of events
	 */
	function process_events
	(
		model: type_model,
		events: Array<type_event>
	): void
	{
		let shall_update_spots: boolean = false;
		let shall_update_entries: boolean = false;
		let shall_update_users: boolean = false;
		
		for (const event of events)
		{
			switch (event.kind)
			{
				default:
				{
					console.warn("unhandled event kind: " + event.kind);
					break;
				}
				case "user_list":
				{
					model.channels[event.data["channel"]].users = event.data["users"];
					shall_update_users = true;
					break;
				}
				case "user_joined":
				{
					if (model.channels.hasOwnProperty(event.data["channel"]))
					{
						model.channels[event.data["channel"]].users.push({"name": event.data["user_name"], "role": ""});
						model.channels[event.data["channel"]].entries.push
						({
							"timestamp": event.timestamp,
							"kind": enum_entrykind.info,
							"sender": null,
							"content": `${event.data["user_name"]} joined`,
						});
						shall_update_users = true;
						shall_update_entries = true;
					}
					else
					{
						// do nothing
					}
					break;
				}
				case "user_parted":
				{
					if (model.channels.hasOwnProperty(event.data["channel"]))
					{
						model.channels[event.data["channel"]].users = model.channels[event.data["channel"]].users.filter
						(
							(user) => (user.name != event.data["user_name"])
						);
						model.channels[event.data["channel"]].entries.push
						({
							"timestamp": event.timestamp,
							"kind": enum_entrykind.info,
							"sender": null,
							"content": `${event.data["user_name"]} left`,
						});
						shall_update_users = true;
						shall_update_entries = true;
					}
					else
					{
						// do nothing
					}
					break;
				}
				case "user_kicked":
				{
					if (model.channels.hasOwnProperty(event.data["channel"]))
					{
						model.channels[event.data["channel"]].users = model.channels[event.data["channel"]].users.filter
						(
							(user) => (user.name != event.data["user_name"])
						);
						model.channels[event.data["channel"]].entries.push
						({
							"timestamp": event.timestamp,
							"kind": enum_entrykind.info,
							"sender": null,
							"content": `${event.data["user_name"]} was kicked by ${event.data["op_name"]}: ${event.data["reason"]}`,
						});
						shall_update_users = true;
						shall_update_entries = true;
					}
					else
					{
						// do nothing
					}
					break;
				}
				case "user_quit":
				{
					for (let channel_name of event.data["channels"])
					{
						if (model.channels.hasOwnProperty(channel_name))
						{
							model.channels[channel_name].users = model.channels[channel_name].users.filter
							(
								(user) => (user.name != event.data["user_name"])
							);
							model.channels[channel_name].entries.push
							({
								"timestamp": event.timestamp,
								"kind": enum_entrykind.info,
								"sender": null,
								"content": `${event.data["user_name"]} quit`,
							});
							shall_update_entries = true;
							shall_update_users = true;
						}
					}
					break;
				}
				case "message_channel":
				{
					model.channels[event.data["channel"]].entries.push
					({
						"timestamp": event.timestamp,
						"kind": enum_entrykind.message,
						"sender": event.data["sender"],
						"content": event.data["content"],
					});
					shall_update_entries = true;
					break;
				}
				case "message_query":
				{
					if (! model.queries.hasOwnProperty(event.data["user_name"]))
					{
						model.queries[event.data["user_name"]] = {"entries": []};
						shall_update_spots = true;
					}
					else
					{
						// do nothing
					}
					model.queries[event.data["user_name"]].entries.push
					({
						"timestamp": event.timestamp,
						"kind": enum_entrykind.message,
						"sender": event.data["sender"],
						"content": event.data["content"],
					});
					shall_update_entries = true;
					break;
				}
			}
		}
		
		if (shall_update_spots) notify(model, "spots_changed");
		if (shall_update_entries) notify(model, "entries_changed");
		if (shall_update_users) notify(model, "users_changed");
	}


	/**
	 * establishes the connection
	 */
	export async function connect
	(
		conf: type_conf,
		model: type_model,
		nickname: string,
		channel_names: Array<string>
	): Promise<void>
	{
		set_state(model, enum_state.connecting);
		const connection_id: string = await backend_call
		(
			conf,
			model.connection_id,
			"connect",
			{
				"server": conf.irc.server,
				"channels": channel_names,
				"nickname": nickname,
			}
		);
		model.connection_id = connection_id;
		model.nickname = nickname;
		for (const channel_name of channel_names)
		{
			model.channels[channel_name] =
			{
				"users": [],
				"entries": [],
			};
		}
		if (channel_names.length > 0)
		{
			set_active(model, {"kind": "channel", "name": channel_names[0]});
		}
		return Promise.resolve<void>(undefined);
	}


	/**
	 * closes the connection
	 */
	export async function disconnect
	(
		conf: type_conf,
		model: type_model
	): Promise<void>
	{
		await backend_call
		(
			conf,
			model.connection_id,
			"disconnect",
			null
		);
		set_state(model, enum_state.offline);
		model.connection_id = null;
		return Promise.resolve<void>(undefined);
	}


	/**
	 * adds a client side message
	 */
	export function send
	(
		conf: type_conf,
		model: type_model,
		content: string
	): void
	{
		switch (model.active.kind)
		{
			case "channel":
			{
				backend_call
				(
					conf,
					model.connection_id,
					"send_channel",
					{
						"channel": model.active.name,
						"content": content,
					}
				);
				const event: type_event =
				{
					"timestamp": get_timestamp(),
					"kind": "message_channel",
					"data":
					{
						"channel": model.active.name,
						"sender": model.nickname,
						"content": content,
					}
				};
				process_events(model, [event]);
				notify(model, "entries_changed");
				notify(model, "message_sent");
				break;
			}
			case "query":
			{
				backend_call
				(
					conf,
					model.connection_id,
					"send_query",
					{
						"receiver": model.active.name,
						"content": content,
					}
				);
				const event: type_event =
				{
					"timestamp": get_timestamp(),
					"kind": "message_query",
					"data":
					{
						"user_name": model.active.name,
						"sender": model.nickname,
						"content": content,
					}
				};
				process_events(model, [event]);
				notify(model, "entries_changed");
				notify(model, "message_sent");
				break;
			}
		}
	}
	
	
	/**
	 * adds a query to a user as spot
	 */
	export function open_query
	(
		model: type_model,
		user_name: string
	): void
	{
		if ((user_name !== model.nickname) && (! model.queries.hasOwnProperty(user_name)))
		{
			model.queries[user_name] = {"entries": []};
			notify(model, "spots_changed");
		}
		else
		{
			// do nothing
		}
	}
	
	
	/**
	 * sets up the model
	 */
	export function setup
	(
		conf: type_conf,
		model: type_model
	): void
	{
		setInterval
		(
			async () =>
			{
				switch (model.state)
				{
					default:
					{
						throw (new Error(`invalid state '${model.state}'`));
						break;
					}
					case enum_state.offline:
					{
						// do nothing
						break;
					}
					case enum_state.connecting:
					{
						const ready: boolean = await backend_call(conf, model.connection_id, "check", null);
						if (ready)
						{
							set_state(model, enum_state.online);
						}
						else
						{
							// do nothing
						}
						break;
					}
					case enum_state.online:
					{
						const events: Array<type_event> = await backend_call(conf, model.connection_id, "fetch", null);
						process_events(model, events);
						break;
					}
				}
			},
			conf.settings.poll_interval_in_milliseconds
		);
		set_state(model, enum_state.offline);
	}
	
}