#!/usr/bin/python3
# Copyright 2014,2015 Bernhard R. Link <brlink@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
# SOFTWARE IN THE PUBLIC INTEREST, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.


import argparse
import os
import sys
import subprocess

libdirdefault = './scan' # replaced by install script, so be careful
sharedirdefault = './rules' # replaced by install script, so be careful

def parse_args():
	parser = argparse.ArgumentParser(description="scan build logs for possible problems")
	parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False)
	parser.add_argument("--libdir", type=str, dest="libdir", default=libdirdefault)
	parser.add_argument("--sharedir", type=str, dest="sharedir", default=sharedirdefault)
	commands = parser.add_subparsers(dest="command")
	check_parser = commands.add_parser("check", help="check the given build logs")
	check_parser.add_argument("--compiledrulesfile", type=str, dest="rulesfile")
	check_parser.add_argument("--blhc", type=str, dest="blhc", help="path to blhc binary ('NO' to disable calling it)")
	check_parser.add_argument("filename", type=str, nargs='+', help="build logs to scan")
	update_parser = commands.add_parser("compile", help="update rule definitions")
	update_parser.add_argument("-o", "--compiledrulesfile", type=str, dest="rulesfile")
	update_parser.add_argument("-p", "--ignorecheckout", action="store_true", default=False, dest="ignorecheckout")
	update_parser.add_argument("-N", "--no-defaults", action="store_true", default=False, dest="nodefaults")
	update_parser.add_argument("rules", type=str, nargs='*', help="rule files or directories containing them")
	update_parser = commands.add_parser("checkout", help="checkout current rule definitions")

	args = parser.parse_args()
	if not args.command:
		parser.error("No command given.")
	return args

def cachedir(args):
	h = os.environ.get("HOME", None)
	if h:
		c = os.path.join(h, ".bls-cache")
		if os.path.exists(c):
			return c
	c = os.environ.get("XDG_CACHE_HOME", None)
	if not c and h:
		c = os.path.join(h, ".cache")
	if c:
		return os.path.join(c, "bls")
	return None

def rulesfile(args):
	if args.rulesfile:
		if os.path.exists(args.rulesfile):
			return args.rulesfile
		else:
			return None
	cache_dir = cachedir(args)
	if cache_dir and os.path.exists(cache_dir):
		rfn = os.path.join(cache_dir, "rules.compiled")
		if os.path.exists(rfn):
			return rfn
	rfn = os.path.join(args.sharedir, "rules.compiled")
	if os.path.exists(rfn):
		return rfn
	return None

def rulesources(args):
	sources = []
	for fn in args.rules:
		sources.append((fn, "command line"))
	if not args.nodefaults:
		h = os.environ.get("HOME", None)
		if h:
			sources.append((os.path.join(h, ".bls", "rules"), "config"))
		c = os.environ.get("XDG_CONFIG_HOME", None)
		if not c and h:
			c = os.path.join(h, ".config")
			if not os.path.exists(c):
				c = None
		if c:
			sources.append((os.path.join(c, "bls", "rules"), "config"))
		cd = cachedir(args)
		checkoutdir = os.path.join(cd, "checkout")
		if args.ignorecheckout or not os.path.exists(checkoutdir):
			sources.append((args.sharedir, "shipped"))
		else:
			sources.append((checkoutdir, "checkout"))
	return sources

def do_compile(args):
	results = []
	rules = {}
	for source, typ in rulesources(args):
		if not os.path.exists(source):
			if typ == "command line":
				print("No such file or directory: %s" % repr(source), file=sys.stderr)
				sys.exit(2)
			else:
				results.append((source, typ, "directory ignored as not found"))
				continue
		if not os.path.isdir(source):
			if typ != "command line":
				results.append((source, typ, "ignored, not a directory"))
				continue
			filename = os.path.basename(source)
			if filename in rules:
				print("WARNING: command line argument %s ignored (as same nameas %s)" % (repr(source), repr(rules[filename])), file=sys.stderr)
				results.append((source, typ, "ignored"))
			else:
				rules[filename] = source
				results.append((source, typ, "rules file"))
			continue
		ignoredCount = 0
		usedCount = 0
		foundCount = 0
		for dirpath, dirnames, filenames in os.walk(source, topdown=True):
			dirnames = True
			for filename in filenames:
				if not (filename.endswith(".description") or filename.endswith(".switch")):
					continue
				if filename in rules:
					ignoredCount = ignoredCount + 1
					continue
				rules[filename] = os.path.join(dirpath, filename)
				usedCount = usedCount + 1
		if ignoredCount:
			results.append((source, typ, "directory with %d rules file (%d more ignored as shadowed by files of the same name in different locations)" % (usedCount, ignoredCount)))
		else:
			results.append((source, typ, "directory with %d rules file" % usedCount))
	if args.verbose:
		for source, typ, description in reversed(results):
			print("%s [%s]: %s" % (source, typ, description))

	rfn = args.rulesfile
	if rfn is None:
		rfn = os.path.join(cachedir(args), "rules.compiled")
	os.makedirs(os.path.dirname(rfn), exist_ok=True)
	cmd = [os.path.join(args.libdir,"compile"), "-o", rfn]
	for filename in sorted(rules):
		cmd.append(rules[filename])
	cmdline = " ".join(repr(x) for x in cmd)
	if args.verbose:
		print("Running %s" % cmdline)
	child = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=None, stderr=None)
	child.wait()
	if child.returncode:
		if child.returncode < 0:
			print("Command %s killed by signal %d" % (cmdline, -child.returncode), file=sys.stderr)
		else:
			print("Command %s failed with exit code %d" % (cmdline, child.returncode), file=sys.stderr)
		return 2
	return 0


