#!/bin/gawk -f
#------------------------------------------------------------------------------
# packages/
# packages/groups
# packages/groups/<group>
# packages/groups/<group>/<package>
#                         :version      :ro: serial-version-release
#                         :size         :ro: ditto
#                         :summary      :ro: ditto
#                         :description  :ro: ditto
# ...                     :tbd          ...
# packages/pending
# packages/pending/install
# packages/pending/install/<package>    -> ../../groups/*/<package>
# packages/pending/remove
# packages/pending/remove/<package>     -> ../../groups/*/<package>
# packages/pending/upgrade
# packages/pending/upgrade/<package>    -> ../../groups/*/<package>
# packages/pending/keep
# packages/pending/keep/<package>       -> ../../groups/*/<package>
# packages/search
# packages/search/<key>
# packages/search/<key>/<package>       -> ../../groups/*/<package>
# packages/state
# packages/state/<package>
#                         :state        :r : available/be-removed/be-installed/installed
#                                       :w : install/remove
# packages/control
#                         :install      :ro: packages # to install
#                         :remove       :ro: -//-     # to remove
#                         :keep         :ro: -//-     # to keep
#                         :dsize        :ro: total    amount to download
#                         :isize        :ro: total    size to be used
#                         :control      :wo: -//-     abort/commit/continue/update/quit
# ...                     :tbd          ...
# packages/sources        :source<n>    :ro: sources.list entries
# packages/cdrom          :ident        :ro: currently inserted cdrom ident, if any
#------------------------------------------------------------------------------

# {{{ 

func _nil() { return "()" }
func _bool(v) { return v ? "#f" : "#t" }
func _quote (v) { return "(quote " v ")" }
func _list(v,	i, s) { for (i in v) s = s "(\"" v[i] "\")\n"; return "(\n" s ")" }
func _plist(v,	i, s) { for (i in v) s = s i " \"" v[i] "\"\n"; return "(\n" s ")" }
func _alist(v, key,	i, s) { for (i in v) s = s "(\"" i "\" " key " \"" v[i] "\")\n"; return "(\n" s ")" }

#---------------------------------------------------------------
func notify(address, command, arg) {
	logdebug("^" address ":" command ":" arg)
	printf "alterator-mailbox-send '\"%s\" \"%s\" \"%s\"'\n", address, command, arg | PIPE
	fflush(PIPE)
}

func request(v,		objects, action, i, y, z)
{
	split("", v)
	logdebug("/===============================\n" $0 "--------------------------------")
	split($0, z, "\n")
	for (i in z) {
		switch (z[i]) {
			case /^_message:.+$/ : { continue }
			case /^[[:blank:]]*$/ : { continue }
			case /^_objects:.+$/ : { v["objects"] = gensub(/^_objects:/, "", "", z[i]); continue }
			default : { match(z[i], /^([^:]+):(.+)$/, y); v[y[1]] = y[2] }
		}
	}
	logvdebug(v)
	logdebug("================================/")
	return v["action"] "," v["objects"]
}

func logerr(s) { printf "%s\n", s > "/dev/stderr" }
func logdebug(s) {if (LOGDEBUG) logerr(s)}
func logvdebug(v, i, s) {if (LOGDEBUG) {for (i in v) s = s i " => '" v[i] "'\n"; printf "%s", s > "/dev/stderr"}}

func responce(s) { logdebug("<*" s); printf "%s\n", s; fflush("/dev/stdout") }
func responce_nil() { responce(_nil()) }
func responce_list(v) {  responce(_list(v)) }
func responce_plist(v) { responce(_plist(v)) }
func responce_alist(v, key) { responce(_alist(v, key)) }

# }}}
# {{{ misc

func onerror(cmd)
{
	cmd = "apt-pipe errors"
    while ((cmd | getline) > 0)	logerr($0)
	close(cmd)
}

func closepipe(cmd)
{
	while((cmd | getline) > 0);
	if (close(cmd)) onerror()
}

# }}}
# {{{ state/<package>

func names(v,	rs, cmd, vv)
{
	cmd = "apt-pipe ls"
	while ((cmd | getline) > 0)	++vv[$1]
	split("", v)
	asorti(vv, v)
	RS = rs
	return close(cmd)
}

