#!/usr/bin/python
import glob, os, sys, string, xml.sax, getopt


__doc__ = """\
List properties defined in FlightGear's <PropertyList> XML files.

Usage:
	lsprop [-v] [-p] [-i|-I] [-f <format>] [<list-of-xml-files>]
	lsprop -h

Options:
	-h, --help         print this help screen
	-v, --verbose      increase verbosity
	-i, --all-indices  also show null indices in properties
	-I, --no-indices   don't show any indices in properties
	-p, --raw-paths    don't use symbols "$FG_ROOT" and "$FG_HOME" as path prefix
	-f, --format       set output format  (default: --format="%f +%l: %p = '%v'")

Format:
	%f  file path
	%l  line number
	%c  column number
	%p  property path
	%t  property type
	%V  raw value (unescaped)
	%v  cooked value (carriage return, non printable chars etc. escaped)
	%q  like %v, but single quotes escaped to \\'
	%Q  like %v, but double quotes escaped to \\"
	%%  percent sign

Environment:
	FG_ROOT
	FG_HOME
	LSPROP_FORMAT      overrides default format

Arguments:
	If no file arguments are specified, then the following files are assumed:
	$FG_ROOT/preferences.xml
	$FG_ROOT/Aircraft/*/*-set.xml

Current settings:\
"""


class config:
	root = "/usr/local/share/FlightGear"
	home = os.environ["HOME"] + "/.fgfs"
	raw_paths = 0
	format = "%f +%l: %p = '%v'"
	verbose = 1
	indices = 1	# 0: no indices;   1: only indices != [0];   2: all indices


def errmsg(msg, color = "31;1"):
	if os.isatty(2):
		print >>sys.stderr, "\033[%sm%s\033[m" % (color, msg)
	else:
		print >>sys.stderr, msg


def cook_path(path, force = 0):
	path = os.path.normpath(os.path.abspath(path))
	if config.raw_paths and not force:
		return path
	if path.startswith(config.root):
		path = path.replace(config.root, "$FG_ROOT", 1)
	elif path.startswith(config.home):
		path = path.replace(config.home, "$FG_HOME", 1)
	return path


class Error(Exception):
	pass


class Abort(Exception):
	pass


class XMLError(Exception):
	def __init__(self, locator, msg):
		msg = "%s in %s +%d:%d" \
				% (msg.replace("\n", "\\n"), cook_path(locator.getSystemId()), \
				locator.getLineNumber(), locator.getColumnNumber())
		raise Error(msg)


class parse_xml_file(xml.sax.handler.ContentHandler):
	def __init__(self, path, nesting = 0, stack = None):
		self.level = 0
		self.path = path
		self.nesting = nesting
		self.type = None
		if stack:
			self.stack = stack
		else:
			self.stack = [[None, None, {}, []]]  # name, index, indices, data

		self.pretty_path = cook_path(path)

		if config.verbose > 1:
			errmsg("FILE %s  (%d)" % (path, nesting), "35")
		if not os.path.exists(path):
			raise Error("file doesn't exist: " + self.pretty_path)

		try:
			xml.sax.parse(path, self, self)
		except ValueError:
			pass			# FIXME hack arount DTD error

	def startElement(self, name, attrs):
		self.level += 1
		if self.level == 1:
			if name != "PropertyList":
				raise XMLError(self.locator, "XML file isn't a <PropertyList>")
		else:
			index = 0
			if attrs.has_key("n"):
				index = int(attrs["n"])
			elif name in self.stack[-1][2]:
				index = self.stack[-1][2][name] + 1
			self.stack[-1][2][name] = index

			self.type = "unspecified"
			if attrs.has_key("type"):
				self.type = attrs["type"]

		if attrs.has_key("include"):
			path = os.path.dirname(os.path.abspath(self.path)) + "/" + attrs["include"]
			if attrs.has_key("omit-node") and attrs["omit-node"] == "y" or self.level == 1:
				self.stack.append([None, None, self.stack[-1][2], []])
			else:
				self.stack.append([name, index, {}, []])
			parse_xml_file(path, self.nesting + 1, self.stack)
		elif self.level > 1:
			self.stack.append([name, index, {}, []])

	def endElement(self, name):
		value = string.join(self.stack[-1][3], '')
		if not len(self.stack[-1][2]) and self.level > 1:
			path = self.pathname()
			if path:
				cooked_value = self.escape(value.encode("iso-8859-15", "backslashreplace"))
				print config.cooked_format % {
					"f": self.pretty_path,
					"l": self.locator.getLineNumber(),
					"c": self.locator.getColumnNumber(),
					"p": path,
					"t": self.type,
					"V": value,
					"v": cooked_value,
					"q": cooked_value.replace("'", "\\'"),
					'Q': cooked_value.replace('"', '\\"'),
				}

		elif len(string.strip(value)):
			raise XMLError(self.locator, "garbage found '%s'" % string.strip(value))

		self.level -= 1
		if self.level:
			self.stack.pop()

	def characters(self, data):
		self.stack[-1][3].append(data)

	def setDocumentLocator(self, locator):
		self.locator = locator

	def pathname(self):
		path = ""
		for e in self.stack[1:]:
			if e[0] == None:	# omit-node
				continue
			path += "/" + e[0]
			if e[1] and config.indices == 1 or config.indices == 2:
				path += "[%d]" % e[1]
		return path

	def escape(self, string):
		s = ""
		for c in string:
			if c == '\n':
				s += '\\n'
			elif c == '\r':
				s += '\\r'
			elif c == '\v':
				s += '\\v'
			elif c == '\\':
				s += '\\\\'
			elif not c.isalnum() and " \t!@#$%^&*()_+|~-=\`[]{};':\",./<>?".find(c) < 0:
				s += "\\x%02x" % ord(c)
			else:
				s += c
		return s

	def warning(self, exception):
		raise XMLError(self.locator, "WARNING: " + str(exception))

	def error(self, exception):
		raise XMLError(self.locator, "ERROR: " + str(exception))

	def fatalError(self, exception):
		raise XMLError(self.locator, "FATAL: " + str(exception))


