#
#  $Id: Registry.py,v 1.4 1999/12/04 22:08:22 rob Exp $
#
#  Copyright 1999 Rob Tillotson <robt@debian.org>
#  All Rights Reserved
#
#  Permission to use, copy, modify, and distribute this software and
#  its documentation for any purpose and without fee or royalty is
#  hereby granted, provided that the above copyright notice appear in
#  all copies and that both the copyright notice and this permission
#  notice appear in supporting documentation or portions thereof,
#  including modifications, that you you make.
#
#  THE AUTHOR ROB TILLOTSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
#  THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
#  AND FITNESS.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
#  SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
#  RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
#  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
#  CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE!
#
"""Registries.

An option registry is a sort of multi-leveled, sectioned dictionary.
Instead of a single key, the contents of a Registry are organized by
two keys: section and option key.  A Registry is thus organized
appropriately for use with INI-style configuration files, and that
is in fact the format read by this module.

A Registry is actually a stack, so that an application can read multiple
configuration files and still be able to retain information about which
files contained what options.  When an option is looked up in the Registry,
the topmost instance of it is returned.  Each Registry level has a label
attribute which can be used for whatever the application wants.
"""

__version__ = '$Id: Registry.py,v 1.4 1999/12/04 22:08:22 rob Exp $'

__copyright__ = 'Copyright 1999 Rob Tillotson <robt@debian.org>'

import string, re, operator
from UserDict import UserDict

rx_section = '^\[(.*)\]'
rx_variable = '^([^ \t=]+)\s*=\s*(.*)$'
rx_comment = '^#'

class OldRegistryLevel:
    def __init__(self, label=None):
	self.data = {}
	self.label = label
	self.case_sensitive = 0
	
    def __str__(self):
	s = ''
	for k in self.data.keys():
	    s = s + '[%s]\n' % k
	    for l in self.data[k].keys():
		s = s + '%s = %s\n' % (l, self.data[k][l])
	    s = s + '\n'
	return s

    def load(self, f):
	if type(f) == type(''): f = open(f, 'r')
	
	sec = None
	for l in map(string.strip, f.readlines()):
	    if not l or re.match(rx_comment, l):
		continue

	    m = re.match(rx_section, l)
	    if m:
		if not self.case_sensitive: sec = string.lower(m.group(1))
		else: sec = m.group(1)
		if not self.data.has_key(sec): self.data[sec] = {}
		continue

	    m = re.match(rx_variable, l)
	    if m:
		if not self.case_sensitive: n = string.lower(m.group(1))
		else: n = m.group(1)
		v = m.group(2)
		self.data[sec][n] = v  # support multiple entries here?

    def has_section(self, k):
	return self.data.has_key(string.lower(k))

    def has_key(self, sec, var):
	if not self.case_sensitive:
	    s = string.lower(sec)
	    v = string.lower(var)
	else:
	    s = sec
	    v = var
	return self.has_section(s) and self.data[s].has_key(v)

    def del_key(self, sec, var):
	if not self.case_sensitive:
	    s = string.lower(sec)
	    v = string.lower(var)
	else:
	    s = sec
	    v = var
	if self.has_key(s, v):
	    del self.data[s][v]

    def del_section(self, sec):
	if not self.case_sensitive: s = string.lower(sec)
	else: s = sec
	if self.data.has_key(s):
	    del self.data[s]
	    
    def get(self, sec, var, default=None):
	try:
	    if not self.case_sensitive:
		s = string.lower(sec)
		v = string.lower(var)
	    else:
		s = sec
		v = var
	    return self.data[s][v]
	except KeyError:
	    return default

    def set(self, sec, var, value):
	if not self.case_sensitive:
	    s = string.lower(sec)
	    v = string.lower(var)
	else:
	    s = sec
	    v = var
	if not self.data.has_key(s):
	    self.data[s] = {}
	self.data[s][v] = value


class OldRegistry:
    is_registry = 1
    def __init__(self):
	self.data = []
	self.base_class = OldRegistryLevel
	
    def push(self, r):
	self.data.append(r)

    def pop(self):
	r = self.data[-1]
	del self.data[-1]
	return r

    def load(self, f, label=None):
	r = self.base_class(label)
	try:
	    r.load(f)
	except:
	    pass
	self.push(r)

    def label(self):
	return self.data[-1].label
	
    def has_section(self, key):
	return reduce(operator.or_, map(lambda x, k=key: x.has_section(k),
					self.data), 0)

    def has_key(self, sec, var):
	return reduce(operator.or_, map(lambda x, s=sec, v=var: x.has_key(s,v),
					self.data), 0)

    def get(self, sec, var, default=None):
	return reduce(lambda d, x, s=sec, v=var: x.get(s,v,d), self.data, default)

