from __future__ import absolute_import
from __future__ import with_statement

import sys, re, os

from sqlalchemy import *
import sqlalchemy
from sqlalchemy.types import UnicodeText
from sqlalchemy.exc import UnboundExecutionError
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import synonym, aliased, joinedload

# Why is this here, not in db? because it's easier to resolve dependencies
# this way
# From Turbogears, originally
from sqlalchemy.orm import create_session as orm_create_session

import sqlalchemy.orm
sqlalchemy.orm.ScopedSession = sqlalchemy.orm.scoped_session

from bazbase.benchmark import benchmarking
from bazbase.dependencies import Dependencies

TXT=u"txt"

class Symbol(object):
    pass
NOT_BLANK = Symbol()

def to_str(s):
    """Takes a unicode or str, and returns a str."""
    # assert not isinstance(s, str), s
    if hasattr(s, 'encode'):
        return s.encode('utf-8')
    else:
        return s

def one_of_list(l):
    if len(l) > 1:
        raise MultipleResultsFound(l)
    elif len(l) == 0:
        raise NoResultFound(l)
    else:
        return l[0]

class LongBlob(LargeBinary):
    __visit_name__ = 'long_blob'

@compiles(LongBlob)
def compile_long_blob(type_, compiler, **kw):
        return "BLOB"

# Need to allow for pretty large cached values and uploaded files
# in prod.
@compiles(LongBlob, "mysql")
def compile_long_blob_mysql(type_, compiler, **kw):
        return "LONGBLOB"

def get_engine():
    """Retrieve the engine based on the current configuration."""
    global _engine
    if not _engine:
        args = custom.get_sqlalchemy_args()
        url = args['url']
        assert url is not None,args
        del args['url']
        _engine = sqlalchemy.create_engine(url, **args)
    if not metadata.is_bound():
        metadata.bind = _engine
        return _engine
def reset_engine():
    global _engine, metadata, session
    _engine = None
    metadata = sqlalchemy.MetaData()
    session = scoped_session(create_session)
    elixir.metadata, elixir.session = metadata, session

from sqlalchemy.orm import sessionmaker, scoped_session
# autocommit is necessary to not have us start in a transaction,
# for some reason.
Session = sessionmaker(autocommit=True)
def create_session():
    """Create a session that uses the engine from thread-local metadata."""
    if not metadata.is_bound():
        get_engine()
    return Session()

import elixir
from elixir import (ManyToOne, Entity, Field, OneToMany,
                    using_options, using_table_options,
                    ManyToMany, setup_all,
                    drop_all, create_all)

from . import NoResultFound, MultipleResultsFound
from bazbase.flavors import FLAVORS
from bazbase import custom, db

from datetime import datetime

reset_engine()
elixir.options_defaults['table_options'] = dict(mysql_engine='InnoDB',
                                                mysql_charset='utf8',
                                                mysql_collate='utf8_bin')

def bazname(ent):
    return 'baz_'+ent.__name__.lower()

class SynManyToOne(ManyToOne):
    def __init__(self,of_kind,synonym,*args,**kw):
        ManyToOne.__init__(self,of_kind,*args,**kw)
        self.synonym = synonym
    def create_properties(self):
        ManyToOne.create_properties(self)
        self.add_mapper_property(self.synonym,synonym(self.name))
        
