class Attr_Diff(dict):
    """
    Represents a change to note attributes, where nodes can represent
    either logical nodes or logical links, and attributes can be added,
    changed or removed
    
    Example:
            attr_diff = {'__type_node' : {n_id: {'__attr_write': {'attr_0': 0,
                                                                  'attr_1': 'a'},
                                                '__attr_remove': ['attr_2'] }}
                         '__type_link' : {l_id: ... }
                        }
    """
    def __init__(self):
        self['__type_node'] = {}
        self['__type_link'] = {}
        self['meta'] = {}

    def init_node_attr_diff(self, n_id):
        ret = {'__attr_write': {},
               '__attr_remove': []}
        self['__type_node'][n_id] = ret
        return ret

    def init_link_attr_diff(self, l_id):
        ret = {'__attr_write': {},
               '__attr_remove': []}
        self['__type_link'][l_id] = ret
        return ret

    @staticmethod
    def from_json_dict(json_dict):
        ret = Attr_Diff()
        for obj_type, writer, remover in [
            ('__type_node', ret.add_node_attr_write, ret.add_node_attr_rm),
            ('__type_link', ret.add_link_attr_write, ret.add_link_attr_rm),
            ]:
            obj_ad_set = json_dict.get(obj_type)
            if None != obj_ad_set:
                for o_id, ad in obj_ad_set.items():
                    if None != ad.get('__attr_write'):
                        for k, v in ad['__attr_write'].items():
                            writer(o_id, k, v)
                    if None != ad.get('__attr_remove'):
                        for k in ad['__attr_remove']:
                            remover(o_id, k)
        if 'meta' in json_dict:
            ret['meta'] = json_dict['meta']
        return ret

    @property
    def type__node(self):
        return self['__type_node']

    @property
    def type__link(self):
        return self['__type_link']

    @property
    def meta(self):
        return self['meta']

    def add_node_attr_write(self, n_id, attr_name, attr_val):

        assert 'id' != attr_name.lower(), 'Attr_Diff: attempt to write to \'id\' attribute'

        n_attr_diff = self['__type_node'].get(n_id)
        if None == n_attr_diff:
            n_attr_diff = self.init_node_attr_diff(n_id)
        n_attr_diff['__attr_write'][attr_name] = attr_val

    def add_node_attr_rm(self, n_id, attr_name):

        assert 'id' != attr_name.lower(), 'Attr_Diff: attempt to remove \'id\' attribute'

        n_attr_diff = self['__type_node'].get(n_id)
        if None == n_attr_diff:
            n_attr_diff = self.init_node_attr_diff(n_id)
        n_attr_diff['__attr_remove'].append(attr_name)

    def add_link_attr_write(self, l_id, attr_name, attr_val):

        assert 'id' != attr_name.lower(), 'Attr_Diff: attempt to write to \'id\' attribute'

        l_attr_diff = self['__type_link'].get(l_id)
        if None == l_attr_diff:
            l_attr_diff = self.init_link_attr_diff(l_id)
        l_attr_diff['__attr_write'][attr_name] = attr_val

    def add_link_attr_rm(self, l_id, attr_name):

        assert 'id' != attr_name.lower(), 'Attr_Diff: attempt to remove \'id\' attribute'
        assert 'name' != attr_name.lower(), 'Attr_Diff: attempt to remove link \'name\' attribute, use write instead'

        l_attr_diff = self['__type_link'].get(l_id)
        if None == l_attr_diff:
            l_attr_diff = self.init_link_attr_diff(l_id)
        l_attr_diff['__attr_remove'].append(attr_name) class Topo_Diff(object):
    """
    Represents a change to the graph topology
    """

    class Commit_Result_Type(dict):
        """
        Topo_Diff graph commit result type
        """
        def __init__(self, node_id_set_add=[],
                           link_id_set_add=[],
                           node_id_set_rm=[],
                           link_id_set_rm=[]):
            self['node_id_set_add'] = node_id_set_add
            self['link_id_set_add'] = link_id_set_add
            self['node_id_set_rm'] = node_id_set_rm
            self['link_id_set_rm'] = link_id_set_rm
            self['meta'] = {}

        @staticmethod
        def from_json_dict(json_dict):
            ret = Topo_Diff.Commit_Result_Type()
            ret.__dict__ = json_dict
            return ret

    class JSON_Encoder(json.JSONEncoder):
        """
        Topo_Diff is not a plain dict, so we provide a json encoder
        """
        def default(self, obj):
            return obj.to_json_dict()

    @staticmethod
    def from_json_dict(json_dict):
        """
        construct from dict - no node/link constructor set must be provided
        """
        ret = Topo_Diff()

        # merge keys - this allows constructor argument omission (link_id_set_rm,
        # node_id_set_rm, etc.) such as when constructing from POST JSON data
        for k, _ in ret.__dict__.items():
            v = json_dict.get(k)
            if None != v:
                ret.__dict__[k] = v
        return ret

    def __init__(self, link_id_set_rm=[],
                       node_id_set_rm=[],
                       node_set_add=[],
                       link_set_add=[],
                       meta={}):

        self.link_id_set_rm = link_id_set_rm
        self.node_id_set_rm = node_id_set_rm
        self.node_set_add = node_set_add
        self.link_set_add = link_set_add
        self.meta = {}

    def __str__(self):
        return __name__ + ': ' + ', '.join('%s: %s' % (k, v) for k, v in self.__dict__.items())

    def len__n_add(self):
        return len(self.node_set_add)

    def len__n_id_rm(self):
        return len(self.node_id_set_rm)

    def len__l_add(self):
        return len(self.link_set_add)

    def len__l_id_rm(self):
        return len(self.link_id_set_rm)

    def is_empty(self):
        return self.len__n_add() + self.len__n_id_rm() + self.len__l_add() + self.len__l_id_rm() == 0

    def to_json_dict(self):
        ret = {k: getattr(self, k) for k in ['link_id_set_rm',
                                             'node_id_set_rm',
                                             'node_set_add',
                                             'link_set_add',
                                             'meta'] }
        return ret

    def to_str__commit_name(self):
        return 'TD: +n:%d l:%d -n:%d -l:%d' % (len(self.node_set_add),
                                               len(self.link_set_add),
                                               len(self.node_id_set_rm),
                                               len(self.link_id_set_rm))

    def check_validity(self, topo_diff_dict):
        """
        Topo_Diff may represent invalid operations, eg. adding a link while
         removing it's end-point - this stub should check for that
        """
        pass class Link():
    """
    documentation anchor - this class currently carries no implementation
    and only acts as a documentation anchor

    link['__src'] - meta attribute for link source object
    link['__dst'] - meta attribute for link destination object
    """

    def __init__(self, src=None, dst=None):
        assert False, 'currently unused'

    class Link_Ptr(dict):
        """
        link['__src_id'] - meta attribute for link source id
        link['__dst_id'] - meta attribute for link destination id
        """
        def __init__(self, src_id=None, dst_id=None):
            assert None is not src_id and None is not dst_id
            self['__src_id'] = src_id
            self['__dst_id'] = dst_id

        @property
        def src_id(self):
            return self['__src_id']

        @property
        def dst_id(self):
            return self['__dst_id']

    @staticmethod
    def link_ptr(src_id=None, dst_id=None):
        """
        init from src_id or dst_id attributes - at least one must be provided
        """
        return Link.Link_Ptr(src_id, dst_id) class RZDoc():

    def __init__(self, rzdoc_name=None):
        self.id = None  # set upon DB commit
        self.name = rzdoc_name

    def __eq__(self, other):
        if not isinstance(other, RZDoc): return False

        assert self.id != None and other.id != None

        return self.id == other.id

    def __hash__(self):
        assert self.id != None

        return self.id.__hash__()

    def __str__(self):
        """ actually returns unicode. """
        return str_to_unicode('rzdoc: name: %s') % (self.name) class RZCommit():
    """
    Rhizi Commit object

    Currently this object is substituted by either a diff object, this currently
    acts as a model stub.
    """

    @staticmethod
    def diff_obj_from_blob(blob):
        """
        @return json.loads(gzip_decompress(base64_decode(blob)))
        """
        blob_gzip = base64.decodestring(blob.encode('utf-8'))
        blob = gzip.zlib.decompress(blob_gzip).decode()
        return json.loads(blob)

    @staticmethod
    def blob_from_diff_obj(obj):
        """
        @return: blob = base64(gzip(json.dumps(obj)))
        """
        obj_bytes = json.dumps(obj).encode('utf-8')
        blob_gzip = gzip.zlib.compress(obj_bytes)
        blob_base64 = base64.encodestring(blob_gzip)
        if not python2:
            blob_base64 = blob_base64.decode('utf-8')

        return blob_base64 class DB_Query(object):
    """
    DB query object
    """

    def __init__(self, q_arr, param_set={}):
        """
        @param q_arr: cypher query as an array, the string is ' '.join(q_str_or_array)
        is used to convert it to string type
        """
        assert type(q_arr) is list

        # establish query type
        self.q_str = ' '.join(q_arr)  # FIXME: rm
        self.pt_root = Cypher_Parser().parse_expression(self.q_str)
        self.param_set = param_set

    def __str__(self):
        return self.pt_root.str__cypher_query()

    def __repr__(self):
        return 'q: %s, params: %s\n%s' % (self.pt_root.str__cypher_query(),
                                          self.param_set,
                                          self.pt_root.str__struct_tree())

    def __iter__(self):
        for keyword, clause_set in self.pt_root.index__kw_to_clause_set().items():
            yield keyword, clause_set

    def t__add_node_filter__meta_label(self):

        if Query_Struct_Type.w == self.query_struct_type:
            return

        meta_label_cond = '0 = length(filter(_lbl in labels(n) where _lbl in {meta_label_set}))'  # filter nodes with meta labels

        c_set__where = self.pt_root.clause_set_by_kw('where')

        if not c_set__where:
            c_set__match = self.pt_root.clause_set_by_kw('match')

            first_match_clause = c_set__match.pop()
            wc = first_match_clause.spawn_sibling__adjacent(e_clause__where)
            wc.spawn_child(e_keyword, 'where')
            wc.spawn_child(e_value).value = meta_label_cond
        else:  # where clause present
            assert len(c_set__where) == 1, 't__add_node_filter__meta_label: no support for multi-where clauses query transformation'
            wc = c_set__where[0]
            wc_cond = wc.condition_value
            wc.set_condition(meta_label_cond + ' and ' + wc_cond)

        self.param_set['meta_label_set'] = neo4j_schema.meta_label_set

    @property
    def query_struct_type(self):
        """
        calc query_structure_type: read|write|read-write
        """
        r = False
        w = False
        for kw, _clause_set in self.pt_root.index__kw_to_clause_set().items():
            if kw in neo4j_cypher_parser.tok_set__kw__write:
                w = True
            if kw == 'match':
                r = True

        if r and w: return Query_Struct_Type.rw
        if r and not w: return Query_Struct_Type.r
        if w and not r: return Query_Struct_Type.w
        return Query_Struct_Type.unknown

    def str__cypher_query(self):
        return self.pt_root.str__cypher_query() class DB_result_set(object):
    def __init__(self, data):
        self.data = data

    def __iter__(self):
        for db_row_dict in self.data['data']:
            # example: dict: {u'row': [{u'title': u'foo'}]}
            assert None != db_row_dict['row']

            yield DB_row(db_row_dict['row'])

    def items(self):
        return [x for x in self] class DB_Raw_Query(object):
    """
    Raw DB query object which does not undergo parsing
    """

    def __init__(self, q_arr, param_set={}):
        self.q_arr = q_arr
        self.param_set = param_set

    def __iter__(self):
        for keyword, clause_set in self.pt_root.index__kw_to_clause_set().items():
            yield keyword, clause_set

    def str__cypher_query(self):
        return ' '.join(self.q_arr) def cfmt(fmt_str, *args, **kwargs):
    """
    @deprecated
    """
    return Cypher_String_Formatter().format(fmt_str, *args, **kwargs) def generate_random_id__uuid():
    """
    generate a random UUID based string ID
    """
    return str(uuid.uuid4()) def rzdoc__ns_label(rzdoc):
    """
    [!] must be quoted when used in queries
    """
    return neo4j_schema.META_LABEL__RZDOC_NS_PREFIX + rzdoc.id def quote__singlequote(label):
    """
    quote label (possibly containing spaces) with single quotes
    """
    return tok__quote__singlequote + label + tok__quote__singlequote def rzdoc__meta_ns_label(rzdoc):
    """
    [!] must be quoted when used in queries
    """
    return neo4j_schema.META_LABEL__RZDOC_NS_META_PREFIX + rzdoc.id def quote__backtick(label):
    """
    quote label (possibly containing spaces) with backticks
    """
    return tok__quote__backquote + label + tok__quote__backquote RESERVED_LABEL__EMPTY_STRING = '__EMPTY_STRING__'
from copy import deepcopy
from time import time
from flask import current_app # for user_db
from .model.graph import Attr_Diff
from .model.graph import Topo_Diff
from .model.model import Link, RZDoc, RZCommit
from .neo4j_cypher import DB_Query, DB_result_set, DB_Raw_Query
from .neo4j_util import cfmt
from .neo4j_util import generate_random_id__uuid, rzdoc__ns_label, \
    quote__singlequote, rzdoc__meta_ns_label, quote__backtick
from . import neo4j_util
from .neo4j_util import RESERVED_LABEL__EMPTY_STRING
from . import neo4j_schema
from . import neo4j_util as db_util
import hashlib
import logging
import re
#    This file is part of rhizi, a collaborative knowledge graph editor.
#    Copyright (C) 2014-2015  Rhizi
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as published
#    by the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.






log = logging.getLogger('rhizi')


class DBO_factory__default(object):
    """
    Default configuration DB op factory, providing the following function:
       - gen_op__rzdb__init_DB(): DB init op
    """

    def gen_op__rzdb__init_DB(self):
        return DBO_rzdb__init_DB(neo4j_schema.RZDOC__DEFAULT_MAINPAGE_NAME,
                                 neo4j_schema.RZDOC__NAME__MAX_LENGTH)


class DB_op(object):
    """
    Transaction (tx) wrapped DB operation possibly composing multiple DB queries
    """
    def __init__(self):
        self.query_set = []
        self.result_set = []
        self.error_set = None
        self.tx_id = None  # int
        self.tx_commit_url = None  # cached from response to tx begin

    def __iter__(self):
        for dbq in self.query_set:
            yield dbq

    def __repr__(self):
        return '%s: tx-id: %s' % (self.__class__.__name__, str(self.tx_id))  # id may be null if Tx open failed

    def iter__r_set(self):
        """
        iterate over (DB_Query index, DB_Query, result | error)
        where result & error are mutually exclusive

        note: index is zero based

        TODO: handle partial iteration due to error_set being non-empty
        """
        r_set_len = len(self.result_set)

        #
        # q_idx: statement index
        # dbq: db query
        # row-set: query result
        #
        for q_idx, dbq in enumerate(self.query_set):
            r_set = None
            if q_idx < r_set_len:  # support partial result recovery

                r_set = DB_result_set(self.result_set[q_idx])