func setstate(v, cmd)
{
	cmd = "apt-pipe " v["state"] " -y " gensub(/^.+\//, "", "", v["objects"])
    while ((cmd | getline) > 0);	
	return close(cmd)
}

func state(v,	name, rs, vv)
{
	rs = RS
	name = gensub(/^.+\//, "", "", v["objects"])
	split("", v)

	RS = ""
	v["state"] = "unavailable"
	cmd = "apt-pipe ls " name
	if ((cmd | getline) > 0 && name == $1)
		v["state"] = "available"

	RS = rs
	if (close(cmd) != 0) return 1

	if (v["state"] == "unavailable") {
		RS = ""
		cmd = "apt-pipe ls " name "*"
		if ((cmd | getline) > 0 && $1 ~ ("^" name "#")) {
			name = $1
			v["state"] = "available"
		}
		while((cmd | getline) > 0);
		RS = rs
		if (close(cmd) != 0) return 1
	}

	RS = ""
	cmd = "apt-pipe ls -i " name
	if ((cmd | getline) > 0 && name == $1)
		v["state"] = "installed"

	RS = rs
	if (close(cmd) != 0) return 1


	RS = ""
	cmd = "apt-pipe ls -u " name
	if ((cmd | getline) > 0 && name == $1)
		v["state"] = "upgradable"

	RS = rs
	if (close(cmd) != 0) return 1

	RS = "\n[[:alnum:]]"
	cmd = "apt-pipe status"
	split("", vv)
	while ((cmd | getline) > 0) {
		if (match($0, /he.+(REMOVED|installed|upgraded)[[:print:]]*[^[:print:]]+(.+)/, vv)) {
			(vv[2] " ") ~ ("[[:space:]]" name "[[:space:]]") && \
				v["state"] = "be-" tolower(vv[1])
		}
	}
	RS = rs
	return close(cmd)
}

# }}}
# {{{ pending

func getpending(v,	cmd, i, j, e, rs, fs, vv)
{
	rs = RS
	fs = FS

	RS = "[[:digit:]]+[[:blank:]]upgraded,[[:blank:]][^[:cntrl:]]+upgraded\\.[[:cntrl:]]"
	FS = "(WARNING: )*The[[:print:]]+be[[:space:]]"

	i = (cmd | getline)
	RS = rs
	FS = fs

	if (i < 0) return 1

	split("", v)
	split("", vv)

	for (i=2; i<=NF; i++) {
		sub(/^upgraded[[:space:]]+/, "upgrade ", $i)
		sub(/^REMOVED:[[:space:]]+/, "remove ", $i)
		sub(/^REPLACED:[[:space:]]+/, "install ", $i)
		sub(/^installed:[[:space:]]+/, "install ", $i)
		sub(/^removed.+are doing!/, "essential ", $i)
		gsub(/\([^\)]+\)?/, "", $i)
		split($i, vv)
		m = vv[1]
		delete vv[1]
		j = 0
		for (e in vv) v[m "," j++] = vv[e]
	}
	logvdebug(v)
}

func findpending(v, name, e)
{
	for (e in v)
		if (name == v[e]) {
			delete v[e]
			return name
		}
	for (e in v)
		if (index(v[e], name) == 1) {
			name = v[e]
			delete v[e]
			return name
		}
	return ""
}

func pending_not_upgraded(v, name, e)
{
	for (e in v)
		if (e ~ /^upgrade/ && v[e] == name)
			return 0
	return 1
}

func pending(v, i, j, rs, cmd)
{
	cmd = CMDDEBUG ? "cat " DEBUG_ARG : "apt-pipe status"
	getpending(v, cmd)

	while((cmd | getline) > 0);
	if (close(cmd)) return 1

	rs = RS
	cmd = "apt-pipe ls -u"
	RS = ""
	if ((cmd | getline) > 0)
		for (i=1; i <= NF; i++)
			if (pending_not_upgraded(v, $i))
				v["keep" "," j++] = $i
	RS = rs
	return (close(cmd))
}

func pending_groups(v,	e, p, vv)
{
	split("", v)
	split("", p)
	split("", vv)
	if (pending(p)) return 1
	for (e in p) ++vv[gensub(/,.+$/, "", "", e)]
	asorti(vv, v)
	return 0
}

func pending_group(v, i, e, group, vv)
{
	group = gensub(/^.+\//, "", "", v["objects"])

	split("", v)
	split("", vv)
	if (pending(vv)) return 1

	for (e in vv)
		if (gensub(/,.+$/, "", "", e) == group)
			v[i++] = vv[e]
}

# }}}
# {{{ packages/groups

func groups(v,	fs, rs, cmd, vv)
{
	rs = RS
	fs = FS
	RS = "\n"
	FS = "\t"
	split("", v)
	split("", vv)
	cmd = "apt-pipe ls -g"
	while ((cmd | getline) > 0) {
		++vv[gensub(/ /, "\\\\ ", "g", gensub(/\//, "|", "g", $1))]
	}
	asorti(vv, v)

	FS = fs
	RS = rs
	return close(cmd)
}

func groups_group(v,	i, rs, cmd)
{
	rs = RS
	cmd = "apt-pipe ls -G " \
		gensub(/ /, "\\\\ ", "g", gensub(/\|/, "/", "g", gensub(/^.+\//, "", "", v["objects"])))

	RS = ""
	cmd | getline
	RS = rs

	split("", v)
	for (i = 1; i < NF; i += 2)	v[$i] = $(i+1)

	return close(cmd)
}

# }}}
# {{{ packages/groups/.../name

func package(v,	rs, info, name, cmd, re)
{
	rs = RS
	name = gensub(/^.+\//, "", "", v["objects"])
	cmd = "apt-pipe show " name
	re = "^.+"															\
		"\\<Installed Size:[[:space:]]+([[:digit:]]+).+"				\
		"\\<Version:[[:space:]]+([^[:space:]]+).+"						\
		"\\<Description:[[:space:]]+([[:print:]]+)"						\
		"(.+)$"

	RS = ""
	# FIXME no more than one, most recent, package
	cmd | getline
	split("", v)
	split(gensub(re, "\\1\0\\2\0\\3\0\\4", "", $0), info, "\0")
	v["size"] = info[1]
	v["version"] = info[2]
	v["summary"] = info[3]
	v["description"] =  gensub(/\n/, "\\\\n", "g", gensub(/^\n /, "", "", gensub(/\"/, "\\\\\"", "g", info[4])))

	# maybe-suck in rest
	while ((cmd | getline) > 0);
	RS = rs
	return close(cmd)
}

# }}}
# {{{ control

func getcontrol(v,	cmd, re0, re1, re2, vv)
{
	split("", v)
	split("", vv)

	cmd = "apt-pipe ls"
	while ((cmd | getline) > 0)
		v["available"] += NF
	close(cmd)
	
	cmd = "apt-pipe ls -i"
	while ((cmd | getline) > 0)
		v["installed"] += NF
	close(cmd)

	cmd = CMDDEBUG ? "cat " DEBUG_ARG : "apt-pipe status"
 	re0 = "^"										\
 		"([[:digit:]]+)[[:space:]][^[:digit:]]+"	\
 		"([[:digit:]]+)[[:space:]][^[:digit:]]+"	\
 		"([[:digit:]]+)[[:space:]][^[:digit:]]+"	\
 		"([[:digit:]]+)[[:space:]][^[:digit:]]+"	\
 		"([[:digit:]]+)[[:space:]].+$"
 	re1 = "^"										\
 		"([[:digit:]]+)[[:space:]][^[:digit:]]+"	\
 		"([[:digit:]]+)[[:space:]][^[:digit:]]+"	\
 		"([[:digit:]]+)[[:space:]][^[:digit:]]+"	\
 		"([[:digit:]]+)[[:space:]].+$"
	re2 = "^Will[[:space:]]need[[:space:]]"		\
		"([[:digit:]]+[^[:space:]]+).+$"
	re3 = "^After[[:space:]]unpacking[[:space:]]"	\
		"([^[:space:]]+)[[:space:]](of|disk).+$"

	while ((cmd | getline) > 0) {
		if ($0 ~ re0) {
			split(gensub(re0, "\\1\0\\2\0\\3\0\\4", "", $0), vv, "\0")
			v["upgrade"] = vv[1] + vv[3]
			v["install"] = vv[2]
			v["remove"] = vv[4] + vv[3]
			v["keep"] = vv[5]
		} else if ($0 ~ re1) {
			split(gensub(re1, "\\1\0\\2\0\\3\0\\4", "", $0), vv, "\0")
			v["upgrade"] = vv[1]
			v["install"] = vv[2]
			v["remove"] = vv[3]
			v["keep"] = vv[4]
		}
		if ($0 ~ re2) {
			v["dsize"] = gensub(re2, "\\1", "", $0)
		}
		if ($0 ~ re3) {
			split(gensub(re3, "\\1\0\\2", "", $0), vv, "\0")
			v["isize"] = vv[1]
			v["foobaz"] = vv[2]
			break
		}
	}

	closepipe(cmd)
}

func setcontrol(v)
{
	switch (v["control"]) {
		case /^abort$/ : { notify("progress", "abort"); break }
		case /^continue$/ : { notify("progress", "end"); break }
		case /^quit$/ : { system("apt-pipe quit"); break }
		case /^update$/ : {	control_update(); break }
		case /^commit$/ : {	control_commit(); break }
	}
}

func control_update(v, cmd, cnt, err, end)
{
	split("", v)
	notify("progress", "begin")
	if (listsources(v)) {
		notify("progress", "abort")
		return
	}

	cmd = "LC_ALL=C cat"
	for (i in v) cmd = cmd " " v[i]
	while((cmd | getline) > 0)
		if (/^rpm / && ! /^rpm[[:blank:]]+cdrom:/)
			cnt += 2 * (NF - 3)
	close(cmd)

	if (cnt == 0) {
		notify("progress", "end")
		return
	}

	end = 0
	err = 0
	notify("progress", "update", cnt)
#	cmd = CMDDEBUG ? "cat " DEBUG_ARG : "apt-pipe update -q |tee /var/cache/alterator/update.log"
	cmd = CMDDEBUG ? "cat " DEBUG_ARG : "apt-pipe update -q"
	while ((cmd | getline) > 0) {
		if (/^(Get|Hit)/) {
			notify("step", gensub(/^(Get:[0-9]+|Hit) /, "", ""), 1)
			continue
		}
		if (/^Building Dependency Tree/) {
			end++
			continue
		}
		if (/^Err/) {
			err++
			errstr = gensub(/^Err /, "", "")
			cmd | getline
			if (! /^[[:blank:]]+Please use apt-cdrom/)
				notify("error", "update", errstr)
			continue
		}
	}

	close(cmd)
	notify("progress", "end")
	if (end == 0 || err > 0) onerror()
}

func control_commit(v,	e, cmd, install, download, units, vv)
{
	install = 0
	download = 0
	split("", v)
	notify("progress", "begin")

#	cmd = CMDDEBUG ? "cat " DEBUG_ARG : "apt-pipe commit -qy|tee /var/cache/alterator/packages.log"
	cmd = CMDDEBUG ? "cat " DEBUG_ARG : "apt-pipe commit -qy"
	if (getpending(v, cmd))
		return progress_error(cmd, "assert", "pending")

	for (e in v) e ~ /install|upgrade/ && install++

	if ((cmd | getline) && /^Need to get/) {
		if (match($4, /^([0-9]+)([^\/]+)(\/|$)/, vv) && vv[1] != "0") {
			download = 1
			units = vv[2]
			notify("progress", "download", vv[1] * 2)
		}
	} else
		return progress_error(cmd, "assert", "get")

	if (! ((cmd | getline) > 0 && /^After unpacking/))
		return progress_error(cmd, "assert", "unpacking")
	if (download && progress_download(cmd, units))
		return progress_error(cmd, "assert", "download")

	cmd | getline
	if (/^There are broken packages/ ||
		/^You can try to fix/)
		return progress_error(cmd, "state", "unmet")
	if ($1 ~ /Err/)
		return progress_error(cmd, "missing", $4 "-" $5)
	if (! /^Committing changes/)
		return progress_error(cmd, "assert", "commit")

	cmd | getline
	if (! /^Preparing/)
		return progress_error(cmd, "assert", "preparing")
	if (install && progress_install(v, cmd, install))
		return progress_error(cmd, "assert", "install")

	notify("progress", "end")
	
	# suck in the rest, if any
	while ((cmd | getline) > 0);
	return close(cmd)
}

func progress_download(cmd, units, vv)
{
	while ((cmd | getline) > 0) {
		if (/^Fetched/)
			return 0
		if (/^There are broken/)
			return 0
		if ($1 ~ /Get:[0-9]+/) {
			match($(NF), /^\[([0-9]+)([0-9\.]*)(.+)\]$/, vv) &&
				notify("step", $(NF-2), vv[3] == units ? vv[1] : "1")
		} else if ($1 ~ /Err/) {
			cmd | getline
		} else if ($1 ~ /Failed/) {
			notify("error", "download", $4)
		}
	}
	return 1
}

func progress_install(v, cmd, install,	n, e)
{	
	notify("progress", "install", install)

	for (e in v)
		if (e ~ /^remove/)
			delete v[e]

	while ((cmd | getline) > 0) {
		if (/^Done\.$/)
			return 0
		if (/^Reading Package Lists/) {
			notify("error", "state", "unfinished")
			return 0
		}

		n = findpending(v, gensub(/^([^[:blank:]\#]+).+$/, "\\1", ""))
		if (n != "") {
			sub(/^[^[:blank:]\#]+[[:blank:]\#]*/, "")
			notify("step", n, 1)
			if ($0 != "") notify("error", "install", $0)
		}
	}
	return 1
}

func progress_error(cmd, errorcode, errorinfo)
{
	while ((cmd | getline) > 0);
	close(cmd)
	onerror()
	notify("error", errorcode, errorinfo)
	notify("progress", "end")
}

# }}}
# {{{ sources

func aptconfig(v,	cmd)
{
	split("", v)
	cmd = "apt-config dump"
	while ((cmd | getline) > 0) {
		if (/^Dir /)
			v["root"] = gensub(/^.+"([^\"]+)".+$/, "\\1", "")
		if (/^Dir::Etc /)
			v["etc"] = gensub(/^.+"([^"]+)".+$/, "\\1", "")
		if (/^Dir::Etc::main/)
			v["main"] = gensub(/^.+"([^"]+)".+$/, "\\1", "")
		if (/^Dir::Etc::parts/)
			v["parts"] = gensub(/^.+"([^"]+)".+$/, "\\1", "")
		if (/^Dir::Etc::sourcelist /)
			v["sourceslist"] = gensub(/^.+"([^"]+)".+$/, "\\1", "")
		if (/^Dir::Etc::sourceparts /)
			v["sourcesparts"] = gensub(/^.+"([^"]+)".+$/, "\\1", "")
		if (/^Dir::Etc::vendorlist /)
			v["vendorlist"] = gensub(/^.+"([^"]+)".+$/, "\\1", "")
		if (/^Dir::Etc::vendorparts /)
			v["vendorparts"] = gensub(/^.+"([^"]+)".+$/, "\\1", "")
		if (/^Acquire::CDROM::mount/)
			v["mountpoint"] = gensub(/^.+"([^"]+)".+$/, "\\1", "")
	}

	return close(cmd)
}

func listsources(v, i, cmd)
{
	if (aptconfig(cf)) return 1

	i = 0
	split("", v)
	v[i] = cf["root"] cf["etc"] cf["sourceslist"]
	cmd = "LC_ALL=C ls -1 " cf["root"] cf["etc"] cf["sourcesparts"] "/*.list 2> /dev/null"
	while ((cmd | getline) > 0)	v[++i] = $0
	close(cmd)
	return 0
}

func getsources(v,	i, cmd, re, mode)
{
	mode = gensub(/^.+\//, "", "", v["objects"])

	split("", v)
	if (listsources(v)) return 1
	cmd = "cat"
	for (i in v) cmd = cmd " " v[i]

	re = "[[:blank:]]*(rpm|rpm-src)[[:blank:]]+(\\[[^]]+\\])*[[:blank:]]*(.+[[:alnum:]])[[:blank:]]*$"
	re = (mode == "active") ? "^" re : "^#+" re

	i = 0
	split("", v)
	while ((cmd | getline) > 0) {
		if ($0 !~ re) continue
		v[i++] = gensub(re, "\\3", "")
	}
	return (close(cmd))
}

# }}}
# {{{ cdrom

func getcdrom(v, cmd)
{
	split("", v)
	cmd = "LC_ALL=C apt-cdrom -m ident 2>&1"
	while((cmd | getline) > 0) {
		# Using CD-ROM mount point /media/cdrom/
		# Mounting CD-ROM
		# Identifying.. [3d711f892b8061b68e7cb8ab466289c2-2]
		# Stored Label: 'ALT Linux 2.9.9.7 Installer (caprifoil)'
		if (/^Identifying/)	v["identity"] = gensub(/^.+\[([^\]]+).+$/, "\\1", "")
		if (/^Stored Label/) v["label"] = gensub(/^.+'([^']*)'.*$/, "\\1", "")
	}
	close(cmd)
	return 0
}

# }}}
# {{{ search

func search(v, cmd, fs, name, group, i)
{
	cmd = "apt-pipe search " gensub(/^.+\//, "", "", v["objects"])
	while ((cmd | getline) > 0)	name[$1] = 1
	if (close(cmd) != 0) return 1

	fs = FS
	FS = "\t"
	cmd = "apt-pipe ls -g"
	while ((cmd | getline) > 0) if (name[$2]) ++group[$1]
	if (close(cmd) != 0) {
		FS = fs
		return 1
	}

	split("", v)
	for (i in group) {
		cmd = "apt-pipe ls -G " i
		while ((cmd | getline) > 0)
			if (name[$1]) v[$1] = $2
		close(cmd)
	}
	FS = fs
	return 0
}

# }}}
# {{{ 

BEGIN {
	LOGDEBUG=0
	PIPE = "sh"
	print "/usr/lib/rpm/pdeath_execute " PROCINFO["pid"] " /usr/bin/apt-pipe quit" | PIPE
	RS = "_message:end"
}

{
	R = request(V)
	RS = "\n"
	logdebug("*>" R)
	switch (R) {
#-- groups
		case /^list,groups$/ : { groups(V) || responce_list(V); break }
		case /^list,groups\/[^\/]+$/ : { groups_group(V) || responce_alist(V, "state"); break }
		case /^read,groups\/[^\/]+\/[^\/]+$/ : { package(V) || responce_plist(V); break }
#-- search
		case /^list,search$/ : { responce_nil(); break }
		case /^list,search\/[^\/]+$/ : { search(V) || responce_alist(V, "state"); break }
		case /^read,search\/[^\/]+\/[^\/]+$/ : { package(V) || responce_plist(V); break }
#-- state
		case /^list,state$/ : { names(V) || responce_list(V); break }
		case /^read,state\/[^\/]+$/ : { state(V) || responce_plist(V); break }
		case /^write,state\/[^\/]+$/ : { setstate(V) || responce_nil(); break }
#-- pending
		case /^list,pending$/ : { pending_groups(V) || responce_list(V); break }
		case /^list,pending\/[^\/]+$/ : { pending_group(V) || responce_list(V); break }
		case /^read,pending\/[^\/]+\/[^\/]+$/ : { package(V) || responce_plist(V); break }
#-- control
		case /^read,control$/ : { getcontrol(V) || responce_plist(V); break }
		case /^write,control$/ : { responce_nil(); setcontrol(V); break }
#-- sources
		case /^list,sources$/ : { split("active passive vendors", V); responce_list(V); break }
		case /^read,sources\/vendors$/ : { getvendors(V) || responce_plist(V); break }
		case /^list,sources\/(active|passive)$/ : { getsources(V) || responce_list(V); break }
#-- cdrom
		case /^read,cdrom\/ident$/ : {getcdrom(V) || responce_plist(V); break }
#-- rest
		case /^,$/ : { break }	# EOF
		default : {
			logerr("!>" R)
			responce("#f")
		}
	}
	onerror()
	RS = "_message:end"
}

END {
	close(PIPE)
}

# }}}

#------------------------------------------------------------------------------
# Local variables:
# mode: awk
# mode: folding
# End:
