#!/usr/pkg/bin/runawk

# Copyright (c) 2011, Aleksey Cheusov <vle@gmx.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
#       copyright notice, this list of conditions and the following
#       disclaimer in the documentation and/or other materials provided
#       with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#env "LC_ALL=C"

#use "power_getopt.awk"
#use "xgetline.awk"
#use "tmpfile.awk"
#use "shquote.awk"
#use "xclose.awk"
#use "xsystem.awk"
#use "psu_funcs.awk"
#use "pkgsrc-dewey.awk"

#.begin-str help
# pkg_update_plan tries to resolve dependencies and conflicts and
#    outputs a summary of packages to be on the system after update.
# Usage: pkg_update_plan <inst_summary> <avail_summary> packages...
# OPTIONS:
#   -h        display this help
#   -v        verbose mode
#   -i        obtains packages specifications from stdin
#   -t        mark <packages> to be installed with "try out" marker.
#   -l        do not mark leaf packages installed as a dependency for removal
#   -p        do not run REQUIRES/PROVIDES consistency check
#   -r        mark dependencies for install/update
#   -R        mark dependent packages for install/update
#   -B        consider packages different if their BUILD_DATE differ
#   -w        if requested package is not found, write a warning message
#             to stderr and continue to work.
#   -W        if package requested for removal is not found, write a warning message
#             to stderr and continue to work.
# <packages> may be either PKGBASEs, PKGNAMEs or PKGPATHs,
# optionally appended by `-' (marked for removal),
# `+' (auto-installed), or `_' (auto-installed flag is not changed).
# Packages without suffix will be marked as installed by user.
#.end-str

BEGIN {
	if (getarg("h")){
		print_help()
		exitnow(0)
	}
	verbose_mode = getarg("v")
	from_stdin   = getarg("i")
	try_out      = getarg("t")
	keep_leaves  = getarg("l")
	reqd_prov_check = !getarg("p")
	with_deps1   = getarg("r")
	with_deps2   = getarg("R")
	if (getarg("B"))
		cmp_summary_opts = "-b"

	if (ARGC < 3 - from_stdin){
		exit 45
	}

	inst_summary  = ARGV [1]
	avail_summary = ARGV [2]
	ARGV [2] = ""
	ARGV [1] = "-"

	inst_deps = tmpfile()
	inst_deps_ok = 0
}

function indent (str, spaces){
	gsub(/\n/, ("\n" spaces), str)
	return (spaces str)
}

function get_largest_version(base, pkgpath,
        i,best,curr)\
{
	if (! (base in avail_base_cnt))
		return ""

	for (i=1; i <= avail_base_cnt[base]; ++i){
		curr = avail_base2pairs[base, i]

		if (pkgpath != "" && pkgpana2pkgpath(curr) != pkgpath)
			continue

		if (!best || pkgname_gt_pkgname(curr, best))
			best = curr
	}

	return best
}

function get_matched_pkgpair(base, pattern,        i,j,curr,patterns,cnt){
	if (! (base in avail_base_cnt))
		return ""

#	print "base=" base > "/dev/stderr"
#	print "pattern=" pattern  > "/dev/stderr"
	cnt = split(pattern, patterns, /[|]/)
	for (i=1; i <= avail_base_cnt[base]; ++i){
		curr = avail_base2pairs[base, i]
#		print "curr=" curr  > "/dev/stderr"
		for (j=1; j <= cnt; ++j){
			if (pattern_match(pkgname2version(curr), pkgname2version(patterns[j])))
				return curr
		}
	}

	return ""
}

function pkg_not_found (pkg){
	print "Package " pkg " cannot be found" > "/dev/stderr"
	if (!ignore_notfound)
		ex = 1
}

