from __future__ import absolute_import
from sqlbuilder import smartsql
from ascetic.mappers import mapper_registry, Mapper
from ascetic.relations import ForeignKey, RelationDescriptor
from ascetic.utils import cached_property, classproperty, to_tuple

try:
    str = unicode  # Python 2.* compatible
    string_types = (basestring,)
    integer_types = (int, long)
except NameError:
    string_types = (str,)
    integer_types = (int,)


class MpMapper(Mapper):
    """The simplest Materialized Path realization.

    Strong KISS principle.
    You shoul to create CHAR field with name "tree_path",
    and "parent_id".
    """

    PATH_SEPARATOR = '/'
    KEY_SEPARATOR = ':'
    PATH_DIGITS = 10

    def _do_prepare_model(self, model):
        setattr(model, 'parent', RelationDescriptor(ForeignKey(
            'self',
            field=tuple('parent_{}'.format(k) for k in to_tuple(self.pk)),
            related_name="children"
        )))
        super(MpMapper, self)._do_prepare_model(self.model)

    def _mp_encode(self, value):
        return str(value).replace('&', '&a').replace(self.KEY_SEPARATOR, '&k').replace(self.PATH_SEPARATOR, '&p')

    def _mp_decode(self, value):
        return value.replace('&p', self.PATH_SEPARATOR).replace('&k', self.KEY_SEPARATOR).replace('&a', '&')

    def _make_tree_path(self, obj):
        tree_path = self.KEY_SEPARATOR.join(self._mp_encode(i).zfill(self.PATH_DIGITS) for i in to_tuple(self.get_pk(obj)))
        if obj.parent:
            tree_path = self.PATH_SEPARATOR.join((obj.parent.tree_path, tree_path))
        tree_path += self.PATH_SEPARATOR
        return tree_path

    @cached_property
    def mp_root(self):
        return self.get_mapper(self.relations['parent'].model)

    def save(self, obj):
        """Sets content_type and calls parent method."""
        try:
            old_tree_path = self.original_data(obj)['tree_path']
        except (AttributeError, KeyError):
            old_tree_path = None

        super(MpMapper, self).save(obj)

        tree_path = self._make_tree_path(obj)

        if old_tree_path != tree_path:
            obj.tree_path = tree_path
            self.original_data(obj, tree_path=tree_path)
            self.mp_root.base_query.where(
                self.mp_root.sql_table.pk == self.get_pk(obj)
            ).update({
                'tree_path': tree_path
            })

            if old_tree_path is not None:
                for obj in self.mp_root.base_query.where(
                    self.mp_root.sql_table.tree_path.startswith(old_tree_path) &
                    self.mp_root.sql_table.pk != self.get_pk(obj)
                ).iterator():
                    self.mp_root.base_query.where(
                        self.mp_root.sql_table.pk == self.get_pk(obj)
                    ).update({
                        'tree_path': tree_path + obj.tree_path[len(old_tree_path):]
                    })
        return self

    def get_ancestors_chained(self, obj, root=False, me=False, reverse=True):
        objs = []
        current = obj
        while (current if root else current.parent_id):
            if current != obj or me:
                objs.append(current)
            current = current.parent
        if reverse:
            objs.reverse()
        return objs

    def get_ancestors(self, obj, root=False, me=False, reverse=True):
        q = self.query
        t = self.mp_root.sql_table
        steps = obj.tree_path.split(self.PATH_SEPARATOR)
        paths = []
        while steps:
            paths.append(self.PATH_SEPARATOR.join(steps) + self.PATH_SEPARATOR)
            paths.pop()
        q = q.where(t.tree_path.in_(paths))

        if not root:
            q = q.where(t.parent.is_not(None))
        if not me:
            q = q.where(t.pk != self.get_pk(obj))

        if reverse:
            q = q.order_by((t.tree_path,))
        else:
            q = q.order_by((t.tree_path.desc(),))
        return q

    def _make_ancestors_cond(self, obj):
        steps = obj.tree_path.split(self.PATH_SEPARATOR)
        paths = []
        while steps:
            paths.append(self.PATH_SEPARATOR.join(steps) + self.PATH_SEPARATOR)
            paths.pop()
        return self.mp_root.sql_table.tree_path.in_(paths)

    def _make_ancestors_cond2(self, obj):
        return smartsql.P(obj.tree_path).startswith(self.mp_root.sql_table.tree_path)

    def get_hierarchical_name(self, obj, sep=', ', root=False, me=True, reverse=True, namegetter=str):
        """returns children QuerySet instance for given parent_id"""
        return sep.join(map(namegetter, self.get_ancestors(obj, root=root, me=me, reverse=reverse)))

    def get_children(self, obj):
        """Fix for MTI"""
        return self.query.where(self.mp_root.sql_table.parent == self.get_pk(obj))

    def _descendants(self, obj):
        r = list(self.get_children(obj))
        for i in r:
            r += i._descendants(obj)
        return r

    def get_descendants_recursive(self, obj, me=False):
        r = []
        if me:
            r.append(obj)
        r += self._descendants(obj)
        return r

    def get_descendants(self, obj, me=False):
        t = self.mp_root.sql_table
        q = self.query.where(t.tree_path.startswith(obj.tree_path))
        if not me:
            q = q.where(t.pk != self.get_pk(obj))
        return q


class MpModel(object):

    def get_ancestors(self, root=False, me=False, reverse=True):
        return self._mapper.get_ancestors(self, root, me, reverse)

    def get_hierarchical_name(self, sep=', ', root=False, me=True, reverse=True, namegetter=str):
        return self._mapper.get_hierarchical_name(self, sep, root, me, reverse, namegetter)

    def get_children(self):
        return self._mapper.get_children(self)

    def get_descendants(self, me=False):
        return self._mapper.get_descendants(self, me)

    @classproperty
    def _mapper(cls):
        return mapper_registry[cls]
