#!/usr/bin/python3 -sP

from sys import argv, exit, stdout, stderr
import argparse
from configparser import ConfigParser
import re
from fnmatch import translate
from glob import iglob
from os import getenv, readlink
from os.path import isfile, isdir, islink
from swidq import SWIDTagCollection, XSLT, resolve_path

CONFIG_FILE = "/etc/swid/swidq.conf"
SWIDTAGS_DIRS = "/etc/swid/swidtags.d/*"
STYLESHEET_DIR = "/usr/share/swidq/stylesheets"

def setup_opts():
	parser = argparse.ArgumentParser(description='Querying SWID tags.')
	select_group = parser.add_argument_group("selection options")
	select_group.add_argument('-p', '--paths', type=str, nargs='+', metavar='PATH', help="process listed directories and SWID tag files")
	select_group.add_argument('-a', '--all', dest='all', action='store_true', help="match tagId/name with glob pattern, default '*'")
	select_group.add_argument('-n', '--name', action='store_true', help="query name instead of tagId")
	select_group.add_argument('--rpm', action='store_true', help="query rpm Resource instead of tagId")

	output_group = parser.add_argument_group("output options")
	output_group.add_argument('-i', '--info', action='store_true', help="output some SWID tag fields")
	output_group.add_argument('-l', '--list-files', action='store_true', help="list files from the SWID tag")
	output_group.add_argument('--dump', action='store_true', help="dump SWID tag content as indented text")
	output_group.add_argument('--xml', action='store_true', help="output SWID tags as XML")
	output_group.add_argument('--output-stylesheet', metavar='FILE', help="output via custom XSLT stylesheet")

	other_group = parser.add_argument_group("other options")
	other_group.add_argument('--debug', action='store_true', help="verbose debugging messages")
	other_group.add_argument('--silent', action='store_true', help="suppress non-fatal warnings")
	other_group.add_argument('-c', '--config', default=CONFIG_FILE, metavar='FILE', help="location of the configuration file")

	positional_group = parser.add_argument_group("remaining arguments")
	positional_group.add_argument('match', type=str, nargs='*', help='tagId, name, or rpm name', metavar='...')
	return parser.parse_args()

def setup_transforms(opts, config):
	stylesheet_dir = getenv('SWIDQ_STYLESHEET_DIR')
	if stylesheet_dir is None or stylesheet_dir == "":
		stylesheet_dir = config.get('swidq', 'stylesheet-dir')
	transforms = []
	if opts.xml:
		transforms.append(XSLT(stylesheet_dir + '/swidq-xml.xslt'))
	if opts.output_stylesheet:
		transforms.append(XSLT(opts.output_stylesheet))
	if opts.dump:
		transforms.append(XSLT(stylesheet_dir + '/swidq-dump.xslt'))
	if opts.info:
		transforms.append(XSLT(stylesheet_dir + '/swidq-info.xslt'))
	if opts.list_files:
		transforms.append(XSLT(stylesheet_dir + '/swidq-files.xslt'))
	return transforms

def load_config(opts):
	config = ConfigParser(delimiters = '=', comment_prefixes = '#')
	config['swidq'] = { 'dir': SWIDTAGS_DIRS, 'stylesheet-dir': STYLESHEET_DIR }
	config.read(opts.config)
	if opts.debug:
		stderr.write("%s: config file [%s]\n" % (argv[0], opts.config))
		for s in config.sections():
			stderr.write("%s: config data [%s]\n" % (argv[0], s))
			for k in config[s].keys():
				stderr.write("%s: config data %s = %s\n" % (argv[0], k, config[s][k]))
	return config

def load_file(tags, f, debug=False, silent=False):
	if debug:
		stderr.write("%s: parsing [%s]\n" % (argv[0], f))
	(tag, fatal, errors) = tags.load_swidtag_file(f)
	if (fatal or (errors and not silent)):
		for e in errors:
			stderr.write("%s: %s\n" % (argv[0], e))
	if not tag:
		return
	if debug:
		stderr.write("%s: %s\n" % (argv[0], tag.get_info()))
	return tag

