#!/usr/bin/python -O

import sys, os
from optparse import OptionParser, OptionValueError
if not hasattr(__builtins__, "set"):
	from sets import Set as set
import re
try:
	import portage
except ImportError:
	from os import path as osp
	sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym"))
	import portage

import portage_const, portage_exception
class WorldHandler(object):

	def name():
		return "world"
	name = staticmethod(name)

	def __init__(self):
		self.invalid = []
		self.not_installed = []
		self.invalid_category = []
		self.okay = []
		self.world_file = os.path.join("/", portage_const.WORLD_FILE)
		self.found = os.access(self.world_file, os.R_OK)

		categories = set(portage.settings.categories)
		myroot = portage.settings["ROOT"]
		vardb = portage.db[myroot]["vartree"].dbapi

		for atom in open(self.world_file).read().split():
			if not portage.isvalidatom(atom):
				self.invalid.append(atom)
				continue
			cp = portage.dep_getkey(atom)
			okay = True
			if not vardb.match(atom):
				self.not_installed.append(atom)
				okay = False
			if portage.catsplit(cp)[0] not in categories:
				self.invalid_category.append(atom)
				okay = False
			if okay:
				self.okay.append(atom)

	def check(self):
		errors = []
		if self.found:
			errors += map(lambda x: "'%s' is not a valid atom" % x, self.invalid)
			errors += map(lambda x: "'%s' is not installed" % x, self.not_installed)
			errors += map(lambda x: "'%s' has a category that is not listed in /etc/portage/categories" % x, self.invalid_category)
		else:
			errors.append(self.world_file + " could not be opened for reading")
		return errors

	def fix(self):
		errors = []
		try:
			portage.write_atomic(self.world_file,
				"\n".join(sorted(self.okay)) + "\n")
		except portage_exception.PortageException:
			errors.append(self.world_file + " could not be opened for writing")
		return errors

class VdbKeyHandler(object):
	def name():
		return "vdbkeys"
	name = staticmethod(name)

	def __init__(self):
		self.list = portage.db["/"]["vartree"].dbapi.cpv_all()
		self.missing = []
		self.keys = ["HOMEPAGE", "SRC_URI", "KEYWORDS", "DESCRIPTION"]
		
		for p in self.list:
			mydir = os.path.join(os.sep, portage.settings["ROOT"], portage_const.VDB_PATH, p)+os.sep
			ismissing = True
			for k in self.keys:
				if os.path.exists(mydir+k):
					ismissing = False
					break
			if ismissing:
				self.missing.append(p)
		
	def check(self):
		return ["%s has missing keys" % x for x in self.missing]
	
	def fix(self):
	
		errors = []
	
		for p in self.missing:
			mydir = os.path.join(os.sep, portage.settings["ROOT"], portage_const.VDB_PATH, p)+os.sep
			if not os.access(mydir+"environment.bz2", os.R_OK):
				errors.append("Can't access %s" % (mydir+"environment.bz2"))
			elif not os.access(mydir, os.W_OK):
				errors.append("Can't create files in %s" % mydir)
			else:
				env = os.popen("bzip2 -dcq "+mydir+"environment.bz2", "r")
				envlines = env.read().split("\n")
				env.close()
				for k in self.keys:
					s = [l for l in envlines if l.startswith(k+"=")]
					if len(s) > 1:
						errors.append("multiple matches for %s found in %senvironment.bz2" % (k, mydir))
					elif len(s) == 0:
						s = ""
					else:
						s = s[0].split("=",1)[1]
						s = s.lstrip("$").strip("\'\"")
						s = re.sub("(\\\\[nrt])+", " ", s)
						s = " ".join(s.split()).strip()
						if s != "":
							try:
								keyfile = open(mydir+os.sep+k, "w")
								keyfile.write(s+"\n")
								keyfile.close()
							except (IOError, OSError), e:
								errors.append("Could not write %s, reason was: %s" % (mydir+k, e))
		
		return errors

def emaint_main(myargv):

	# Similar to emerge, emaint needs a default umask so that created
	# files (such as the world file) have sane permissions.
	os.umask(022)

	# TODO: Create a system that allows external modules to be added without
	#       the need for hard coding.
	modules = {"world" : WorldHandler}

	module_names = modules.keys()
	module_names.sort()
	module_names.insert(0, "all")

	def exclusive(option, *args, **kw):
		var = kw.get("var", None)
		if var is None:
			raise ValueError("var not specified to exclusive()")
		if getattr(parser, var, ""):
			raise OptionValueError("%s and %s are exclusive options" % (getattr(parser, var), option))
		setattr(parser, var, str(option))


	usage = "usage: emaint [options] " + " | ".join(module_names)

	usage+= "\n\nCurrently emaint can only check and fix problems with one's world\n"
	usage+= "file.  Future versions will integrate other portage check-and-fix\n"
	usage+= "tools and provide a single interface to system health checks."


	parser = OptionParser(usage=usage, version=portage.VERSION)
	parser.add_option("-c", "--check", help="check for problems",
		action="callback", callback=exclusive, callback_kwargs={"var":"action"})
	parser.add_option("-f", "--fix", help="attempt to fix problems",
		action="callback", callback=exclusive, callback_kwargs={"var":"action"})
	parser.action = None


	(options, args) = parser.parse_args(args=myargv)
	if len(args) != 1:
		parser.error("Incorrect number of arguments")
	if args[0] not in module_names:
		parser.error("%s target is not a known target" % args[0])

	if parser.action:
		action = parser.action
	else:
		print "Defaulting to --check"
		action = "-c/--check"

	if args[0] == "all":
		tasks = modules.values()
	else:
		tasks = [modules[args[0]]]


	if action == "-c/--check":
		status = "Checking %s for problems"
		func = "check"
	else:
		status = "Attempting to fix %s"
		func = "fix"


	for task in tasks:
		print status % task.name()
		inst = task()
		result = getattr(inst, func)()
		if result:
			print
			print "\n".join(result)
			print "\n"

	print "Finished"

if __name__ == "__main__":
	emaint_main(sys.argv[1:])