class Element(Entity):
    _ename = Field(Unicode(128), unique=True, required=True, index=True,
                   colname='ename', synonym='ename')
    # http://www.sitepoint.com/article/hierarchical-data-database/2/
    # Not unique because there's no standard SQL way to make our crazy
    # updates not break unique constraints
    treeleft = Field(Integer,required=True,index=True)
    treeright = Field(Integer,required=True,index=True)
    propvals = OneToMany('Propval')
    # can be "normal" or "toplevel"
    _orgmode = Field(Unicode(32),required=True,default=u'normal',
                     colname='orgmode',synonym='orgmode')
    # Version is incremented when our ancestry or children change,
    # or a propval is added to or removed from us.
    version = Field(Integer, required=True, default=0)

    using_options(order_by='treeleft',
                  tablename=bazname)
    
    @staticmethod
    def get(*args, **kw):
        if len(args) > 0:
            assert len(args) == 1
            ret = Element.get_by(ename=args[0])
            if ret is None:
                raise NoResultFound(args[0])
            return ret
        else:
            return one_of_list(Element.__search(kw))

    @staticmethod
    def get_raw(**kw):
        if len(kw) == 1:
            k, v = kw.items()[0]
            prop = Prop.get(unicode(k, 'utf-8'))
            pv = Propval.get_by(prop=prop, value=v)
            if pv is None:
                raise NoResultFound("No propval with %s='%s'!"
                                    % (k, v))
            else:
                return pv.element
        else:
            return one_of_list(Element.__search(kw, raw=True))
    
    @classmethod
    def get_with(cls, __dct, **kw):
        if __dct is not None:
            return one_of_list(cls.__search(__dct, is_unicode=True))
        else:
            return one_of_list(cls.__search(kw))

    def __init__(self, ename, parent=False, treeleft=0,treeright=0,
                 fromhook=False):
        assert '.' not in ename
        assert ' ' not in ename
        assert '/' not in ename
        if fromhook:
            assert treeleft != 0
            assert treeright != 0
        else:
            assert treeleft == 0
            assert parent is not False
        Entity.__init__(self,_ename=ename,treeleft=treeleft,
                        treeright=treeright)
        if not fromhook:
            from . import structure
            # This has a side-effect that sets treeleft,treeright properly
            if parent is not None:
                parent = structure.SqlElement(parent)
            db.esetattr(structure.SqlElement(self), 'parent', None, parent)

    @property
    def parent(self):
        if self.treeleft == 0:
            # Not done being constructed yet
            return False
        else:
            return (Element.query.filter(
                and_(Element.treeleft < self.treeleft,
                     Element.treeright > self.treeright))
                    .order_by(Element.treeright-Element.treeleft).first())

    @property
    def orgmode(self):
        return self._orgmode

    @staticmethod
    def getRoot():
        return Element.get_by(treeleft=1)

    def isRoot(self):
        return self.treeleft == 1

    @classmethod
    def getAll(cls):
        """Returns all elements, starting with the root element and always
        returning the parent before its children."""
        return cls.query.order_by(Element.treeleft).all()

    def __querySubtypes(self):
        midparent = aliased(Element)
        return (Element.query.filter(
                and_(Element.treeleft > self.treeleft,
                     Element.treeright < self.treeright,
                     ~exists().where(
                        and_(midparent.treeleft > self.treeleft,
                             midparent.treeright < self.treeright,
                             Element.treeleft > midparent.treeleft,
                             Element.treeright < midparent.treeright)))))
    def getChildren(self):
        return self.__querySubtypes().all()
    def hasChildren(self):
        return self.treeright - self.treeleft > 1
    
    def __listDescendants(self, includeMe=False, restrictions={}, query=None):
        if includeMe:
            a = Element.treeleft >= self.treeleft
            b = Element.treeright <= self.treeright
        else:
            a = Element.treeleft > self.treeleft
            b = Element.treeright < self.treeright
        if query is None:
            query = Element.query
        query = query.filter(and_(a, b)).order_by(Element.treeleft)
        if restrictions:
            return Element.__search(restrictions, query=query)
        else:
            return query.all()
    def getDescendants(self,**kw):
        """Return a list of this Element's descendents, depth-first order."""
        return self.__listDescendants(restrictions=kw)
    def withDescendants(self,**kw):
        """Return a list of this Element and its descendents."""
        return self.__listDescendants(includeMe=True, restrictions=kw)
    def countDescendants(self):
        return (self.treeright - self.treeleft - 1) / 2
    def getLeaves(self):
        return self.__listDescendants(includeMe=True,
                                      query=Element.query.filter(
            Element.treeright-Element.treeleft==1))
    def getNonleaves(self):
        return self.__listDescendants(includeMe=True, query=Element.filter(
            Element.treeright-Element.treeleft!=1))
    def __queryAncestors(self):
        return (Element.query.filter(and_(Element.treeleft < self.treeleft,
                                          Element.treeright > self.treeright))
                .order_by(Element.treeleft))
    def getAncestors(self,*args,**kw):
        return self.__queryAncestors(*args,**kw).all()
    def isAncestorOf(self,element):
        return (self.treeleft < element.treeleft
                and self.treeright > element.treeright)

    def childTree(self):
        """Return a child tree structure.  Since there's only a single root,
        just returns that root, not a list of roots."""

        return Element.asTree(self.__listDescendants(includeMe=True))[0]

    def queryRelations(*elements):
        """Return a query for all elements either an ancestor or a descendent
        of any of the given elements.  Includes the given elements.
        Can be called like e.queryRelations()
        or Element.queryRelations(e1,e2,e3)."""

        return Element.query.filter(
            or_(or_(and_(Element.treeleft < e.treeleft,
                         Element.treeright > e.treeright)
                    for e in elements),
                or_(and_(Element.treeleft >= e.treeleft,
                         Element.treeright <= e.treeright)
                    for e in elements))).order_by(Element.treeleft)

    @staticmethod
    def asTree(elms):
        """Formats elms as a tree.

        elms should be a list of elements ordered by treeleft.
        The return value is a list of roots.  A node is either a leaf
        or a subtree.
        A leaf is [element].  A subtree is [element, [<>], [<>]]."""
        ret = []
        stack = [ret]
        while len(elms) > 0:
            e = elms.pop(0)
            while len(stack) > 1 and stack[-1][0].treeright < e.treeleft:
                stack.pop()
            bubble = [e]
            stack[-1].append(bubble)
            stack.append(bubble)
        return ret
    
    @property
    def ename(self):
        return self._ename

    def __unicode__(self):
        return self.ename
    def __repr__(self):
        return "<model.Element: %s>"%self.ename.encode('utf-8')

    def __contains__(self, key):
        return key in self.propname_list()

    def __getitem__(self, key):
        try:
            pv = Propval.query.join(Propval.prop).filter(
                and_(Prop.name == key,
                     Propval.element_id == self.id)).one()
            return pv
        except NoResultFound:
            raise KeyError("Element %s does not have prop %s!" % (self.ename,
                                                                  key))

    def propval_for_prop(self, prop):
        try:
            pv = Propval.query.filter(
                and_(Propval.prop_id == prop.id,
                     Propval.element_id == self.id)).one()
            return pv
                
        except NoResultFound:
            raise KeyError("Element %s does not have prop %s!" % (self.ename,
                                                                  prop.name))
    def __setitem__(self, key, value):
        prop = Prop.set_or_create(name=key)
        if not prop:
            raise KeyError("Prop '%s' is not defined!"%key)
        else:
            self.set_value_for_prop(prop, value)

    def set_value_for_prop(self, prop, value):
        # -? typecheck?
        Propval.set_or_create(element=self,prop=prop,value=value)
    
    def __delitem__(self, key):
        prop = Prop.get_by(name=key)
        if not prop:
            raise KeyError("Prop '%s' is not defined!"%key)
        pv = Propval.get_by(prop=prop,element=self)
        if pv is None:
            raise KeyError("Element %s has no value for %s!"
                           % (self.ename, key))
        if key not in self.parent:
            pv.delete()
        else:
            pv.value = prop.eval_default(self)
    def __iter__(self):
        return iter(self.propname_list())

    def propname_list(self):
        from . import structure
        return structure.SqlElement(self).list_props()

    @classmethod
    def __search(cls, restrictions, query=None, is_unicode=False, raw=False):
        """elm.search(category='mon',name='sakura') returns a list of
        elements with those propvals, using inheritance and such.

        restrictions must have string keys, like it game from **kw,
        unless is_unicode is True."""
        if query is None:
            query = cls.query
        elms = query.all()
        
        for pname in restrictions:
            newelms = []
            # TODO(xavid): deal with this more cleanly.
            if not is_unicode:
                pname = unicode(pname, 'utf-8')
            prop = Prop.get_by(name=pname)
            assert FLAVORS[prop.flavor].indexed
            if raw:
                assert FLAVORS[prop.flavor].raw
            else:
                assert not FLAVORS[prop.flavor].raw

            target = restrictions[pname]
            # TODO(xavid): replace by hasattr with a method
            if isinstance(target, Element):
                target = FLAVORS['reference'].element_to_string(target)

            for e in elms:
                if pname in e:
                    if raw:
                        rendered = e[pname].value
                    else:
                        from .wiki import NoContentException
                        try:
                            rendered = e[pname].render()
                        except NoContentException:
                            continue

                    if target is NOT_BLANK:
                        if rendered != '':
                            newelms.append(e)
                    else:
                        if rendered == target:
                            newelms.append(e)
            elms = newelms
        return elms
    @classmethod
    def search(cls, __dct=None, **kw):
        if __dct is not None:
            return cls.__search(__dct, is_unicode=True)
        else:
            return cls.__search(kw)

    def delete(self):
        db.edelete(self)

    def __eq__(self, other):
        return hasattr(other, 'ename') and self.ename == other.ename
    def __hash__(self):
        return hash(self.ename)