#####
# Conversion from old to new format
#####
def convert_config_file(f):
    if type(f) == type(''): f = open(f, 'r')
    o = []
    
    havesec = 0
    for l in map(string.strip, f.readlines()):
	if not l or re.match(rx_comment, l):
	    continue

	m = re.match(rx_section, l)
	if m:
	    if havesec: o.append('};\n\n')
	    else: havesec = 1
	    o.append(m.group(1)+' {\n')
	    continue
	
	m = re.match(rx_variable, l)
	if m:
	    n = m.group(1)
	    v = m.group(2)
	    o.append('  %s "%s";\n' % (string.replace(n, '.', '::'),
				       string.replace(v,'"','\\"')))

    if havesec: o.append('};\n')
    return string.join(o, '')

	

####################################################
# New config file format stuff
####################################################

_comment_re = '(?ms)(?:^\s*\\/\\/.*?$)|(?:\\/\\*.*?\\*\\/)'
_tokenize_re = re.compile('''(?xis)(
		    (?:"")|(?:".*?[^\\\\]")                               # string
		  | \{ | \} | ;                                       # various tokens
		  | (?:\\/\\/.*?\\\n)|(?:\\/\\*.*?\\*\\/)
		  | (?:[^" \t\n\{\};][^ \t\n\{\};]*)
		  )
		  ''')
_path_sep = '::'

def tokenize(s):
    """Tokenize a string.

    Returns a list of tokens, each one a string.  Tokenization is
    based on a regular expression (_tokenize_re); the string is
    processed with re.split, and all of the matches are returned.
    """
    s = string.join(re.split(_comment_re, s), '')
    l = re.split(_tokenize_re, s)
    return filter(lambda x: x is not None and x[0] != '/',
		  map(lambda x, y: y % 2 and x or None,
		      l, range(len(l))))
    

def unquote_string(s):
    """Convert a quoted string to an unquoted string.
    """
    if s and s[0] == '"': return string.replace(s[1:-1], '\\"', '"')
    else: return s
    
def assign_value(dict, path, tokens):
    """Assignment helper function for parser.

    Given a base path and list of tokens, assigns the value to the
    specified entry below the path (that is, for tokens 0 ... n,
    tokens 0 ... n-1 are assumed to specify additional path components,
    and token n is assumed to be a value.  Multiple values at the
    same path are converted to a list.
    """
    if tokens:
	p = string.join(path+tokens[:-1], _path_sep)
	value = tokens[-1]
	
	if dict.has_key(p):
	    if type(dict[p]) != type([]): dict[p] = [ dict[p] ]
	    dict[p].append(value)
	else:
	    dict[p] = value

def delim_split(l):
    """Split a token list by delimiters.

    Finds the delimiters used in parsing ({, }, and ;) and returns
    pairs of (token-list, delimiter), where the token-list is a
    list of tokens which came between the last delimiter and this
    one.
    """
    m = []
    acc = []
    for x in l:
	if x not in ['{', '}', ';']:
	    acc.append(x)
	else:
	    m.append((acc, x))
	    acc = []
    if acc:
	m.append((acc,None))
    return m


def parse(s):
    """Main parsing function.

    Parses a configuration string and returns a dictionary representing
    its contents.
    """
    path = []
    dict = {}
    
    for t, d in delim_split(tokenize(s)):
	if t: t = map(unquote_string, t)
	
	if d == ';' or not d:
	    if t: assign_value(dict, path, t)
	elif d == '}':
	    if t: assign_value(dict, path, t)
	    del path[-1]
	elif d == '{':
	    path.append(string.join(t, _path_sep))
	    
    return dict

def parsefile(f):
    if type(f) == type(''): f = open(f)
    return parse(f.read())

_achars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'

def quotechar(s):
    o = ord(s)
    if o >= 32 and o <= 126: return s
    elif o == 9: return '\\t'
    elif o == 10: return '\\n'
    elif o == 13: return '\\r'
    else: return '\\%03o' % o
    
def maybe_quote(s):
    if type(s) != type(''): return s
    if not s: return ('""')
    if filter(lambda x: x not in _achars, s):
	return '"'+string.join(map(quotechar, s),'')+'"'
    else: return s

