import sys
from decorator import decorator
from collections import OrderedDict
import re

from . import formatting, cache, db
from .dependencies import Dependencies

PARENT_DOT_THIS = '<<parent.this />>'
# TODO(xavid): normalize things that match PARENT_DOT_THIS_PAT?
PARENT_DOT_THIS_PAT = re.compile('^\s*<<parent.this */>>\s*$')
def is_parent_dot_this(s):
    #return PARENT_DOT_THIS_PAT.search(s) is not None
    return PARENT_DOT_THIS == s

class Element(object):
    __slots__ = ('_ename')
    @property
    def ename(self): return self._ename
    
    def __init__(self, ename):
        assert isinstance(ename, unicode), repr(ename)
        self._ename = ename

    def set_ename(self, ename):
        oldename = self._ename
        self._ename = ename
        db.esetattr(self, 'ename', oldename, ename)

    # TODO(xavid): change to has_prop()
    def has_propval(self, propname):
        return propname in self.get_value_map()
    def get_propval(self, propname):
        value_map = self.get_value_map()
        try:
            assert isinstance(value_map[propname], Propval), (value_map,
                                                              propname)
            return value_map[propname]
        except KeyError:
            return None
    def get_parent(self):
        return get_parent(self.ename, self)
    def get_parent_ename(self):
        p = get_parent(self.ename, self)
        if p is None:
            return None
        else:
            return p.ename

    def set_prop(self, propname, value, format='creole'):
        db.setprop(self, propname, value, format)
    def get_prop(self, propname):
        try:
            return self.get_value_map()[propname].value
        except KeyError:
            return None
    def remove_prop(self, propname):
        db.delete(self, propname)

    def get_orgmode(self):
        raise NotImplementedError
    def set_orgmode(self, orgmode):
        db.esetattr(self, 'orgmode', self.get_orgmode(), orgmode)

    def get_descendants(self):
        raise NotImplementedError
    def is_ancestor_of(self, other):
        raise NotImplementedError

    def get_ancestors(self):
        raise NotImplementedError

    def has_children(self):
        return len(get_children(self.ename, self)) > 0
    def get_children(self):
        return get_children(self.ename, self)

    def list_props(self):
        return self.get_value_map().keys()

    def create_child(self, ename):
        raise NotImplementedError

    @cache.metadata_cached('nice_value_map')
    def get_nice_value_map(self, deps=None):
        ret = {}
        for propname in self.list_props():
            flav = get_flavor(propname)
            # TODO(xavid): dependency on the flavor of the prop?
            if not flav.quick and not flav.binary:
                nv, ds = formatting.nice_value(self.get_propval(propname))
                ret[propname] = nv
                deps.update(ds)
        return ret

    def _get_value_map_impl(self, od):
        raise NotImplementedError
    
    @cache.metadata_cached('value_map')
    def get_value_map(self, deps=None):
        ret = OrderedDict()
        # Can't check without looking at this, especially since this is
        # now how we check the list of props in an element.
        # This is still useful because of the svn-revision
        # shortcutting.  These deps are only used for the metadata
        # itself; callers from, say, wiki will cache based on
        # addElementPropvalsDep().
        # i.e., can't do deps.addDep(Propval(self, pv.prop.name, pv.value))
        deps.addFragileDep(self)
        self._get_value_map_impl(ret)
        return ret

    @cache.metadata_cached('final_map')
    def get_final_map(self, deps=None):
        """Returns a map of propname to ename mapping the props
        that are defaulting to a parent element's final propval to
        that parent element's ename.  It does not include other
        propnames at all.

        A propval is 'final' if:
        * It doesn't start with '<<default'.
        * It's not PARENT_DOT_THIS.
        * It's not empty.
        * It's not only whitespace."""
        vm = self.get_value_map()
        ret = {}
        for pname in vm:
            value = vm[pname].value
            deps.addDep(vm[pname])
            if not is_parent_dot_this(value):
                # Don't add to map.
                continue
            anc = self.get_ancestors()
            deps.addParentDep(self)
            while is_parent_dot_this(value) and anc:
                e = anc.pop()
                pv = e.get_propval(pname)
                if pv is not None:
                    deps.addDep(pv)
                    value = pv.value
                else:
                    deps.addNoPropvalDep(e.ename, pname)
                    value = None
                    break
                deps.addParentDep(e)
            if (value is not None
                and not value.startswith('<<default')
                and not is_parent_dot_this(value)
                and len(value) > 0 and not value.isspace()):
                ret[pname] = e.ename
            else:
                pass
        return ret

    @cache.metadata_cached('flavor_map')
    def get_flavor_map(self, deps=None):
        ret = {}
        # In theory we could avoid getting propvals at all, like the
        # following, but we don't have prop-only dependencies.
        # TODO(xavid): Add prop dependencies/versioning?
        #for prop in (Prop.query.join((Propval,Prop.id == Propval.prop_id))
        #             .filter(Propval.element_id == self.id).all()):
        vm = self.get_value_map()
        for pname in vm:
            # TODO(xavid): we don't actually care about the value
            deps.addDep(vm[pname])
            ret[pname] = get_prop(pname).flavor
        return ret

    def set_parent(self, parent):
        db.esetattr(self, 'parent', self.get_parent(), parent)

    def __unicode__(self):
        return self.ename
    def __repr__(self):
        return "<$Element: %s>"%self.ename.encode('utf-8')
    def __eq__(self, other):
        return hasattr(other, 'ename') and self.ename == other.ename
    def __hash__(self):
        return hash(self.ename)

def get_root_element():
    return impl.get_root_element()

def create_root_element(ename):
    return impl.create_root_element(ename)

def list_all_elements():
    root = get_root_element()
    return [root] + root.get_descendents()

TXT = u'txt'

class Propval(object):
    __slots__ = ('_element', '_propname', '_value', '_format')
    @property
    def element(self): return self._element
    @property
    def propname(self): return self._propname
    @property
    def value(self): return self._value
    @property
    def format(self): return self._format

    def __init__(self, element, propname, value, format):
        self._element = element
        self._propname = propname
        self._value = value
        self._format = format

    def __setstate__(self, state):
        ename, propname, value, format = state
        self.__init__(get_element(ename), propname, value, format)

    def __getstate__(self):
        return (self.element.ename, self.propname, self.value, self.format)

    # TODO(xavid): replace this with a method in conversion.
    def render(self, format=TXT, filters={}, method='render', offset=0):
        from . import conversion
        im = conversion.convert_any(self.element.ename, self.propname,
                                    [format], filters,
                                    method=method, offset=offset + 1)
        return im.asData()

    def set_value(self, value):
        self._value = value
        self.element.set_prop(self.propname, value)

    def __repr__(self):
        return "<$Propval: %s.%s>" % (self.element.ename.encode('utf-8'),
                                      self.propname.encode('utf-8'))

class Prop(object):
    __slots__ = ('_name', '_flavor', '_default', '_visible', '_comment')
    @property
    def name(self): return self._name
    @property
    def flavor(self): return self._flavor
    @property
    def default(self): return self._default
    @property
    def visible(self): return self._visible
    @property
    def comment(self): return self._comment

    def __init__(self, name, flavor, default, visible, comment):
        self._name = name
        self._flavor = flavor
        self._default = default
        self._visible = visible
        self._comment = comment

    def set_flavor(self, flavor):
        raise NotImplementedError

    def set_default(self, default):
        raise NotImplementedError

    def set_visible(self, visible):
        raise NotImplementedError

    def set_comment(self, comment):
        raise NotImplementedError

    # Should be ordered such that each element is followed by a single
    # region consisting of any included descendants.  Because of prop
    # inheritance, there shouldn't be holes.
    def containing_elements(self):
        raise NotImplementedError

def list_all_props():
    return impl.list_all_props()

def list_all_elements():
    return impl.list_all_elements()