############################################################
# preparations
function process_pkg (pkg, try_out, hash_val, reqd,
      base,suff,pair,rempair,pipe,arr,cmp) \
{
	if (!hash_val)
		hash_val = 0

	if (pkg == "_"){
		# _ means update all package update is available for
		pipe = "pkg_cmp_summary -p " cmp_summary_opts " " inst_summary " " avail_summary
		while (0 < (pipe | getline cmp)){
			if (cmp ~ /^[<!]/){
				split(cmp, arr)
				process_pkg(arr [3] "_")
			}
		}
		return
	}

	if (match(pkg, /([-+_\/]|--)$/)){
		# suffix presents
		suff = substr(pkg, RSTART)
		base = substr(pkg, 1, RSTART-1)
	}else{
		# suffix is absent
		suff = ""
		base = pkg
	}

	bbase = base # PKGBASE if base is PKGNAME
	sub(/-[0-9].*$/, "", bbase)

	if (base != bbase && (bbase in inst_base2pair)){
		rempair = inst_base2pair [bbase]
	}else if (base in inst_base2pair){
		rempair = inst_base2pair [base]
	}else if (base in inst_name2pair){
		rempair = inst_name2pair [base]
	}else if (base in inst_path2pair){
		rempair = inst_path2pair [base]
	}else if (match(base, /;/)){
		rempair = pkgname2pkgbase(substr(base,RSTART + 1))
		if (rempair in inst_base2pair)
			rempair = inst_base2pair [rempair]
		else
			rempair = base
	}else if (suff == "-" || suff == "--"){
		print_eqs(0)
		if (!warning[base]++){
			print "Package " base " is marked for removal but not installed" > "/dev/stderr"
		}
		if (!ignore_notfound_removal) {
			ex = 1
		}
		return
	}

	if (pkgpana2pkgbase(rempair) in inst_base2keep)
		return

	if (suff == "--"){
		if (!inst_deps_ok){
			xsystem("pkg_summary2deps -dnr " inst_summary " > " inst_deps)
			inst_deps_ok = 1
		}
		pipe = "pkg_subgraph_deps -np '" pair2name(rempair) "' < " inst_deps
		while (0 < (pipe | getline pkg)){
			process_pkg(pkgname2pkgbase(pkg) "-")
		}
		return
	}

	if ((base SUBSEP 1) in avail_base2pairs){
		if (reqd == ""){
			if (rempair == "")
				pair = get_largest_version(base)
			else if (! (rempair in inst_pairs))
				pair = get_largest_version(base)
			else if ((rempair in rem_pkgs) && \
					 ! (pkgpana2pkgbase(rempair) in add_base))
			{
				pair = get_largest_version(base)
			}else{
				pair = get_largest_version(base, pkgpana2pkgpath(rempair))
			}
		}else{
			pair = get_matched_pkgpair(base, reqd)

			if (pair == ""){
				bad_package = base
				print_eqs(0)
				pkg_not_found(reqd)
				return
			}
		}
	}else if (base in avail_name2pair){
		pair = avail_name2pair [base]
	}else if (base in avail_path2pair){
		pair = avail_path2pair [base]
	}else if (index(base, ";")){
		pair = base
	}else if (suff != "-"){
		bad_package = base
		print_eqs(0)

		if (base != rempair && sub(/;.*/, "", rempair)){
			print "Update for " base "(" rempair ") cannot be found" > "/dev/stderr"
			ex = 1
		}else{
			pkg_not_found(base)
		}

		return
	}

	if (suff == "/"){
		# do not update
		delete rem_pkgs [pair]
		delete add_pkgs [pair]
		delete add_auto_pkgs [pair]
		delete add_auto_pkgs [pair]
		delete add_base [pkgpana2pkgbase(pair)]
		return
	}

	if (ex)
		return

	base = pair
	sub(/^.*;/, "", base)
	base = pkgname2pkgbase(base)

	if (suff == "_"){
		if (!(base in inst_base2auto) || inst_base2auto [base])
			suff = "+"
		else
			suff = ""
	}

	if (suff == "-"){
		rem_pkgs [rempair] = 0
		for (p in add_auto_pkgs)
			if (pkgpana2pkgbase(p) == base)
				delete add_auto_pkgs[p]
		for (p in add_pkgs)
			if (pkgpana2pkgbase(p) == base)
				delete add_pkgs[p]
		delete add_base [base]
	}else if (suff == "+"){
		if (! (pair in add_pkgs)){
			if (! (pair in add_auto_pkgs))
				add_auto_pkgs [pair] = hash_val
			rem_pkgs [rempair] = hash_val
			add_base [base] = hash_val
		}
	}else{
		delete add_auto_pkgs [pair]
		if (! (pair in add_pkgs))
			add_pkgs [pair] = hash_val
		rem_pkgs [rempair] = hash_val
		add_base [base] = hash_val
		if (try_out)
			try_out_pkgbase [base] = 0
	}
}