# recursively write the tree
#
# The 'hints' dict helps this function write things in a format like what
# the user expects; it is merely cosmetic, but painful to implement :)
#
# hint keys are the same as registry keys, for instance the key
# 'Pyrite::Conduit' refers to the level of the tree rooted at Pyrite::Conduit.
# hint values are dictionaries, which contain the hints.  The current hints
# are:
#
#  'order': [list of sub-keys]
#    Order the output.  The list contains *sub-keys only*, that is, only the
#    order of the *next* level of the tree.  The subtrees listed will be output
#    first, then the remaining ones (if any) will follow.
#
#  'iterate': (true or false)
#    If true, then this is an iterated level.  By an "iterated" level, I mean
#    one where the two-key form is used, eg.
#
#      Conduit "Backup" { key value; key value; };
#        instead of
#      Conduit { Backup { key value; key value; }; };
#
def writelevel(dict, hints={}, dkey=''):
    print dkey
    s = []
    keys = dict.keys()
    keys.sort()

    if hints.has_key(dkey):
	order = hints[dkey].get('order',[])
	if order:
	    for x in order[:]:
		if x in keys: keys.remove(x)
		else: order.remove(x)
	    keys = order + keys
	    print 'ordering:', keys

    for k in keys:
	i = dict[k]
	if type(i) == type({}):
	    hk = dkey and dkey+'::'+k or k
	    if hints.has_key(hk) and hints[hk].get('iterate',0):
		kkeys = i.keys()
		kkeys.sort()
		for kk in kkeys:
		    ii = i[kk]
		    if type(ii) == type({}):
			s.append('%s %s {' % (maybe_quote(k), maybe_quote(kk)))
			s = s + map(lambda x: '  '+x, writelevel(ii, hints,
								 hk and hk+'::'+kk or kk))
			s.append('};')
		    elif type(ii) == type([]):
			s.append('%s::%s {' % (maybe_quote(k), maybe_quote(kk)))
			for v in ii: s.append('  %s;' % maybe_quote(v))
			s.append('};')
		    else:
			s.append('%s::%s %s;' % (maybe_quote(k), maybe_quote(kk),
						 maybe_quote(ii)))
	    else:
		s.append('%s {' % maybe_quote(k))
		s = s + map(lambda x: '  '+x, writelevel(i,hints,dkey and dkey+'::'+k or k))
		s.append('};')
	elif type(i) == type([]):
	    s.append('%s {' % maybe_quote(k))
	    for v in i: s.append('  %s;' % maybe_quote(v))
	    s.append('};')
	else:
	    s.append('%s %s;' % (maybe_quote(k), maybe_quote(i)))
    return s

def maketree(dict):	
    tree = {}
    for k, i in dict.items():
	d = tree
	dk = string.split(k,'::')
	dv, dk = dk[-1], dk[:-1]
	for kk in dk:
	    if not d.has_key(kk): d[kk] = {}
	    d = d[kk]
	d[dv] = i
    return tree

def writeconf(dict, hints={}):
    tree = maketree(dict)

    return string.join(writelevel(tree, hints), '\n')
	    


# add path functions to this later
class RegistryLevel(UserDict):
    def __init__(self, l):
	UserDict.__init__(self)
	self.label = l

    def load(self, f):
	self.data = parsefile(f)

    def save(self, f, hints={}):
	if type(f) == type(''): f = open(f,'w')
	f.write(writeconf(self.data, hints))
	f.write('\n')
	f.close()
	
class Registry:
    is_registry = 1
    def __init__(self, *a, **kw):
	self.data = []
	self.base_class = RegistryLevel
	
    def push(self, r):
	self.data.append(r)

    def pop(self):
	r = self.data[-1]
	del self.data[-1]
	return r

    def pickle(self, f):
	if type(f) == type(''): f = f.open(f, 'w')
	pickle.dump(self.data, f)

    def unpickle(self, f):
	if type(f) == type(''): f = f.open(f, 'r')
	d = pickle.load(f)
	if type(d) != type([]): raise RuntimeError, 'bad pickle'
	self.data = d
	
    def load(self, f, label=None):
	r = self.base_class(label)
	try:
	    r.load(f)
	except:
	    pass
	self.push(r)

    def label(self):
	return self.data[-1].label

    def has_key(self, key):
	return reduce(operator.or_, map(lambda x, k=key: x.has_key(k), self.data), 0)

    def get(self, key, default=None):
	return reduce(lambda d, x, k=key: x.get(k,d), self.data, default)

    
