#!/bin/sh

: "${IRC_TZ:=UTC}"
: "${IRC_HL_COLOR:=blue}"
: "${IRC_CHAN_WIDTH:=12}"
: "${IRC_NICK_WIDTH:=9}"
: "${IRC_MSG_MAX:=0}"
: "${IRC_TIME_FMT:=%H:%M}"
: "${IRC_SHOW_TS:=1}"
: "${IRC_PM_LABEL:=pm}"
: "${IRC_HL_BOLD:=0}"
: "${IRC_COLOR:=1}"
: "${IRC_MAX_NICK:=0}"
: "${IRC_MAX_CHAN:=0}"
: "${IRC_TAB_WIDTH:=0}"
: "${IRC_TRUNC_SUFFIX:=...}"
: "${IRC_ACTION_COLOR:=}"
: "${IRC_NOTICE_COLOR:=}"
TZ="$IRC_TZ"
export TZ

awk -v hl_color="$IRC_HL_COLOR" -v chan_width="$IRC_CHAN_WIDTH" -v nick_width="$IRC_NICK_WIDTH" -v msg_max="$IRC_MSG_MAX" \
	-v time_fmt="$IRC_TIME_FMT" -v show_ts="$IRC_SHOW_TS" -v pm_label="$IRC_PM_LABEL" -v hl_bold="$IRC_HL_BOLD" \
	-v color_on="$IRC_COLOR" -v max_nick="$IRC_MAX_NICK" -v max_chan="$IRC_MAX_CHAN" -v tab_width="$IRC_TAB_WIDTH" \
	-v trunc_suffix="$IRC_TRUNC_SUFFIX" -v action_color="$IRC_ACTION_COLOR" -v notice_color="$IRC_NOTICE_COLOR" '