function pair2name (pair){
	return substr(pair, index(pair, ";") + 1)
}

function pair2hw (pair,           idx){
	idx = index(pair, ";")
	return substr(pair, idx+1) " (" substr(pair, 1, idx-1) ")"
}

############################################################
# reading and analysing installed and available summaries
BEGIN {
	keep = auto = 0
	while (xgetline0(inst_summary)){
		if ($0 ~ /^PKGNAME=/){
			pkgname = substr($0, 9)
		}else if ($0 ~ /^PKGPATH=/){
			pkgpath = substr($0, 9)
		}else if ($0 ~ /^automatic=/){
			auto = (substr($0, 11) != "")
		}else if ($0 ~ /^keep=/){
			keep = (substr($0, 6) != "")
		}else if ($0 ~ /^PROVIDES=/){
			provides_arr [substr($0, 10)] = 1
		}else if ($0 ~ /^COMMENT=fake package/){
			fake = 1
		}else if (NF == 0){
			pkgbase = pkgname
			sub(/-[^-]*$/, "", pkgbase)
			pkgpair = (pkgpath ";" pkgname)

			for (i in provides_arr){
				inst_provides [i] = pkgpair
			}
			delete provides_arr

			if (fake)
				fake_packages [pkgbase] = 0

			inst_pairs     [pkgpair] = 1
			inst_name2pair [pkgname] = pkgpair
			inst_base2pair [pkgbase] = pkgpair
			inst_path2pair [pkgpath] = pkgpair
			inst_base2auto [pkgbase] = auto
			if (keep)
				inst_base2keep [pkgbase] = 1

			pkgname = pkgpath = ""
			fake = keep = auto = 0
		}
	}
	xclose(inst_summary)

	while (xgetline0(avail_summary)){
		if ($0 ~ /^PKGNAME=/){
			pkgname = substr($0, 9)
		}else if ($0 ~ /^PKGPATH=/){
			pkgpath = substr($0, 9)
		}else if ($0 ~ /^PROVIDES=/){
			provides_arr [substr($0, 10)] = 1
		}else if (NF == 0){
			pkgbase = pkgname
			sub(/-[^-]*$/, "", pkgbase)
			pkgpair = (pkgpath ";" pkgname)

			if (pkgbase in inst_base2pair){
				instpath = inst_base2pair [pkgbase]
				sub(/;.*/, "", instpath)
			}else{
				instpath = pkgpath
			}

			avail_base2pairs[pkgbase, ++avail_base_cnt[pkgbase]] = pkgpair
			avail_path2pair [pkgpath] = pkgpair
			avail_name2pair [pkgname] = pkgpair
			avail_pair [pkgpair] = 0

			pkgname = pkgpath = ""

			for (i in provides_arr){
				if (! (i in avail_provides)){
					avail_provides [i] = pkgpair
				}else{
					add = 1

					if (!index(avail_provides [i], " ")){
						oldname = pair2name(avail_provides [i])
						oldbase = pkgname2pkgbase(oldname)
						newname = pair2name(pkgpair)
						newbase = pkgname2pkgbase(newname)

						if (newbase == oldbase){
							oldver = pkgname2version(oldname)
							newver = pkgname2version(newname)

							add = 0
							if (newver > oldver)
								avail_provides [i] = pkgpair
						}
					}
					if (add)
						avail_provides [i] = avail_provides [i] " " pkgpair
				}
			}
			delete provides_arr
		}
	}
	xclose(avail_summary)
}

