"""
"""
import common
from common import pprint, any, all
import web
import simplejson
import account
 
def get_thing(store, key, revision=None):
    if isinstance(key, common.Reference):
        key = unicode(key)
    json = store.get(key, revision)
    return json and common.Thing.from_json(store, key, json)
 
class PermissionEngine:
    """Engine to check if a user has permission to modify a document.
    """
    def __init__(self, store):
        self.store = store
        self.things = {}
 
    def get_thing(self, key):
        try:
            return self.things[key]
        except KeyError:
            t = get_thing(self.store, key)
            self.things[key] = t
            return t
 
    def has_permission(self, author, key):
        # admin user can modify everything
        if author and author.key == account.get_user_root() + 'admin':
            return True
 
        permission = self.get_permission(key)
        if permission is None:
            return True
        else:
            groups = permission.get('writers') or [] 
            # admin users can edit anything
            groups = groups + [self.get_thing('/usergroup/admin')]
            for group in groups:
                if group.key == '/usergroup/everyone':
                    return True
                elif author is not None:
                    members = [m.key for m in group.get('members', [])]
                    if group.key == '/usergroup/allusers' or author.key in members:
                        return True
                else:
                    return False        
 
    def get_permission(self, key):
        """Returns permission for the specified key."""
        def parent(key):
            if key == "/":
                return None
            else:
                return key.rsplit('/', 1)[0] or "/"
 
        def _get_permission(key, child_permission=False):
            if key is None:
                return None
            thing = self.get_thing(key)
            if child_permission:
                permission = thing and (thing.get("child_permission") or thing.get("permission"))
            else:
                permission = thing and thing.get("permission")
            return permission or _get_permission(parent(key), child_permission=True)
 
        return _get_permission(key)
 
class SaveProcessor:
    def __init__(self, store, author):
        self.store = store
        self.author = author
        self.permission_engine = PermissionEngine(self.store)
 
        self.things = {}
 
        self.types = {}
 
        self.key = None
 
    def process_many(self, docs):
        keys = [doc['key'] for doc in docs]
        self.things = dict((doc['key'], common.Thing.from_dict(self.store, doc['key'], doc)) for doc in docs)
 
        def parse_type(value):
            if isinstance(value, basestring):
                return value
            elif isinstance(value, dict) and 'key' in value:
                return value['key']
            else:
                return None
 
        # for verifying expected_type, type of the referenced objects is required. 
        # Finding the the types in one shot instead of querying each one separately.
        for doc in docs:
            self.types[doc['key']] = parse_type(doc.get('type'))
        refs = list(k for k in self.find_references(docs) if k not in self.types)
        self.types.update(self.find_types(refs))
 
        prev_data = self.get_many(keys)
        docs = [self._process(doc['key'], doc, prev_data.get(doc['key'])) for doc in docs]
 
        return [doc for doc in docs if doc]
 
    def find_types(self, keys):
        types = {}
 
        if keys:
            d = self.store.get_metadata_list(keys)
            type_ids = list(set(row.type for row in d.values())) 
            typedict = self.store.get_metadata_list_from_ids(type_ids)
 
            for k, row in d.items():
                types[k] = typedict[row.type].key
        return types
 
    def find_references(self, d, result=None):
        if result is None:
            result = set()
 
        if isinstance(d, dict):
            if len(d) == 1 and d.keys() == ["key"]:
                result.add(d['key'])
            else:
                for k, v in d.iteritems():
                    if k != "type":
                        self.find_references(v, result)
        elif isinstance(d, list):
            for v in d:
                self.find_references(v, result)
        return result
 
    def get_thing(self, key):
        try:
            return self.things[key]
        except KeyError:
            t = get_thing(self.store, key)
            self.things[key] = t
            return t
 
    def get_type(self, key):
        try:
            return self.types[key]
        except KeyError:
            t = get_thing(self.store, key)
            return t and t.type.key
 
    def get_many(self, keys):
        d = self.store.get_many_as_dict(keys)
        return dict((k, simplejson.loads(json)) for k, json in d.items())
 
    def process(self, key, data):
        prev_data = self.get_many([key])
        return self._process(key, data, prev_data.get(key))
 
    def _process(self, key, data, prev_data=None):
        self.key = key # hack to make key available when raising exceptions.
 
 
        if 'key' not in data:
            data['key'] = key
 
        if web.ctx.get('infobase_bootstrap', False):
            return data
 
        assert data['key'] == key
 
        data = common.parse_query(data)
        self.validate_properties(data)
        prev_data = prev_data and common.parse_query(prev_data)
 
        if not web.ctx.get('disable_permission_check', False) and not self.has_permission(self.author, key):
            raise common.PermissionDenied(message='Permission denied to modify %s' % repr(key))
 
        type = data.get('type')
        if type is None:
            raise common.BadData(message="missing type", at=dict(key=key))
        type = self.process_value(type, self.get_property(None, 'type'))
        type = self.get_thing(type)
 
        # when type is changed, consider as all object is modified and don't compare with prev data.
        if prev_data and prev_data.get('type') != type.key:
            prev_data = None
 
        data = self.process_data(data, type, prev_data)
 
        for k in common.READ_ONLY_PROPERTIES:
            data.pop(k, None)
            prev_data and prev_data.pop(k, None)
 
        if data == prev_data:
            return None
        else:
            return data
 
    def has_permission(self, author, key):
        return self.permission_engine.has_permission(author, key)
 
    def get_property(self, type, name):
        if name == 'type':
            return web.storage(name='type', expected_type=web.storage(key='/type/type', kind="regular"), unique=True)
        elif name in ['permission', 'child_permission']:
            return web.storage(name=name, expected_type=web.storage(key='/type/permission', kind="regular"), unique=True)
        else:
            for p in type.get('properties', []):
                if p.get('name') == name:
                    return p
 
    def validate_properties(self, data):
        rx = web.re_compile('^[a-z][a-z0-9_]*$')
        for key in data:
            if not rx.match(key):
                raise common.BadData(message="Bad Property: %s" % repr(key), at=dict(key=self.key))
 
    def process_data(self, d, type, old_data=None, prefix=""):
        for k, v in d.items():
            if v is None or v == [] or web.safeunicode(v).strip() == '':
                del d[k]
            else:
                if old_data and old_data.get(k) == v:
                    continue
                p = self.get_property(type, k)
                if p:
                    d[k] = self.process_value(v, p, prefix=prefix)
                else:
                    d[k] = v
        if type:
            d['type'] = common.Reference(type.key)
 
        return d
 
    def process_value(self, value, property, prefix=""):
        unique = property.get('unique', True)
        expected_type = property.expected_type.key
 
        at = {"key": self.key, "property": prefix + property.name}
 
        if isinstance(value, list):
            if unique is True:
                raise common.BadData(message='expected atom, found list', at=at, value=value)
 
            p = web.storage(property.copy())
            p.unique = True
            return [self.process_value(v, p) for v in value]
 
        if unique is False:    
            raise common.BadData(message='expected list, found atom', at=at, value=value)
 
        type_found = common.find_type(value)
 
        if expected_type in common.primitive_types:
            # string can be converted to any type and int can be converted to float
            try:
                if type_found == '/type/string' and expected_type != '/type/string':
                    value = common.primitive_types[expected_type](value)
                elif type_found == '/type/int' and expected_type == '/type/float':
                    value = float(value)
            except ValueError, e:
                raise common.BadData(message=str(e), at=at, value=value)
        elif property.expected_type.kind == 'embeddable':
            if isinstance(value, dict):
                return self.process_data(value, property.expected_type, prefix=at['property'] + ".")
            else:
                raise common.TypeMismatch(expected_type, type_found, at=at, value=value)
        else:
            if type_found == '/type/string':
                value = common.Reference(value)
 
        type_found = common.find_type(value)
 
        if type_found == '/type/object':
            type_found = self.get_type(value)
 
            # type is not found only when the thing id not found.
            if type_found is None:
                raise common.NotFound(key=unicode(value), at=at)
 
        if expected_type != type_found:
            raise common.BadData(message='expected %s, found %s' % (property.expected_type.key, type_found), at=at, value=value)
        return value
 
 
class WriteQueryProcessor:
    def __init__(self, store, author):
        self.store = store
        self.author = author
 
    def process(self, query):
        p = SaveProcessor(self.store, self.author)
 
        for q in serialize(query):
            q = common.parse_query(q)
 
            if not isinstance(q, dict) or q.get('key') is None:
                continue
 
            key = q['key']                
            thing = get_thing(self.store, key)
            create = q.pop('create', None)
 
            if thing is None:
                if create:
                    q = self.remove_connects(q)
                else:
                    raise common.NotFound(key=key)
            else:
                q = self.connect_all(thing._data, q)
 
            yield p.process(key, q)
 
    def remove_connects(self, query):
        for k, v in query.items():
            if isinstance(v, dict) and 'connect' in v:
                if 'key' in v:
                    value = v['key'] and common.Reference(v['key'])
                else:
                    value = v['value']
                query[k] = value
        return query
 
    def connect_all(self, data, query):
        """Applys all connects specified in the query to data.
 
            >>> p = WriteQueryProcessor(None, None)
            >>> data = {'a': 'foo', 'b': ['foo', 'bar']}
 
            >>> query = {'a': {'connect': 'update', 'value': 'bar'}, 'b': {'connect': 'insert', 'value': 'foobar'}}
            >>> p.connect_all(data, query)
            {'a': 'bar', 'b': ['foo', 'bar', 'foobar']}
 
            >>> query = {'a': {'connect': 'update', 'value': 'bar'}, 'b': {'connect': 'delete', 'value': 'foo'}}
            >>> p.connect_all(data, query)
            {'a': 'bar', 'b': ['bar']}
 
            >>> query = {'a': {'connect': 'update', 'value': 'bar'}, 'b': {'connect': 'update_list', 'value': ['foo', 'foobar']}}
            >>> p.connect_all(data, query)
            {'a': 'bar', 'b': ['foo', 'foobar']}
        """
        import copy
        data = copy.deepcopy(data)
 
        for k, v in query.items():
            if isinstance(v, dict):
                if 'connect' in v:
                    if 'key' in v:
                        value = v['key'] and common.Reference(v['key'])
                    else:
                        value = v['value']            
                    self.connect(data, k, v['connect'], value)
        return data
 
    def connect(self, data, name, connect, value):
        """Modifies the data dict by performing the specified connect.
 
            >>> getdata = lambda: {'a': 'foo', 'b': ['foo', 'bar']}
            >>> p = WriteQueryProcessor(None, None)
 
            >>> p.connect(getdata(), 'a', 'update', 'bar')
            {'a': 'bar', 'b': ['foo', 'bar']}
            >>> p.connect(getdata(), 'b', 'update_list', ['foobar'])
            {'a': 'foo', 'b': ['foobar']}
            >>> p.connect(getdata(), 'b', 'insert', 'foobar')
            {'a': 'foo', 'b': ['foo', 'bar', 'foobar']}
            >>> p.connect(getdata(), 'b', 'insert', 'foo')
            {'a': 'foo', 'b': ['foo', 'bar']}
            >>> p.connect(getdata(), 'b', 'delete', 'foobar')
            {'a': 'foo', 'b': ['foo', 'bar']}
        """
        if connect == 'update' or connect == 'update_list':
            data[name] = value
        elif connect == 'insert':
            if value not in data[name]:
                data[name].append(value)
        elif connect == 'delete':
            if value in data[name]:
                data[name].remove(value)
        return data
 
def serialize(query):
    ""
    r"""Serializes a nested query such that each subquery acts on a single object.
 
        >>> q = {
        ...     'create': 'unless_exists',
        ...     'key': '/foo',
        ...     'type': '/type/book',
        ...     'author': {
        ...        'create': 'unless_exists',
        ...        'key': '/bar',
        ...     },
        ...     'descption': {'value': 'foo', 'type': '/type/text'}
        ... }
        >>> serialize(q)
        [{
            'create': 'unless_exists',
            'key': '/bar'
        }, {
            'author': {
                'key': '/bar'
            },
            'create': 'unless_exists',
            'descption': {
                'type': '/type/text',
                'value': 'foo'
            },
            'key': '/foo',
            'type': '/type/book'
        }]
        >>> q = {
        ...     'create': 'unless_exists',
        ...     'key': '/foo',
        ...     'authors': {
        ...         'connect': 'update_list',
        ...         'value': [{
        ...             'create': 'unless_exists',
        ...             'key': '/a/1'
        ...         }, {
        ...             'create': 'unless_exists',
        ...             'key': 'a/2'
        ...         }]
        ...     }
        ... }
        >>> serialize(q)
        [{
            'create': 'unless_exists',
            'key': '/a/1'
        }, {
            'create': 'unless_exists',
            'key': 'a/2'
        }, {
            'authors': {
                'connect': 'update_list',
                'value': [{
                    'key': '/a/1'
                }, {
                    'key': 'a/2'
                }]
            },
            'create': 'unless_exists',
            'key': '/foo'
        }]
    """
    def flatten(query, result, path=[], from_list=False):
        """This does two things. 
	    1. It flattens the query and appends it to result.
        2. It returns its minimal value to use in parent query.
        """
        if isinstance(query, list):
            data = [flatten(q, result, path + [str(i)], from_list=True) for i, q in enumerate(query)]
            return data
        elif isinstance(query, dict):
            #@@ FIX ME
            q = query.copy()
            for k, v in q.items():
                q[k] = flatten(v, result, path + [k])
 
            if 'key' in q:
                result.append(q)
 
            if from_list:
                #@@ quick fix
                if 'key' in q:
                    data = {'key': q['key']}
                else:
                    # take keys (connect, key, type, value) from q
                    data = dict((k, v) for k, v in q.items() if k in ("connect", "key", "type", "value"))
            else:
                # take keys (connect, key, type, value) from q
                data = dict((k, v) for k, v in q.items() if k in ("connect", "key", "type", "value"))
            return data
        else:
            return query
 
    result = []
    flatten(query, result)                         
    return result
 
if __name__ == "__main__":
    import doctest
    doctest.testmod()