class Symbol(object):
    pass
# Singleton because None and False can be valid values.
NOT_FOUND = Symbol()

def str_memoed(key):
    def helper(func, arg):
        k = '%s:%s' % (key, arg)
        val = cache.get_memo(k, NOT_FOUND)
        if val is NOT_FOUND:
            val = func(arg)
            cache.set_memo(k, val)
        return val
    return decorator(helper)

@str_memoed('element')
def get_element(ename):
    return impl.get_element(ename)

# Special restriction values for 
# search_elements()/get_element_with()/get_element_raw().
# (Be sure to add to implementations' _get_element_raw_simple() if you add 
# more.) 
NOT_BLANK = Symbol()

def search_elements(restrictions, raw=False):
    """search_elements({u'category':'mon',u'name':'sakura')
    returns a list of elements with those propvals, using inheritance and such.
    
    restrictions must have unicode keys."""
    elms = list_all_elements()

    for pname in restrictions:
        target = restrictions[pname]
        newelms = []
        flav = get_flavor(pname)
        assert flav.indexed or target is NOT_BLANK, (flav, target)
        if raw:
            assert flav.raw or target is NOT_BLANK
        else:
            assert not flav.raw

        if hasattr(target, 'ename'):
            from bazbase.flavors import FLAVORS
            target = FLAVORS['reference'].element_to_string(target)

        for e in elms:
            if e.has_propval(pname):
                if raw:
                    rendered = e.get_prop(pname)
                else:
                    from . import conversion
                    from .wiki import NoContentException
                    try:
                        rendered = conversion.render(e, pname)
                    except NoContentException:
                        continue

                if target is NOT_BLANK:
                    if rendered != '':
                        newelms.append(e)
                else:
                    if rendered == target:
                        newelms.append(e)

        elms = newelms
    return elms
def _one_of_list(l):
    if len(l) == 1:
        return l[0]
    elif len(l) == 0:
        return None
    else:
        raise LookupError("Multiple results!")    
def get_element_with(restrs):
    return _one_of_list(search_elements(restrs))
def get_element_raw(restrs):
    # TODO(xavid): changing the efficeint case to a search and implementing
    #              the non-efficient case as a filtering of the results for the
    #              first k,v could be cool
    if len(restrs) == 1 and hasattr(impl, '_get_element_raw_simple'):
        k, v = restrs.items()[0]
        return impl._get_element_raw_simple(k, v)
    else:
        return _one_of_list(search_elements(restrs, raw=True))

@str_memoed('prop')
def get_prop(pname):
    return impl.get_prop(pname)

def create_prop(pname, flavor=None):
    return impl.create_prop(pname, flavor)

@str_memoed('flavor')
def get_flavor(pname):
    from bazbase.flavors import FLAVORS
    prop = get_prop(pname)
    if prop is not None:
        return FLAVORS[prop.flavor]
    else:
        return None

def get_propval(ename, pname):
    e = get_element(ename)
    if e is not None:
        return e.get_propval(pname)
    else:
        return None
def has_propval(ename, pname):
    return get_element(ename).has_propval(pname)

def e_memoed(key):
    def helper(func, ename, e=None):
        k = '%s:%s' % (key, ename)
        val = cache.get_memo(k, NOT_FOUND)
        if val is NOT_FOUND:
            if e is None:
                e = get_element(ename)
            val = func(ename, e)
            cache.set_memo(k, val)
        return val
    return decorator(helper)

@e_memoed('parent')
def get_parent(ename, e=None):
    return impl.get_parent(ename, e)
def element_get_parent(e):
    return get_parent(e.ename, e)

@e_memoed('children')
def get_children(ename, e=None):
    return impl.get_children(ename, e)

# Overall operations
def clear_database():
    impl.clear_database()

def flush_database_for_test():
    impl.flush_database_for_test()

def verify_tree_for_test():
    impl.verify_tree_for_test()

def set_impl(i):
    global impl
    impl = i