PARENT_DOT_THIS = '<<parent.this />>'

PLACEHOLDER_PAT = re.compile(r'^\s*<<<([\w.-]+)\s*/>>>')

# Valid flavors are currently u'string',u'text',u'integer',u'references',
# or u'boolean'
# but are basically defined in flavors and extended in custom
# All props with the same name have the same flavor, even if "unrelated"

class Prop(Entity):
    _name = Field(Unicode(128),unique=True,required=True,index=True,
                  colname='name', synonym='name')
    _flavor = Field(Unicode(128),colname='flavor',synonym='flavor')
    _default = Field(LargeBinary, required=True,
                     colname='default', synonym='default')
    # True for props that get viewed directly outside the edit view,
    # like "product".
    _visible = Field(Boolean, colname='visible', synonym='visible',
                      required=True)
    _comment = Field(UnicodeText(), colname='comment', synonym='comment')
    propvals = OneToMany('Propval')

    using_options(tablename=bazname)
    
    @staticmethod
    def get(pname):
        ret = Prop.get_by(name=pname)
        if ret is None:
            raise NoResultFound("No property '%s' defined!"%pname)
        return ret

    def __init__(self, name, flavor=None, default=None,
                 visible=False, comment=u'', fromhook=False):
        assert ' ' not in name
        assert '.' not in name
        assert ':' not in name
        assert '/' not in name
        assert '=' not in name
        assert '@' not in name
        assert '|' not in name
        assert '^' not in name
        assert '>' not in name
        if flavor is None:
            flavor = custom.DEFAULT_FLAVOR
        if default is None:
            default = PARENT_DOT_THIS
        else:
            # assert isinstance(default, str), repr(default)
            pass
            
        Entity.__init__(self,_name=name, _flavor=flavor,
                        _default=default, _visible=visible,
                        _comment=comment)
        if not fromhook:
            from . import structure
            sqlp = structure.SqlProp(self)
            db.psetattr(sqlp, 'flavor', flavor)
            db.psetattr(sqlp, 'default', default)
            db.psetattr(sqlp, 'visible', visible)
            db.psetattr(sqlp, 'comment', comment)

    def __set_name(self,newname):
        db.psetattr(self,'pname',newname)
    def __get_name(self):
        return self._name
    name = property(__get_name,__set_name)

    def __set_flavor(self,newflavor):
        db.psetattr(self,'flavor',newflavor)
    def __get_flavor(self):
        return self._flavor
    flavor = property(__get_flavor,__set_flavor)

    def __set_default(self,newdefault):
        db.psetattr(self,'default',newdefault)
    def __get_default(self):
        return self._default
    default = property(__get_default,__set_default)

    def eval_default(self, element):
        m = PLACEHOLDER_PAT.search(self.default)
        if m is not None:
            holder = m.group(1)
            if holder == "now":
                return FLAVORS['timestamp'].now()
            elif holder == "ename":
                if (element.parent[self.name].value and
                    element.parent[self.name].value != element.parent.ename):
                    return PARENT_DOT_THIS
                else:
                    return element.ename.encode('utf-8')
            elif holder == "parent.this":
                return element.parent[self.name].value
            else:
                raise KeyError("Unknown prop default placeholder '%s'!"
                               % holder)
        else:
            return self.default

    def __set_visible(self, newvisible):
        db.psetattr(self, 'visible', newvisible)
    def __get_visible(self):
        return self._visible
    visible = property(__get_visible, __set_visible)

    def __set_comment(self, newcomment):
        db.psetattr(self, 'comment', newcomment)
    def __get_comment(self):
        return self._comment
    comment = property(__get_comment, __set_comment)

    def delete(self):
        db.pdelete(self)

    @classmethod
    def getAll(cls):
        return cls.query.all()

    def elementTree(self):
        return Element.asTree(Element.query.join(Element.propvals)
                              .filter(Propval.prop_id == self.id)
                              .order_by(Element.treeleft).all())


    @classmethod
    def set_or_create(cls,name,flavor=None):
        ret = cls.get_by(name=name)
        if ret is None:
            if flavor is None:
                flavor = custom.DEFAULT_FLAVOR
            ret = cls(name=name,
                      flavor=flavor)
        elif flavor is not None:
            ret.flavor = flavor
        return ret

class Propval(Entity):
    prop = ManyToOne('Prop',required=True)
    element = ManyToOne('Element',required=True)
    _value = Field(LongBlob, required=True,
                   colname='value', synonym='value')
    # A map of overlay identifiers to overlayed values.
    overlays = Field(PickleType, default={})
    version = Field(Integer, required=True, default=0)
    #format = Field(Unicode(32), required=True, default=u'creole')

    using_options(tablename=bazname)

    def __init__(self,element,prop,value=None,fromhook=False):
        assert element is not None,kw
        if prop.flavor != u'blob':
            assert '<<<' not in str(value), repr(str(value))
        if value is None:
            value = prop.eval_default(element)
            assert '<<<' not in str(value), repr(str(value))
        # assert isinstance(value, str), repr(value)
        Entity.__init__(self, element=element,
                        prop=prop, _value=value)
        if not fromhook:
            db.setprop(element, prop.name, value)

    @classmethod
    def getAll(cls):
        return cls.query.all()

    #@property
    #def value(self):
    #    return self._value

    def get_value_and_format(self):
        if self.prop.flavor == 'blob':
            if self._value.startswith('<<<format:'):
                format = self._value[len('<<<format:'):]
                with open(blobname(self.element.ename, self.prop.name)) as fil:
                    value = fil.read()
            else:
                assert not self._value.startswith('<<<'), self._value
                value = self._value
                format = 'creole'
        else:
            value = self._value
            format = 'creole'
        return value, format

    def __repr__(self):
        try:
            return "<model:%s[%s]>" % (self.element.ename.encode('utf-8'),
                                       self.prop.name.encode('utf-8'))
        except UnboundExecutionError:
            return "<model:Unbound Propval>"

    def __unicode__(self):
        return unicode(self.render(method='unicode', offset=1))

    def render(self, format=TXT, filters={}, method='render', offset=0):
        from . import conversion
        im = conversion.convert_any(self.element.ename, self.prop.name,
                                    [format], filters,
                                    method=method, offset=offset + 1)
        return im.asData()
    
    def has_overlay(self, identifier):
        return identifier in self.overlays
    def get_overlay(self, identifier):
        return self.overlays[identifier]
    def set_overlay(self, identifier, value):
        self.overlays[identifier] = value