function add_deps (summary_fn, dependent,             depsfn){
	depsfn = tmpfile()
	xsystem("pkg_summary2deps -dpnr " summary_fn " > " depsfn)

	cmd = "pkg_subgraph_deps -n " (dependent ? "" : "-r ") " -p ' "
	for (p in add_auto_pkgs){
		if (!add_auto_pkgs [p])
			cmd = (cmd " " p)
	}
	for (p in add_pkgs){
		if (!add_pkgs [p])
			cmd = (cmd " " p)
	}
	cmd = cmd "' " depsfn
	while (0 < (cmd | getline)){
		if (summary_fn == avail_summary || ($1 in avail_pair))
			process_pkg($1 "_", 0, 1)
	}
	xsystem("rm " depsfn)
}

BEGIN {
	update_pkgpath_list = ""

	ignore_notfound = getarg("w")
	ignore_notfound_removal = getarg("W")

	if (from_stdin){
		while (xgetline0()){
			process_pkg($1, try_out)
		}
	}else{
		for (i=(ARGV [3] == "--" ? 4 : 3); i < ARGC; ++i){
			process_pkg(ARGV [i], try_out)
		}
	}

	for (d in fake_packages)
		process_pkg(d "_")

	ignore_notfound = 0

	if (ex)
		exitnow(ex)

	# -r
	if (with_deps1)
		add_deps(avail_summary, 0)

	# -R
	if (with_deps2)
		add_deps(inst_summary, 1)
}

############################################################
function grep_package_cmd (package, field){
	return "pkg_grep_summary -r -fPKGNAME,PKGPATH,DEPENDS,CONFLICTS,COMMENT,BUILD_DATE,REQUIRES,PROVIDES,FILE_NAME -t strlist " \
		field " " shquote(package)
}

function rm (file){
	xsystem("rm " shquote(file))
}

function rem_packages_from_summary (input, output,             pkgs,cmd){
	pkgs = hash_to_indices(rem_pkgs)
	gsub(/;/, ",", pkgs)

	if (pkgs != "")
		# remove specified packages from inst_summary
		cmd = "pkg_grep_summary -v -t strlist PKGPANA '" pkgs "' < "	\
			shquote(input) " > " output
	else
		cmd = ("cp " input " " output)

	system(cmd)
}

function create_packages_summary (output, hash,                  pkgs,cmd)
{
	pkgs = hash_to_indices(hash)
	gsub(/;/, ",", pkgs)

	cmd = grep_package_cmd(pkgs, "PKGPANA") " < " shquote(avail_summary)

	close(output)
	printf "" > output
	while (cmd | getline){
		if (/^PKGNAME=/)
			pkgbase = pkgname2pkgbase(substr($0, 9))

		if (NF == 0){
			print "force_update=yes" > output
			if (pkgbase in try_out_pkgbase)
				print "try_out=yes" > output
			pkgbase = ""
		}

		print $0 > output
	}
	xclose(output)
	xclose(cmd)
}

function hash_to_indices (hash,           i,ret){
	ret = ""
	for (i in hash){
		if (ret == "")
			ret = i
		else
			ret = ret " " i
	}
	return ret
}

function generate_plan (output,           tmpfn,ii,s1)
{
	if (keep_leaves)
		s1 = output
	else
		s1 = tmpfile()

	rem_packages_from_summary(inst_summary, s1)

	for (ii in add_pkgs){
		# add specified packages to inst_summary
		tmpfn = tmpfile()
		create_packages_summary(tmpfn, add_pkgs)

		system("cat " tmpfn " >> " s1)
		rm(tmpfn)

		break
	}

	for (ii in add_auto_pkgs){
		# add specified packages as auto-installed to inst_summary
		tmpfn = tmpfile()
		create_packages_summary(tmpfn, add_auto_pkgs)

		system("awk 'NF == 0 {print \"automatic=yes\"} {print}' " tmpfn " >> " s1)
		rm(tmpfn)
		break
	}

	if (!keep_leaves){
		cmd = "pkg_summary2leaves -vra " s1 " > " output
		system(cmd)
		rm(s1)
	}
}