def add_findings(findings, lines):
	for l in lines:
		if not l:
			continue
		tag, detail, lineno = l.split("|",2)
		if lineno:
			lineno = int(lineno)
		else:
			lineno = None
		findings.append((tag, detail, lineno))

def do_check(args):
	status = 0
	blhc = args.blhc
	if not blhc:
		blhc = "/usr/bin/blhc"
		if not os.path.exists(blhc):
			print("No blhc installed. blhc results will not be included (use --blhc=NO to disable)", file=sys.stderr)
			blhc = None
	elif blhc == "NO":
		blhc = None
	elif not os.path.exists(blhc):
		print("No such file %s (given as --blhc)" % blhc, file=sys.stderr)
		sys.exit(2)
	rfn = rulesfile(args)
	if not rfn:
		print("Fatal error: Cannot find rules file.", file=sys.stderr)
		sys.exit(2)
	for filename in args.filename:
		findings = []
		# TODO: support scanning compressed files...
		# call the scanner:
		cmd = [os.path.join(args.libdir,"scan"), "-r", rfn, filename]
		child = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=None)
		cmd = " ".join(repr(x) for x in cmd)
		out, err = child.communicate(None)
		lines = out.decode("UTF-8").split("\n")
		if child.returncode:
			if child.returncode < 0:
				print("Command %s killed by signal %d" % (cmd, -child.returncode), file=sys.stderr)
			else:
				print("Command %s failed with exit code %d" % (cmd,child.returncode), file=sys.stderr)
			status = 2
		else:
			if len(lines) > 0 and lines[-1] != "":
				print("Command %s returned malformed data" % cmd, file=sys.stderr)
			status = 2
		add_findings(findings, lines)
		if blhc:
			cmd = [blhc, "--buildd", filename]
			child = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=None)
			cmd = " ".join(repr(x) for x in cmd)
			out, err = child.communicate(None)
			lines = out.decode("UTF-8").split("\n")
			if child.returncode:
				if child.returncode < 0:
					print("Command %s killed by signal %d" % (cmd, -child.returncode), file=sys.stderr)
				else:
					print("Command %s failed with exit code %d" % (cmd,child.returncode), file=sys.stderr)
				status = 2
			else:
				if len(lines) > 0 and lines[-1] != "":
					print("Command %s returned malformed data" % cmd, file=sys.stderr)
				status = 2
			add_findings(findings, lines)
		# output results:
		for (tag, detail, lineno) in findings:
			print("%s%s%s: %s %s%s%s" % (
				filename,
				"" if lineno is None else ":",
				"" if lineno is None else str(lineno),
				tag,
				"(" if detail else "",
				detail,
				")" if detail else "",
			))
			# TODO: output line triggering this
	return status

def do_checkout(args):
	cd = cachedir(args)
	checkoutdir = os.path.join(cd, "checkout")
	os.makedirs(os.path.dirname(checkoutdir), exist_ok=True)
	cmd = ["svn","checkout", "svn://svn.debian.org/svn/qa/trunk/data/bls/descriptions", checkoutdir]
	cmdline = " ".join(cmd)
	child = subprocess.Popen(cmd, stdin=None, stdout=None, stderr=None)
	child.wait()
	if child.returncode:
		if child.returncode < 0:
			print("Command %s killed by signal %d" % (cmdline, -child.returncode), file=sys.stderr)
		else:
			print("Command %s failed with exit code %d" % (cmdline, child.returncode), file=sys.stderr)
		return 2
	return 0

args = parse_args()
if args.command == "compile":
	r = do_compile(args)
elif args.command == "check":
	r = do_check(args)
elif args.command == "checkout":
	r = do_checkout(args)
else:
	parser.error("Unknown command")
	r = 255
sys.exit(r)