def load_tag_collection(dirs, resolve_dir_symlinks=False, debug=False, silent=False):
	exit_status = 0
	tags = SWIDTagCollection()

	for f in dirs:
		if resolve_dir_symlinks and islink(f):
			target = readlink(f)
			f = resolve_path(f, target)
		if isfile(f) or f == '-':
			load_file(tags, f, debug, silent)
		else:
			fg = sorted(iglob(f))
			if not fg:
				stderr.write("%s: no file matching [%s]\n" % (argv[0], f))
				exit_status = 1
			for g in fg:
				if resolve_dir_symlinks and islink(g):
					target = readlink(g)
					g = resolve_path(g, target)
				if isdir(g):
					for gg in sorted(iglob(g + "/*.swidtag")):
						load_file(tags, gg, debug, silent)
				else:
					load_file(tags, g, debug, silent)

	return (tags, exit_status)

def list_supplemental(tags, tag, seen=set(), indent=""):
	collect_seen = set( { tag.get_path(): True } )
	for stag in tags.supplemental_for(tag):
		spath = stag.get_path()
		breaking_loop = ""
		if spath in seen and len(tags.supplemental_for(stag)) > 0:
			breaking_loop = " ... breaking loop"
		stdout.write("%s+ %s %s%s\n" % (indent, stag.get_tagid(), spath, breaking_loop))
		if not breaking_loop:
			local_seen = set(seen)
			local_seen.add(spath)
			collect_seen.update(list_supplemental(tags, stag, local_seen, indent=indent + "  "))
	return collect_seen

def mark_supplemental_seen(tags, tag, seen):
	for stag in tags.supplemental_for(tag):
		spath = stag.get_path()
		if spath not in seen:
			seen.add(spath)
			mark_supplemental_seen(tags, stag, seen)

def process_tag_collection(tags, transforms, opts):
	tags.compute_supplemental(stderr=stderr, prefix="%s: " % argv[0], debug=opts.debug, silent=opts.silent)
	seen = set()
	if opts.all and len(opts.match) == 0:
		opts.match.append('*')
	separator = None
	for tag in tags:
		path = tag.get_path()
		if tag.is_supplemental():
			if path in seen:
				continue
		seen.add(path)

		matched = False
		if opts.paths and not opts.match:
			matched = True
		else:
			value = []
			if opts.rpm:
				value = tag.get_rpm_resources()
			if opts.name:
				value.append(tag.get_name())
			if not (opts.rpm or opts.name):
				value = [ tag.get_tagid() ]
			if opts.all:
				for t in opts.match:
					m = re.compile(translate(t))
					for v in value:
						if m.match(v):
							matched = True
							break
					if matched:
						break
			else:
				for v in value:
					if v in opts.match:
						matched = True
						break

		if not matched:
			continue

		if transforms:
			if separator:
				stdout.write("%s\n" % separator)
				stdout.flush()
			else:
				separator = "---"
			supplemented = tag.with_supplemented(tags)
			for t in transforms:
				t.process(supplemented).write_output(stdout.buffer)
			mark_supplemental_seen(tags, tag, seen)
			continue

		sup_indent = ""
		if tag.is_supplemental():
			stdout.write("- ")
			sup_indent = "  "

		stdout.write("%s %s\n" % (tag.get_tagid(), path))
		seen.update(list_supplemental(tags, tag, indent=sup_indent))

def main():
	opts = setup_opts()
	config = load_config(opts)
	transforms = setup_transforms(opts, config)

	dirs = None
	if opts.paths:
		dirs = opts.paths

	if dirs is None:
		d = config.get('swidq', 'dir')
		if d == '':
			stderr.write("%s: no dir configured in [%s]\n" % (argv[0], opts.config))
			exit(2)
		dirs = re.split(r' +', d)

	(tags, exit_status) = load_tag_collection(dirs, not(opts.paths), opts.debug, opts.silent)
	try:
		process_tag_collection(tags, transforms, opts)
		stdout.flush()
	except BrokenPipeError as e:
		stderr.close()
		return exit_status

	if opts.debug:
		stderr.write("%s: exitting.\n" % argv[0])

	return exit_status

if __name__ == '__main__':
	exit(main())