function ltrim(s) { sub(/^[ \t\r\n]+/, "", s); return s }
function rtrim(s) { sub(/[ \t\r\n]+$/, "", s); return s }
function trim(s) { return rtrim(ltrim(s)) }
function nick_from_prefix(p,  n) {
	n = p
	sub(/^:/, "", n)
	sub(/!.*/, "", n)
	return n
}
function raw_trailing(raw,   tmp, i) {
	tmp = raw
	i = match(tmp, / :/)
	if (i > 0) return substr(tmp, i + 2)
	if (tmp ~ /^:/ && tmp ~ /^:[^ ]+ [^ ]+ :/) {
		sub(/^:[^ ]+ [^ ]+ :/, "", tmp)
		return tmp
	}
	return ""
}
function ts() { return strftime(time_fmt) }
function is_channel(t) { return (t ~ /^#|^\+|^&|^!/) }
function pad_right(s, width,   pad, tabs, spaces) {
	if (tab_width > 0) {
		pad = width - length(s)
		if (pad <= 0) return s
		tabs = int(pad / tab_width)
		spaces = pad - (tabs * tab_width)
		return s sprintf("%" tabs "s", "") sprintf("%" spaces "s", "")
	}
	return sprintf("%-" width "." width "s", s)
}
function pad_left(s, width,   pad) { return sprintf("%" width "." width "s", s) }
function pad16(s,   out) { return pad_right(s, chan_width) }
function padnick(s,   out) { return pad_left(s, nick_width) }
function padnick_colored(raw, colored,   w, pad) {
	raw = substr(raw, 1, nick_width)
	w = length(raw)
	pad = sprintf("%" (nick_width - w) "s", "")
	return pad colored
}
function quote_reason(s) {
	if (s == "") return ""
	gsub(/"/, "\\\"", s)
	return "\"" s "\""
}
function fmt_action(msg, chan,   left) {
	left = pad16(trim_chan(chan))
	if (show_ts) return sprintf("[%s] %s %s", ts(), left, msg)
	return sprintf("%s %s", left, msg)
}
function fmt_action_line(chan, nick, text,   left, who) {
	left = pad16(trim_chan(chan))
	who = padnick(trim_nick(nick))
	if (show_ts) return sprintf("[%s] %s %s : %s", ts(), left, who, text)
	return sprintf("%s %s : %s", left, who, text)
}
function color_code(name) {
	if (!color_on) return ""
	if (name == "black") return "\033[30m"
	if (name == "red") return "\033[31m"
	if (name == "green") return "\033[32m"
	if (name == "yellow") return "\033[33m"
	if (name == "blue") return "\033[34m"
	if (name == "magenta") return "\033[35m"
	if (name == "cyan") return "\033[36m"
	if (name == "white") return "\033[37m"
	if (name == "bright_black") return "\033[90m"
	if (name == "bright_red") return "\033[91m"
	if (name == "bright_green") return "\033[92m"
	if (name == "bright_yellow") return "\033[93m"
	if (name == "bright_blue") return "\033[94m"
	if (name == "bright_magenta") return "\033[95m"
	if (name == "bright_cyan") return "\033[96m"
	if (name == "bright_white") return "\033[97m"
	return "\033[34m"
}
function color_reset() { return (color_on ? "\033[0m" : "") }
function bold_code() { return (color_on ? "\033[1m" : "") }
function colorize(s, c, b,   out) {
	if (!color_on || c == "") return s
	out = color_code(c)
	if (b) out = out bold_code()
	return out s color_reset()
}
function should_highlight(msg,   n) {
	if (mynick == "") return 0
	n = tolower(mynick)
	return (tolower(msg) ~ ("(^|[^[:alnum:]_])" n "([^[:alnum:]_]|$)"))
}
function label_for(code) {
	if (code == "001") return "WELCOME"
	if (code == "002") return "YOURHOST"
	if (code == "003") return "CREATED"
	if (code == "004") return "MYINFO"
	if (code == "005") return "ISUPPORT"
	if (code == "251") return "LUSER"
	if (code == "252") return "LUSEROP"
	if (code == "253") return "LUSERUNKNOWN"
	if (code == "254") return "LUSERCHANNELS"
	if (code == "255") return "LUSERME"
	if (code == "256") return "ADMINME"
	if (code == "257") return "ADMINLOC"
	if (code == "258") return "ADMINLOC"
	if (code == "259") return "ADMINEMAIL"
	if (code == "263") return "TRYAGAIN"
	if (code == "301") return "AWAY"
	if (code == "302") return "USERHOST"
	if (code == "303") return "ISON"
	if (code == "305") return "UNAWAY"
	if (code == "306") return "NOWAWAY"
	if (code == "311") return "WHOIS"
	if (code == "312") return "WHOIS"
	if (code == "313") return "WHOIS"
	if (code == "317") return "WHOIS"
	if (code == "318") return "WHOIS"
	if (code == "319") return "WHOIS"
	if (code == "321") return "LIST"
	if (code == "322") return "LIST"
	if (code == "323") return "LIST"
	if (code == "331") return "TOPIC"
	if (code == "332") return "TOPIC"
	if (code == "333") return "TOPIC"
	if (code == "341") return "INVITE"
	if (code == "351") return "VERSION"
	if (code == "352") return "WHO"
	if (code == "315") return "WHO"
	if (code == "353") return "NAMES"
	if (code == "366") return "NAMES"
	if (code == "367") return "BANLIST"
	if (code == "368") return "BANLIST"
	if (code == "371") return "INFO"
	if (code == "372") return "MOTD"
	if (code == "374") return "ENDOFINFO"
	if (code == "375") return "MOTD"
	if (code == "376") return "MOTD"
	if (code == "381") return "OPER"
	if (code == "382") return "REHASH"
	if (code == "391") return "TIME"
	if (code == "401") return "NOSUCHNICK"
	if (code == "402") return "NOSUCHSERVER"
	if (code == "403") return "NOSUCHCHANNEL"
	if (code == "404") return "CANNOTSEND"
	if (code == "405") return "TOOMANYCHANNELS"
	if (code == "406") return "WASNOSUCHNICK"
	if (code == "407") return "TOOMANYTARGETS"
	if (code == "409") return "NOORIGIN"
	if (code == "411") return "NORECIPIENT"
	if (code == "412") return "NOTEXTTOSEND"
	if (code == "413") return "NOTOPLEVEL"
	if (code == "414") return "WILDTOPLEVEL"
	if (code == "421") return "UNKNOWNCOMMAND"
	if (code == "422") return "NOMOTD"
	if (code == "423") return "NOADMININFO"
	if (code == "424") return "FILEERROR"
	if (code == "431") return "NONICKNAMEGIVEN"
	if (code == "432") return "ERRONEUSNICKNAME"
	if (code == "433") return "NICKNAMEINUSE"
	if (code == "436") return "NICKCOLLISION"
	if (code == "437") return "UNAVAILRESOURCE"
	if (code == "441") return "USERNOTINCHANNEL"
	if (code == "442") return "NOTONCHANNEL"
	if (code == "443") return "USERONCHANNEL"
	if (code == "444") return "NOLOGIN"
	if (code == "445") return "SUMMONDISABLED"
	if (code == "446") return "USERSDISABLED"
	if (code == "451") return "NOTREGISTERED"
	if (code == "461") return "NEEDMOREPARAMS"
	if (code == "462") return "ALREADYREGISTRED"
	if (code == "463") return "NOPERMFORHOST"
	if (code == "464") return "PASSWDMISMATCH"
	if (code == "465") return "YOUREBANNED"
	if (code == "467") return "KEYSET"
	if (code == "471") return "CHANNELISFULL"
	if (code == "472") return "UNKNOWNMODE"
	if (code == "473") return "INVITEONLYCHAN"
	if (code == "474") return "BANNEDFROMCHAN"
	if (code == "475") return "BADCHANNELKEY"
	if (code == "476") return "BADCHANMASK"
	if (code == "477") return "NOCHANMODES"
	if (code == "478") return "BANLISTFULL"
	if (code == "481") return "NOPRIVILEGES"
	if (code == "482") return "CHANOPRIVSNEEDED"
	if (code == "483") return "CANTKILLSERVER"
	if (code == "484") return "RESTRICTED"
	if (code == "485") return "UNIQOPPRIVSNEEDED"
	if (code == "491") return "NOOPERHOST"
	if (code == "501") return "UMODEUNKNOWNFLAG"
	if (code == "502") return "USERSDONTMATCH"
	return ""
}
function parse(line,   rest, i, part, n, tok) {
	delete params
	prefix = ""
	cmd = ""
	params_n = 0
	trailing = ""
	rest = line
	if (rest ~ /^:/) {
		i = index(rest, " ")
		if (i > 0) {
			prefix = substr(rest, 1, i - 1)
			rest = substr(rest, i + 1)
		} else {
			prefix = rest
			rest = ""
		}
	}
	i = index(rest, " ")
	if (i > 0) {
		cmd = substr(rest, 1, i - 1)
		rest = substr(rest, i + 1)
	} else {
		cmd = rest
		rest = ""
	}
	rest = ltrim(rest)
	if (rest ~ / :/) {
		i = match(rest, / :/)
		part = substr(rest, 1, i - 1)
		trailing = substr(rest, i + 2)
	} else if (rest ~ /^:/) {
		trailing = substr(rest, 2)
		part = ""
	} else {
		part = rest
	}
	part = trim(part)
	if (part != "") {
		n = split(part, tok, /[ ]+/)
		for (i = 1; i <= n; i++) params[i] = tok[i]
		params_n = n
	} else {
		params_n = 0
	}
}
function fmt_numeric(code,   s, target) {
	target = (params_n >= 1 ? params[1] : "")
	s = trailing
	if (code == "005") {
		s = ""
		if (params_n > 1) {
			for (i = 2; i <= params_n; i++) s = s (i > 2 ? " " : "") params[i]
		}
		if (trailing != "") s = s (s != "" ? " " : "") trailing
	}
	if (s == "") {
		s = ""
		if (params_n > 1) {
			for (i = 2; i <= params_n; i++) s = s (i > 2 ? " " : "") params[i]
		}
	}
	s = trim(s)
	if (s == "") {
		s = trim(raw_trailing(line))
	}
	if (s == "") return ""

	if (code == "353") {
		chan = (params_n >= 3 ? params[3] : "")
		return sprintf("NAMES %s: %s", chan, s)
	}
	if (code == "366") {
		chan = (params_n >= 2 ? params[2] : "")
		return sprintf("NAMES: end %s", chan)
	}
	if (code == "332") {
		chan = (params_n >= 2 ? params[2] : "")
		return sprintf("TOPIC %s: %s", chan, s)
	}
	if (code == "331") {
		chan = (params_n >= 2 ? params[2] : "")
		return sprintf("TOPIC %s: %s", chan, s)
	}
	if (code == "333") {
		chan = (params_n >= 2 ? params[2] : "")
		return sprintf("TOPIC %s: %s", chan, s)
	}
	if (code == "322") {
		chan = (params_n >= 2 ? params[2] : "")
		users = (params_n >= 3 ? params[3] : "")
		return sprintf("LIST %s (%s): %s", chan, users, s)
	}
	if (code == "321" || code == "323") {
		return sprintf("LIST: %s", s)
	}
	if (code == "311") {
		nick = (params_n >= 2 ? params[2] : "")
		user = (params_n >= 3 ? params[3] : "")
		host = (params_n >= 4 ? params[4] : "")
		return sprintf("WHOIS %s: %s@%s %s", nick, user, host, s)
	}
	if (code == "312") {
		nick = (params_n >= 2 ? params[2] : "")
		server = (params_n >= 3 ? params[3] : "")
		return sprintf("WHOIS %s: %s %s", nick, server, s)
	}
	if (code == "313") {
		nick = (params_n >= 2 ? params[2] : "")
		return sprintf("WHOIS %s: %s", nick, s)
	}
	if (code == "317") {
		nick = (params_n >= 2 ? params[2] : "")
		idle = (params_n >= 3 ? params[3] : "")
		return sprintf("WHOIS %s: idle %ss %s", nick, idle, s)
	}
	if (code == "318") {
		nick = (params_n >= 2 ? params[2] : "")
		return sprintf("WHOIS %s: %s", nick, s)
	}
	if (code == "319") {
		nick = (params_n >= 2 ? params[2] : "")
		return sprintf("WHOIS %s: %s", nick, s)
	}
	if (code == "352") {
		chan = (params_n >= 2 ? params[2] : "")
		user = (params_n >= 3 ? params[3] : "")
		host = (params_n >= 4 ? params[4] : "")
		nick = (params_n >= 6 ? params[6] : "")
		return sprintf("WHO %s: %s %s@%s %s", chan, nick, user, host, s)
	}
	if (code == "315") {
		return sprintf("WHO: %s", s)
	}
	if (code == "341") {
		nick = (params_n >= 2 ? params[2] : "")
		chan = (params_n >= 3 ? params[3] : "")
		return sprintf("INVITE: %s %s", nick, chan)
	}
	if (code == "372" || code == "375" || code == "376") {
		return sprintf("MOTD: %s", s)
	}

	label = label_for(code)
	if (code ~ /^[4-5][0-9][0-9]$/) {
		return sprintf("ERROR: %s", s)
	}
	if (label != "") {
		return sprintf("%s: %s", label, s)
	}
	return sprintf("INFO: %s", s)
}
function sanitize_line(s) {
	if (s ~ /sh: out of range/) return ""
	if (s ~ /^\[/) return s
	if (s ~ /^\033\[/) return s
	if (s ~ /^\)[^ ]+ quit /) {
		sub(/^\)/, "", s)
		return s
	}
	if (s ~ /^\)[^ ]+ (left|joined) /) sub(/^\)/, "", s)
	if (s ~ /^\)[^ ]+ (left|joined) /) {
		tmp = s
		sub(/^\)/, "", tmp)
		i = index(tmp, " ")
		if (i > 0) {
			nick = substr(tmp, 1, i - 1)
			tmp = substr(tmp, i + 1)
			i = index(tmp, " ")
			if (i > 0) {
				action = substr(tmp, 1, i - 1)
				tmp = substr(tmp, i + 1)
				i = index(tmp, " ")
				if (i > 0) {
					chan = substr(tmp, 1, i - 1)
					reason = substr(tmp, i + 1)
				} else {
					chan = tmp
					reason = ""
				}
				if (reason ~ /^\(.*\)$/) {
					reason = substr(reason, 2, length(reason) - 2)
				} else {
					reason = ""
				}
				if (reason != "" && length(reason) > length(nick)) {
					if (substr(reason, length(reason) - length(nick) + 1) == nick) {
						nick = reason
					}
				}
				s = nick " " action " " chan
				if (reason != "") s = s " " reason
			}
		}
	}
	return s
}
function print_line(s) {
	if (s == "") return
	s = sanitize_line(s)
	if (s != "") print s
}
function limit_with_suffix(s, max,   suffix, keep) {
	if (max <= 0 || length(s) <= max) return s
	suffix = (trunc_suffix != "" ? trunc_suffix : "")
	keep = max - length(suffix)
	if (keep <= 0) return substr(s, 1, max)
	return substr(s, 1, keep) suffix
}
function trim_nick(s) { return limit_with_suffix(s, max_nick) }
function trim_chan(s) { return limit_with_suffix(s, max_chan) }
function limit_msg(s) {
	return limit_with_suffix(s, msg_max)
}
function extract_trailing(raw, cmd,   tmp) {
	tmp = raw
	sub(/^:[^ ]+ /, "", tmp)
	if (cmd != "") sub(cmd " [^ ]+ ?", "", tmp)
	sub(/^:/, "", tmp)
	return trim(tmp)
}
{
	line = $0
	gsub(/\r/, "", line)
	if (line !~ /^:/ && line !~ /^PING /) {
		next
	}
	if (line ~ /^PING /) {
		print_line("** PING " substr(line, 6))
		next
	}
	parse(line)
	if (cmd ~ /^[0-9][0-9][0-9]$/) {
		if (cmd == "001" && params_n >= 1) mynick = params[1]
		out = fmt_numeric(cmd)
		if (out != "") print_line(out)
		next
	}
	if (cmd == "PRIVMSG") {
		target = (params_n >= 1 ? params[1] : "")
		if (trailing == "") trailing = trim(raw_trailing(line))
		trailing = limit_msg(trailing)
		sender = trim_nick(nick_from_prefix(prefix))
		if (should_highlight(trailing) && sender != mynick) {
			sender_colored = colorize(sender, hl_color, hl_bold)
			sender = padnick_colored(sender, sender_colored)
		} else {
			sender = padnick(sender)
		}
		if (is_channel(target)) {
			target = pad16(trim_chan(target))
			if (show_ts) print_line(sprintf("[%s] %s %s : %s", ts(), target, sender, trailing))
			else print_line(sprintf("%s %s : %s", target, sender, trailing))
		} else {
			pm = "[" pm_label "]"
			if (mynick != "" && sender == mynick && target != "") pm = "[" pm_label " " target "]"
			pm = pad16(trim_chan(pm))
			if (show_ts) print_line(sprintf("[%s] %s %s : %s", ts(), pm, sender, trailing))
			else print_line(sprintf("%s %s : %s", pm, sender, trailing))
		}
		next
	}
	if (cmd == "NOTICE") {
		target = (params_n >= 1 ? params[1] : "")
		if (trailing == "") trailing = trim(raw_trailing(line))
		trailing = limit_msg(trailing)
		out = sprintf("-%s- %s", trim_nick(nick_from_prefix(prefix)), trailing)
		if (notice_color != "") out = colorize(out, notice_color, 0)
		print_line(fmt_action(out, ""))
		next
	}
	if (cmd == "JOIN") {
		chan = (trailing != "" ? trailing : (params_n >= 1 ? params[1] : ""))
		if (chan == "") chan = trim(raw_trailing(line))
		out = sprintf("JOIN %s", trim_chan(chan))
		if (action_color != "") out = colorize(out, action_color, 0)
		print fmt_action_line(chan, nick_from_prefix(prefix), out)
		next
	}
	if (cmd == "PART") {
		chan = (params_n >= 1 ? params[1] : "")
		if (chan == "") chan = trim(raw_trailing(line))
		if (trailing == "") trailing = trim(raw_trailing(line))
		trailing = limit_msg(trailing)
		if (trailing != "" && trailing != trim_nick(nick_from_prefix(prefix))) {
			out = sprintf("PART %s %s", trim_chan(chan), quote_reason(trailing))
		} else {
			out = sprintf("PART %s", trim_chan(chan))
		}
		if (action_color != "") out = colorize(out, action_color, 0)
		print fmt_action_line(chan, nick_from_prefix(prefix), out)
		next
	}
	if (cmd == "QUIT") {
		msg = trailing
		if (msg == "") msg = trim(raw_trailing(line))
		msg = limit_msg(msg)
		if (msg != "") out = sprintf("QUIT %s", quote_reason(msg))
		else out = sprintf("QUIT")
		if (action_color != "") out = colorize(out, action_color, 0)
		print fmt_action_line("", nick_from_prefix(prefix), out)
		next
	}
	if (cmd == "NICK") {
		newnick = (trailing != "" ? trailing : (params_n >= 1 ? params[1] : ""))
		if (mynick != "" && nick_from_prefix(prefix) == mynick) mynick = newnick
		out = sprintf("NICK %s", trim_nick(newnick))
		if (action_color != "") out = colorize(out, action_color, 0)
		print fmt_action_line("", nick_from_prefix(prefix), out)
		next
	}
	if (cmd == "TOPIC") {
		chan = (params_n >= 1 ? params[1] : "")
		out = sprintf("TOPIC %s : %s", trim_chan(chan), limit_msg(trailing))
		if (action_color != "") out = colorize(out, action_color, 0)
		print fmt_action_line(chan, nick_from_prefix(prefix), out)
		next
	}
	if (cmd == "MODE") {
		target = (params_n >= 1 ? params[1] : "")
		mode = ""
		if (params_n > 1) {
			for (i = 2; i <= params_n; i++) mode = mode (i > 2 ? " " : "") params[i]
		}
		out = sprintf("MODE %s %s", trim_chan(target), mode)
		if (action_color != "") out = colorize(out, action_color, 0)
		print fmt_action_line(target, nick_from_prefix(prefix), out)
		next
	}

	if (prefix != "") {
		out = nick_from_prefix(prefix) " " cmd
	} else {
		out = cmd
	}
	if (params_n > 0) {
		for (i = 1; i <= params_n; i++) out = out " " params[i]
	}
	if (trailing != "") out = out " :" trailing
	print_line(out)
}
'