def main():
	if 'FG_ROOT' in os.environ:
		config.root = os.environ['FG_ROOT'].lstrip().rstrip("/\\\t ")
	if 'FG_HOME' in os.environ:
		config.home = os.environ['FG_HOME'].lstrip().rstrip("/\\\t ")
	if 'LSPROP_FORMAT' in os.environ:
		config.format = os.environ['LSPROP_FORMAT']

	# options
	try:
		opts, args = getopt.getopt(sys.argv[1:], \
				"hviIpf:", \
				["help", "verbose", "all-indices", "no-indices", "raw-paths", "format="])
	except getopt.GetoptError, msg:
		errmsg("Error: %s" % msg)
		return -1

	for o, a in opts:
		if o in ("-h", "--help"):
			print __doc__
			print '\t--format="%s"' % config.format.replace('"', '\\"')
			return 0
		if o in ("-v", "--verbose"):
			config.verbose += 1
		if o in ("-i", "--all-indices"):
			config.indices = 2
		if o in ("-I", "--no-indices"):
			config.indices = 0
		if o in ("-p", "--raw-paths"):
			config.raw_paths = 1
		if o in ("-f", "--format"):
			config.format = a

	# format
	f = config.format
	f = f.replace("\\e", "\x1b")
	f = f.replace("\\033", "\x1b")
	f = f.replace("\\x1b", "\x1b")
	f = f.replace("%%", "\x01\x01")
	f = f.replace("%f", "\x01(f)s")
	f = f.replace("%l", "\x01(l)d")
	f = f.replace("%c", "\x01(c)d")
	f = f.replace("%p", "\x01(p)s")
	f = f.replace("%t", "\x01(t)s")
	f = f.replace("%V", "\x01(V)s")
	f = f.replace("%v", "\x01(v)s")
	f = f.replace("%q", "\x01(q)s")
	f = f.replace('%Q', '\x01(Q)s')
	f = f.replace("%", "%%")
	f = f.replace("\x01", "%")
	config.cooked_format = f

	if config.verbose > 2:
		print >>sys.stderr, "internal format = [%s]" % config.cooked_format

	# arguments
	if not len(args):
		args = [config.root + "/preferences.xml"]
		if not os.path.exists(args[0]):
			errmsg("Error: environment variable FG_ROOT not set or set wrongly?")
			return -1
		for f in glob.glob(config.root + '/Aircraft/*/*-set.xml'):
			args.append(f)

	for arg in args:
		try:
			parse_xml_file(arg)
		except Abort, e:
			errmsg("Abort: " + e.args[0])
			return -1
		except Error, e:
			errmsg("Error: " + e.args[0])
		except IOError, (errno, msg):
			errmsg("Error: " + msg)
			return errno
		except KeyboardInterrupt:
			print >>sys.stderr, "\033[m"
			return 0


if __name__ == "__main__":
	sys.exit(main())