setup_all()

# Indexes -- sort of a hack
Index('PropvalInd',Propval._descriptor.get_column('prop_id'),
      Propval._descriptor.get_column('element_id'),
      unique=True)
# Needs SQLAlchemy 0.7+
#Index('PropAndValue', Propval._descriptor.get_column('prop_id'),
#      Propval._descriptor.get_column('value'), mysql_length=16)

blobdir = None

def blobname(ename, pname):
    return os.path.join(blobdir, '%s.%s.blob' % (ename, pname))

CREOLE_COMPAT_FORMATS = ('creole', '.creole', '.txt')

class hook(object):
    def begin(self):
        session.begin()
    def prepare(self):
        session.flush()
    def commit(self):
        session.commit()
        session.close()
    def abort(self):
        # Close does an implicit rollback(), but if we do an explicit one
        # we don't session.expunge_all() properly
        # session.rollback()
        session.close()

    def invalidate(self, propvals):
        for pv in propvals:
            if pv.version is None:
                pv.version = 1
            else:
                pv.version += 1
    def invalidate_propval(self, propval):
        self.invalidate(propvals=[propval])
    def invalidate_prop(self, prop):
        self.invalidate(propvals=prop.propvals)
    def invalidate_element(self, element):
        if element.version is None:
            element.version = 1
        else:
            element.version += 1

    def esetattr(self,e,attr, oldval, val):
        e = e._e
        assert attr in ('parent','ename','orgmode')
        if attr == 'ename':
            assert e is not None
            oldename = e.ename
            e._ename = val
            # Cache entries include the element ID in addition to ename,
            # so this can't reuse old cache entries.
        elif attr == 'orgmode':
            e._orgmode = val
        elif attr == 'parent':
            parent = val._e if val else None
            if e is None or e.treeleft == 0:
                self.oldparent = None
                self.created = True
                precount = Element.query.filter(Element.treeleft != 0).count()
                if parent is None:
                    assert precount == 0,precount
                    # Must be the toplevel element, which is unique
                    treeleft = 1
                    treeright = 2
                else:
                    assert Element.getRoot().treeright == precount*2,(
                        "precondition",
                        Element.getRoot().treeright,precount*2)
                    # Move old nodes for insert
                    session.flush()
                    point = parent.treeright
                    Element.query.filter(Element.treeright >= point).update(dict(treeright=Element.treeright+2))
                    Element.query.filter(Element.treeleft >= point).update(dict(treeleft= Element.treeleft+2))
                    treeleft = point
                    treeright = point+1
                    session.expire_all()
                e.treeleft = treeleft
                e.treeright = treeright

                if parent is not None:
                    assert e.parent == parent,(repr(point),
                                                  repr(e),
                                                  (e.treeleft,e.treeright),
                                                  repr(e.parent),
                                                  (e.parent.treeleft,
                                                   e.parent.treeright),
                                                  repr(parent),
                                                  (parent.treeleft,
                                                   parent.treeright))
                    assert Element.getRoot().treeright == (precount+1)*2,(
                        Element.getRoot().treeright, (precount+1)*2)
            else:
                # Changing parent
                self.oldparent = e.parent
                self.created = False
                assert self.oldparent is not None,(e.ename,e.treeleft)
                assert parent is not None
                assert not (parent.treeleft > e.treeleft
                            and parent.treeright < e.treeright),"An element cannot be its own grandpa!"

                # Mod tree
                oldleft = e.treeleft
                oldright = e.treeright
                delta = oldright-oldleft
                #rightq = parent.treeright > self.oldparent.treeright
                rightq = parent.treeright > oldright
                if rightq:
                    newleft = parent.treeright - delta - 1
                else:
                    newleft = parent.treeright
                newright = newleft + delta
                dist = newleft-oldleft
                # We need to get the ids of us and children first, because the
                # following updates will cause confusion
                descandme = [e.id for e in Element.query.filter(
                        and_(Element.treeleft >= oldleft,
                             Element.treeright <= oldright))]
                session.flush()
                if rightq:
                    session.execute(Element.table.update(
                        and_(Element.treeright > oldright,
                             Element.treeright <= newright),
                        values=dict(treeright=Element.treeright-delta-1)))
                    session.execute(Element.table.update(
                        and_(Element.treeleft > oldright,
                             Element.treeleft <= newright),
                        values=dict(treeleft=Element.treeleft-delta-1)))
                else:
                    session.execute(Element.table.update(
                        and_(Element.treeright >= newleft,
                             Element.treeright < oldleft),
                        values=dict(treeright=Element.treeright+delta+1)))
                    session.execute(Element.table.update(
                        and_(Element.treeleft >= newleft,
                             Element.treeleft < oldleft),
                        values=dict(treeleft=Element.treeleft+delta+1)))
                # Me and my children, so both left and right must be in the range
                session.execute(Element.table.update(
                    Element.id.in_(descandme),
                    values=dict(treeleft=Element.treeleft + dist,
                                treeright=Element.treeright + dist)
                    ))
                session.expire_all()

                # Clear defaulting propvals we no longer need
                npropval = aliased(Propval)
                opropval = aliased(Propval)
                for pv in Propval.query.join(Propval.prop).filter(
                    and_(Propval.element_id == e.id,
                         Propval.value == Prop.default,
                         exists().where(
                            and_(opropval.element_id == self.oldparent.id,
                                 opropval.prop_id == Propval.prop_id)),
                         ~exists().where(
                            and_(npropval.element_id == parent.id,
                                 npropval.prop_id == Propval.prop_id)))):
                    pv.delete()
    def post_esetattr(self, se, attr, oldval, val):
        e = se._e
        if attr == 'parent':
            if e.treeleft != 1:
                parent = e.parent
                # Defaulting propvals
                for pv in Propval.query.filter_by(element_id=parent.id):
                    mev = Propval.get_by(prop_id=pv.prop_id, element_id=e.id)
                    if mev is None:
                        # Create defaults to parent
                        db.setprop(se, pv.prop.name, pv.prop.eval_default(e),
                                   'creole')
                # Expire here, because otherwise the invalidations we're
                # about to do can break things
                session.flush()
                session.expire_all()
                self.invalidate_element(e)
                self.invalidate_element(parent)
                # Invalidate descendants, because this may add or remove
                # propvals from them.
                for d in e.getDescendants():
                    self.invalidate_element(d)
        
    def edelete(self,se):
        e = se._e
        assert e.treeleft != 1,"You can't delete the root element."
        assert e.treeright - e.treeleft == 1,"You can't delete an element with children."
        for pv in e.propvals:
            self.delete(se,pv.prop.name,invalidateDependencies=False,
                        unconditionally=True)
        oldright = e.treeright
        # The element no longer exitsts, so its cache entries won't be used.
        Entity.delete(e)
        # Fill in the gap; we know that we have no children
        session.flush()
        session.execute(Element.table.update(
            Element.treeright > oldright,
            values=dict(treeright=Element.treeright-2)))
        session.execute(Element.table.update(
            Element.treeleft > oldright,
            values=dict(treeleft=Element.treeleft-2)))
        session.expire_all()

    def psetattr(self,p,attr,val):
        if p is None:
            assert False
            #assert attr != 'pname'
            #p=Prop(name=pname,fromhook=True,
            #       **{attr:val})
        else:
            p = p._p
            if attr in ('flavor', 'visible', 'comment'):
                setattr(p, '_'+attr, val)
                if attr != 'comment':
                    self.invalidate_prop(p)
            elif attr == 'default':
                olddefault = p.default
                p._default = to_str(val)
                # Mod whatever was defaulting
                session.flush()
                session.execute(Propval.table.update(
                    and_(Propval.prop == p,
                         Propval.value == olddefault),
                    values=dict(value=val)))
                session.expire_all()
                # TODO(xavid): could get away with only invalidating the
                #              ones that were changed.
                self.invalidate_prop(p)
            elif attr == 'pname':
                oldname = p._name
                p._name = val
                # Things are cached by both propname and prop id, so this
                # can't steal cache entries.
            else:
                raise Exception('Unknown pattr %s!'%attr)
        
    def pdelete(self,p):
        assert len(p._p.propvals) <= 0,"You can't delete an in-use Prop! Used by: " + repr(p._p.propvals)
        Entity.delete(p._p)

    def setprop(self, e, pname, val, format):
        e = e._e
        assert isinstance(val, str),repr(val)
        p = Prop.set_or_create(name=pname)
        if not FLAVORS[p.flavor].binary and not FLAVORS[p.flavor].raw:
            assert '<<<' not in val, repr(val)

        is_blob = FLAVORS[p.flavor].binary

        if is_blob and val != '' and val != PARENT_DOT_THIS:
            with open(blobname(e.ename, pname), 'w') as fil:
                fil.write(val)
            assert format != 'creole', (pname, val)
            val = '<<<format:' + format.encode('utf-8')
        else:
            assert format in CREOLE_COMPAT_FORMATS, format
        
        pv = Propval.get_by(element=e,prop=p)
        if pv is None:
            pv = Propval(element=e,prop=p,value=val,fromhook=True)
            self.invalidate_element(e)
        else:
            pv._value = val
        self.invalidate_propval(pv)

    def post_setprop(self, se, pname, val, format):
        e = se._e
        p = Prop.get(pname)
        # -? Not really necessary if the prop wasn't just created...
        for sub in e.getChildren():
            kidv = Propval.get_by(prop=p,element=sub)
            if kidv is None:
                from . import structure
                # Create defaults to parent; recurses.
                db.setprop(structure.SqlElement(sub), pname,
                           p.eval_default(sub), 'creole')
    
    def delete(self,e,pname,invalidateDependencies=True, unconditionally=False):
        e = e._e
        p = Prop.get(pname)
        pv = Propval.get_by(element=e,prop=p)

        if invalidateDependencies:
            self.invalidate_propval(pv)
            self.invalidate_element(e)
            for d in e.getDescendants():
                self.invalidate_element(d)
        if (not unconditionally and e.parent is not None
            and Propval.get_by(element=e.parent, prop=p) is not None):
            pv.value = p.eval_default(e)
        else:
            Entity.delete(pv)

# Megaops
def clear():
    # Possibly this should be something like:
    # meta.reflect(bind=someengine)
    # for table in reversed(meta.sorted_tables):
    #         someengine.execute(table.delete())
    # to handle tables that used to exist but no longer do.        
    drop_all()
    create_all()

    for f in os.listdir(blobdir):
        if f.endswith('.blob'):
            os.unlink(os.path.join(blobdir, f))

def verifyTree():
    # This breaks the abstraction barrier; possibly it should live in model
    from sqlalchemy import or_, and_
    with db.begin_transaction():
        count = Element.query.count()
        for i in xrange(1,2*count+1):
            assert (Element.query.filter(
                or_(Element.treeleft == i,
                    Element.treeright == i)).count() == 1)
        for e in Element.query.all():
            assert (e.treeleft < e.treeright)
            assert ((e.treeright - e.treeleft + 1) // 2 ==
                    Element.query.filter(
                        and_(Element.treeleft >= e.treeleft,
                             Element.treeright <= e.treeright)
                    ).count())

__all__ = ['Element', 'Prop', 'Propval']