function get_imghash (){
	return length(hash_to_indices(rem_pkgs)) " " length(hash_to_indices(add_pkgs)) " " length(hash_to_indices(add_auto_pkgs))
}

function add_line_to_hash (hash, idx, value){
	if (++hash [SUBSEP idx SUBSEP value] == 1){ # no repetitions
		if (hash [idx] != "")
			hash [idx] = hash [idx] "\n"

		hash [idx] = hash [idx] value
	}
}

function show_progress (){
	if (verbose_mode){
		printf "    R:%d U:%d A:%d\n", length(rem_pkgs), length(add_pkgs),
			length(add_auto_pkgs) > "/dev/stderr"
		if (verbose_mode > 1){
			printf "rem_pkgs:" > "/dev/stderr"
			for (i in rem_pkgs){
				printf " %s", i > "/dev/stderr"
			}
			printf "\n" > "/dev/stderr"

			printf "add_pkgs:" > "/dev/stderr"
			for (i in add_pkgs){
				printf " %s", i > "/dev/stderr"
			}
			printf "\n" > "/dev/stderr"

			printf "add_auto_pkgs:" > "/dev/stderr"
			for (i in add_auto_pkgs){
				printf " %s", i > "/dev/stderr"
			}
			printf "\n" > "/dev/stderr"

			printf "add_base:" > "/dev/stderr"
			for (i in add_base){
				printf " %s", i > "/dev/stderr"
			}
			printf "\n" > "/dev/stderr"
		}
	}
}

function resolve (flags){
	cmd = "pkg_lint_summary -" flags " " planfn " > " errfn
	if (!system(cmd))
		return 0

	ex = 0
	while (xgetline0(errfn)){
		if (verbose_mode > 1)
			print $0 > "/dev/stderr"

		ex2 = 0
		if ($1 == "d:" && $2 == "not_found"){
			dep1 = $3
			sub(/[|].*$/, "", dep1)
			pkgbase = pkgname2pkgbase($6)
			depbase = pkgname2pkgbase(dep1)

			if (! (depbase in add_base)){
				add_line_to_hash(expl_line, depbase, $0)
				expl_next [depbase] = pkgbase
				addme [depbase] = $3
			}else if (! (pkgbase in add_base)){
				add_line_to_hash(expl_line, pkgbase, $0)
				expl_next [pkgbase] = depbase
				addme [pkgbase] = ""
			}else{
				if (!bad_package)
					print "Update plan cannot be built" > "/dev/stderr"
				add_line_to_hash(expl_line, depbase, $0)
				add_line_to_hash(expl_line, pkgbase, $0)
				bad_package = pkgbase
				ex = ex2 = 1
			}
		}else if ($1 == "c:" && $2 == "conflict"){
			pkgbase = pkgname2pkgbase($8)
			cnfbase = pkgname2pkgbase($5)

			if ((pkgbase in add_base) && !(cnfbase in add_base)){
				add_line_to_hash(expl_line, cnfbase, $0)
				expl_next [cfgbase] = pkgbase
				if (! (cnfbase in addme))
					addme [cnfbase] = ""
			}else{
				if (!bad_package)
					print "Update plan cannot be built" > "/dev/stderr"
				add_line_to_hash(expl_line, cnfbase, $0)
				add_line_to_hash(expl_line, pkgbase, $0)
				bad_package = pkgbase
				ex = ex2 = 1
			}
		}else if ($1 == "u:" && $2 == "unicity"){
			pkgbase = $3

			if (!bad_package)
				print "Update plan cannot be built" > "/dev/stderr"
			add_line_to_hash(expl_line, pkgbase, $0)
			expl_next [pkgbase] = ""
			bad_package = pkgbase
			ex = ex2 = 1
		}else if ($1 == "l:" && $2 == "not_found"){
			pkgbase = pkgname2pkgbase($5)

			if (!(pkgbase in add_base)){
				add_line_to_hash(expl_line, pkgbase, $0)
				expl_next [pkgbase] = pkgpana2pkgbase(inst_provides [$3])
				if (! (pkgbase in addme))
					addme [pkgbase] = ""
			}else if (($3 in avail_provides) &&
					  !index(avail_provides [$3], " "))
			{
				# mark package as installed by user, otherwise it will be removed
				# from update plan as an auto-installed leaf
				pkg = avail_provides [$3]
				add_line_to_hash(expl_line, pkg, $0)
				expl_next [pkg] = pkgpana2pkgbase(inst_provides [$3])
				addme [pkg] = "1"
			}else{
				print_eqs(0)
				if ($3 in avail_provides){
					printf "%s is provided by the following packages: %s\n", $3, avail_provides [$3] > "/dev/stderr"
					printf "Select one of them manually\n" > "/dev/stderr"
				}else{
					printf "%s is required by %s\n   but is not provided by any package\n", $3, $5 > "/dev/stderr"
				}
				bad_package = pkgbase
				ex = ex2 = 1
			}
		}else if ($1 == "L:" && $2 == "not_found"){
			print "This should not happen" > "/dev/stderr"
			ex = ex2 = 1
		}else{
			print "This should not happen" > "/dev/stderr"
			ex = ex2 = 1
		}
	}
	xclose(errfn)

	return 1
}

function print_eqs (force){
	if (force || !eqs_printed)
		print "==========================================" > "/dev/stderr"
	eqs_printed = 1
}

function debug_expl (i){
	print "expl_line:" > "/dev/stderr"
	for (i in expl_line){
		printf "   expl_line [%s]=%s\n", i, expl_line [i] > "/dev/stderr"
	}

	print "expl_next:" > "/dev/stderr"
	for (i in expl_next){
		printf "   expl_next [%s]=%s\n", i, expl_next [i] > "/dev/stderr"
	}
}

function add_base2pair (pkgbase,                i)
{
	for (i in add_pkgs){
		if (pkgpana2pkgbase(i) == pkgbase)
			return i
	}

	for (i in add_auto_pkgs){
		if (pkgpana2pkgbase(i) == pkgbase)
			return i
	}

	return ""
}

function show_trace (){
	if (verbose_mode > 1){
		printf "bad_package = %s\n", bad_package > "/dev/stderr"
		debug_expl()
	}

	msg = ""
	print_eqs(1)
	print "Details:" > "/dev/stderr"
	pkg = bad_package
	while (pkg){
		old_name = pair2name(inst_base2pair [pkg])
		new_name = pair2name(add_base2pair(pkg))
		if (!new_name)
			new_name = "?"
		msg = (indent(expl_line [pkg], "   ") "\n" msg)
		msg = (old_name " --> " new_name "\n" msg)
		pkg = expl_next [pkg]
	}
	print msg
}

BEGIN {
	planfn = tmpfile()
	errfn  = tmpfile()

	old_imghash = get_imghash()

	while (1){
		show_progress()

		for (i in add_pkgs){
			assert(!(i in add_auto_pkg))
		}

		generate_plan(planfn)
		if (!resolve("dcu") && (!reqd_prov_check || !resolve("l")))
			break

		if (ex)
			break

		for (pkg in addme){
			if (addme [pkg] == "1")
				process_pkg(pkg)
			else
				process_pkg(pkg "_", 0, "", addme [pkg])
		}
		delete addme

		new_imghash = get_imghash()
		if (old_imghash == new_imghash){
			break
		}

		old_imghash = new_imghash
	}

	if (ex){
		show_trace()

		if (verbose_mode > 2){
			system("cat " planfn " 1>&2")
		}
		exitnow(1)
	}else{
		exitnow(system("cat " planfn))
	}
}
