type int = number;
type float = number;


var _conf: any = null;


function get_timestamp(): int
{
	return Math.floor(Date.now()/1000);
}


function hash_string_to_unit(x: string): float
{
	return (x.split("").reduce((x, y) => ((x + y.charCodeAt(0)) % 32), 0) / 32);
}


function get_usercolor(name: string): string
{
	const hue: float = hash_string_to_unit(name);
	return `hsl(${(hue*360).toFixed(2)},50%,75%)`;
}


async function backend_call(connection_id: (null | string), action: string, data: any): Promise<any>
{
	const response = await fetch
	(
		`${_conf["backend"]["scheme"]}://${_conf["backend"]["host"]}:${_conf["backend"]["port"].toFixed(0)}/${_conf["backend"]["path"]}`,
		{
			"method": "POST",
			"body": JSON.stringify({"id": connection_id, "action": action, "data": data}),
		}
	);
	if (response.ok)
	{
		return response.json();
	}
	else
	{
		console.error(response.text());
		return Promise.reject<any>(new Error("backend call failed"));
	}
}

enum enum_state
{
	offline = "offline",
	connecting = "connecting",
	online = "online",
}


type type_event =
{
	timestamp: int;
	kind: string;
	data: any;
};


type type_spot =
{
	kind: string;
	name: string;
};


type type_entry =
{
	timestamp: int;
	sender: string;
	content: string;
};


type type_user =
{
	name: string;
	role: string;
};


type type_channel =
{
	users: Array<type_user>;
	entries: Array<type_entry>;
};


type type_query =
{
	entries: Array<type_entry>;
};


type type_model =
{
	state: enum_state;
	connection_id: (null | string);
	nickname: (null | string);
	channels: Record<string, type_channel>;
	queries: Record<string, type_query>;
	active: (null | type_spot);
};



function model_set_state(model: type_model, state: enum_state): void
{
	model.state = state;
	view_update_state(model);
}


function model_set_active(model: type_model, spot: type_spot): void
{
	model.active = spot;
	view_update_spots(model);
	view_update_entries(model);
	view_update_users(model);
}


function model_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 "userlist":
			{
				model.channels[event.data["channel"]].users = event.data["users"];
				shall_update_users = true;
				break;
			}
			case "message_channel":
			{
				model.channels[event.data["channel"]].entries.push
				({
					"timestamp": event.timestamp,
					"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,
					"sender": event.data["sender"],
					"content": event.data["content"],
				});
				shall_update_entries = true;
				break;
			}
		}
	}
	
	// update view
	{
		if (shall_update_spots) view_update_spots(model);
		if (shall_update_entries) view_update_entries(model);
		if (shall_update_users) view_update_users(model);
	}
}



function view_update_state(model: type_model): void
{
	document.querySelector("body").setAttribute("class", model.state);
}


function view_update_spots(model: type_model): void
{
	let dom_spots: HTMLUListElement = document.querySelector("#spots");
	const spots: Array<type_spot> = (
		[]
		.concat(Object.keys(model.channels).map((name) => ({"kind": "channel", "name": name})))
		.concat(Object.keys(model.queries).map((name) => ({"kind": "query", "name": name})))
	);
	dom_spots.textContent = "";
	for (const spot of spots)
	{
		let dom_spot: HTMLLIElement = document.createElement("li");
		dom_spot.classList.add("spot");
		{
			let dom_kind: HTMLSpanElement = document.createElement("span");
			dom_kind.classList.add("spot_kind");
			dom_kind.textContent = spot.kind;
			dom_spot.appendChild(dom_kind);
		}
		{
			let dom_name: HTMLSpanElement = document.createElement("span");
			dom_name.classList.add("spot_sender");
			dom_name.textContent = spot.name;
			dom_spot.appendChild(dom_name);
		}
		dom_spot.classList.toggle("spot_active", ((spot.kind === model.active.kind) && (spot.name === model.active.name)));
		dom_spot.addEventListener
		(
			"click",
			(e) =>
			{
				model_set_active(model, spot);
			}
		);
		dom_spots.appendChild(dom_spot);
	}
}


function view_update_entries(model: type_model): void
{
	let dom_entries: HTMLUListElement = document.querySelector("#entries");
	let entries: Array<type_entry>;
	switch (model.active.kind)
	{
		case "channel":
		{
			entries = model.channels[model.active.name].entries;
			break;
		}
		case "query":
		{
			entries = model.queries[model.active.name].entries;
			break;
		}
	}
	dom_entries.textContent = "";
	for (const entry of entries)
	{
		let dom_entry: HTMLLIElement = document.createElement("li");
		dom_entry.classList.add("entry");
		{
			let dom_time: HTMLSpanElement = document.createElement("span");
			dom_time.classList.add("entry_time");
			dom_time.textContent = (new Date(entry.timestamp*1000)).toISOString().slice(11, 19);
			dom_entry.appendChild(dom_time);
		}
		{
			let dom_sender: HTMLSpanElement = document.createElement("span");
			dom_sender.classList.add("entry_sender");
			dom_sender.style.color = get_usercolor(entry.sender);
			dom_sender.textContent = entry.sender;
			dom_entry.appendChild(dom_sender);
		}
		{
			let dom_content: HTMLSpanElement = document.createElement("span");
			dom_content.classList.add("entry_content");
			dom_content.textContent = entry.content;
			dom_entry.appendChild(dom_content);
		}
		dom_entries.appendChild(dom_entry);
	}
	dom_entries.scrollTo(0, dom_entries["scrollTopMax"]);
}


function view_update_users(model: type_model): void
{
	let dom_users: HTMLUListElement = document.querySelector("#users");
	dom_users.textContent = "";
	let users: Array<type_user>;
	switch (model.active.kind)
	{
		default:
		{
			console.warn("unhandled kind: " + model.active.kind);
			users = [];
			break;
		}
		case "channel":
		{
			users = model.channels[model.active.name].users;
			break;
		}
		case "query":
		{
			users = [{"name": model.nickname, "role": ""}, {"name": model.active.name, "role": ""}];
			break;
		}
	}
	const users_sorted: Array<type_user> = users.sort
	(
		(x, y) =>
		(
			(x.role >= y.role)
			? -1
			: (
				(x.role === y.role)
				? ((x.name < y.name) ? -1 : +1)
				: +1
			)
		)
	);
	for (const user of users_sorted)
	{
		let dom_user: HTMLLIElement = document.createElement("li");
		dom_user.classList.add("user");
		{
			let dom_role: HTMLSpanElement = document.createElement("span");
			dom_role.textContent = user.role;
			dom_user.appendChild(dom_role);
		}
		{
			let dom_name: HTMLSpanElement = document.createElement("span");
			dom_name.textContent = user.name;
			dom_name.style.color = get_usercolor(user.name);
			dom_user.appendChild(dom_name);
		}
		dom_users.appendChild(dom_user);
	}
}


function view_setup(model: type_model): void
{
	document.querySelector<HTMLInputElement>("#channel").value = _conf["irc"]["predefined_channel"];
	document.querySelector<HTMLInputElement>("#nickname").value = (_conf["irc"]["predefined_nickname_prefix"] + (Math.random()*100).toFixed(0));
	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(model.connection_id, "check", null);
					if (ready)
					{
						model_set_state(model, enum_state.online);
					}
					else
					{
						// do nothing
					}
					break;
				}
				case enum_state.online:
				{
					const events: Array<type_event> = await backend_call(model.connection_id, "fetch", null);
					model_process_events(model, events);
					break;
				}
			}
		},
		_conf["settings"]["poll_interval_in_milliseconds"]
	);
	model_set_state(model, enum_state.offline);
}


function control_setup(model: type_model): void
{
	document.querySelector("#connect > form").addEventListener
	(
		"submit",
		async (event) =>
		{
			event.preventDefault();
			
			model_set_state(model, enum_state.connecting);
			
			let dom_nickname: HTMLInputElement = document.querySelector<HTMLInputElement>("#nickname");
			let dom_channel: HTMLInputElement = document.querySelector<HTMLInputElement>("#channel");
			const nickname: string = dom_nickname.value;
			const channel_names: Array<string> = dom_channel.value.split(",");
			
			const connection_id: string = await backend_call
			(
				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": [],
				};
			}
			// TODO: can crash
			model_set_active(model, {"kind": "channel", "name": channel_names[0]});
		}
	);
	document.querySelector("#disconnect").addEventListener
	(
		"click",
		async (event) =>
		{
			await backend_call
			(
				model.connection_id,
				"disconnect",
				null
			);
			model_set_state(model, enum_state.offline);
			model.connection_id = null;
		}
	);
	document.querySelector("#main > form").addEventListener
	(
		"submit",
		async (e) =>
		{
			event.preventDefault();
			
			let dom_content: HTMLInputElement = document.querySelector<HTMLInputElement>("#content");
			const content: string = dom_content.value;
			dom_content.value = "";
			dom_content.focus();
			switch (model.active.kind)
			{
				case "channel":
				{
					const event: type_event =
					{
						"timestamp": get_timestamp(),
						"kind": "message_channel",
						"data":
						{
							"channel": model.active.name,
							"sender": model.nickname,
							"content": content,
						}
					};
					model_process_events(model, [event]);
					view_update_entries(model);
					backend_call
					(
						model.connection_id,
						"send_channel",
						{
							"channel": model.active.name,
							"content": content,
						}
					);
					break;
				}
				case "query":
				{
					const event: type_event =
					{
						"timestamp": get_timestamp(),
						"kind": "message_query",
						"data":
						{
							"user_name": model.active.name,
							"sender": model.nickname,
							"content": content,
						}
					};
					model_process_events(model, [event]);
					backend_call
					(
						model.connection_id,
						"send_query",
						{
							"receiver": model.active.name,
							"content": content,
						}
					);
					break;
				}
			}
		}
	);
}


var _model;
async function main(): Promise<void>
{
	_conf = await fetch("conf.json").then(x => x.json());
	const model: type_model =
	{
		"state": enum_state.offline,
		"connection_id": null,
		"nickname": null,
		"channels": {},
		"queries": {},
		"active": null,
	};
_model = model;
	view_setup(model);
	control_setup(model);
}


function init(): void
{
	document.addEventListener
	(
		"DOMContentLoaded",
		(event) => {main();}
	);
}