import re
import weakref
from buildbot import util

class Properties(util.ComparableMixin):
    """
    I represent a set of properties that can be interpolated into various
    strings in buildsteps.

    @ivar properties: dictionary mapping property values to tuples 
        (value, source), where source is a string identifing the source
        of the property.

    Objects of this class can be read like a dictionary -- in this case,
    only the property value is returned.

    As a special case, a property value of None is returned as an empty 
    string when used as a mapping.
    """

    compare_attrs = ('properties',)

    def __init__(self, **kwargs):
        """
        @param kwargs: initial property values (for testing)
        """
        self.properties = {}
        self.pmap = PropertyMap(self)
        if kwargs: self.update(kwargs, "TEST")

    def __getstate__(self):
        d = self.__dict__.copy()
        del d['pmap']
        return d

    def __setstate__(self, d):
        self.__dict__ = d
        self.pmap = PropertyMap(self)

    def __getitem__(self, name):
        """Just get the value for this property."""
        rv = self.properties[name][0]
        return rv

    def has_key(self, name):
        return self.properties.has_key(name)

    def getProperty(self, name, default=None):
        """Get the value for the given property."""
        return self.properties.get(name, (default,))[0]

    def getPropertySource(self, name):
        return self.properties[name][1]

    def asList(self):
        """Return the properties as a sorted list of (name, value, source)"""
        l = [ (k, v[0], v[1]) for k,v in self.properties.items() ]
        l.sort()
        return l

    def __repr__(self):
        return repr(dict([ (k,v[0]) for k,v in self.properties.iteritems() ]))

    def setProperty(self, name, value, source):
        self.properties[name] = (value, source)

    def update(self, dict, source):
        """Update this object from a dictionary, with an explicit source specified."""
        for k, v in dict.items():
            self.properties[k] = (v, source)

    def updateFromProperties(self, other):
        """Update this object based on another object; the other object's """
        self.properties.update(other.properties)

    def render(self, value):
        """
        Return a variant of value that has any WithProperties objects
        substituted.  This recurses into Python's compound data types.
        """
        # we use isinstance to detect Python's standard data types, and call
        # this function recursively for the values in those types
        if isinstance(value, (str, unicode)):
            return value
        elif isinstance(value, WithProperties):
            return value.render(self.pmap)
        elif isinstance(value, list):
            return [ self.render(e) for e in value ]
        elif isinstance(value, tuple):
            return tuple([ self.render(e) for e in value ])
        elif isinstance(value, dict):
            return dict([ (self.render(k), self.render(v)) for k,v in value.iteritems() ])
        else:
            return value

class PropertyMap:
    """
    Privately-used mapping object to implement WithProperties' substitutions,
    including the rendering of None as ''.
    """
    colon_minus_re = re.compile(r"(.*):-(.*)")
    colon_plus_re = re.compile(r"(.*):\+(.*)")
    def __init__(self, properties):
        # use weakref here to avoid a reference loop
        self.properties = weakref.ref(properties)

    def __getitem__(self, key):
        properties = self.properties()
        assert properties is not None

        # %(prop:-repl)s
        # if prop exists, use it; otherwise, use repl
        mo = self.colon_minus_re.match(key)
        if mo:
            prop, repl = mo.group(1,2)
            if properties.has_key(prop):
                rv = properties[prop]
            else:
                rv = repl
        else:
            # %(prop:+repl)s
            # if prop exists, use repl; otherwise, an empty string
            mo = self.colon_plus_re.match(key)
            if mo:
                prop, repl = mo.group(1,2)
                if properties.has_key(prop):
                    rv = repl
                else:
                    rv = ''
            else:
                rv = properties[key]

        # translate 'None' to an empty string
        if rv is None: rv = ''
        return rv

class WithProperties(util.ComparableMixin):
    """
    This is a marker class, used fairly widely to indicate that we
    want to interpolate build properties.
    """

    compare_attrs = ('fmtstring', 'args')

    def __init__(self, fmtstring, *args):
        self.fmtstring = fmtstring
        self.args = args

    def render(self, pmap):
        if self.args:
            strings = []
            for name in self.args:
                strings.append(pmap[name])
            s = self.fmtstring % tuple(strings)
        else:
            s = self.fmtstring % pmap
        return s
