"""
Parser for a Type / Class Tree

After a call to build_type_hirarchy, the types are put into a hirarchy, then
the class parsing starts.

The main features this thing provides is to detect type derived superclasses and
rearanges of them as new classes within the defining one.

        class Foo(Bar, Baz) ->
        class Foo(Bar):
                class Baz(Baz)

where in this not yet built when doing this,
we are in its metacls __new__ yet.

later Foo.Baz can be getting special settings without influencing the other
Baz classes.

This allows for very small definition specs.
"""
# -------------------------------------------------------------- Build the Tree
from wax.base import python, out
hirarchy    =  []
hirarchy_built = False
# non interesting bases, up to the first type:
mixins = {}
classes = {}
cls_counter = 0

pystring = str
if python == 2:
    pystring = basestring
    __pragma__('ifdef', 'server')
    from wax.tree_builder.py2 import cls_with_meta
    __pragma__('endif')
else:
    from wax.tree_builder.py3 import cls_with_meta

class MixinTypeMetaClass(type):
    def __new__(cls, name, bases, attrs):
        ncls = type.__new__(cls, name, bases, attrs)
        #ncls = super(MixinTypeMetaClass, cls).__new__(cls, name, bases, attrs)
        mixins.setdefault(ncls, [])
        return ncls


def out_clses(cs):
    r = ''
    for c in cs:
        r += ', name: ' + c.__name__
    return 'Classes: ' + r[1:]

class MT(object): __metaclass__ = MixinTypeMetaClass
def build_type_hirarchy(root):
    ''' e.g. Project'''
    while 1:
        if hirarchy[0] != root:
            hirarchy.pop(0)
        else:
            break
    out ('have hirarchy', [n.__name__ for n in hirarchy])
    global hirarchy_built
    hirarchy_built = True # close hirachy, following are specific classes

def add_to(*dests, **kw):
    'convenience wrapper for add_cls - from spec'
    dests = _list(dests)
    for parent in dests:
        for k, childs in kw.items():
            for child in _list(childs):
                if not child.type == k:
                    raise Exception("Can't add: %s is no %s" % (child, k))
                add_cls(parent, child)

def add_cls(parent, child):
    """ called from the spec """
    check_allow_add(parent, child)
    out ('    adding %s to %s' % (child, parent))
    new_sub = TypeMetaClass.__new__(TypeMetaClass, child.__name__, (child,),
                        {'_parent': parent})
    #new_sub = type(child.__name__, (child,), {'_parent': parent})
    setattr(parent, child.__name__, new_sub)
    return new_sub



class HirarchyErr(Exception): pass
def check_allow_add(cls, sub):
    if not sub._hirarchy == cls._hirarchy + 1:
        for c in cls._allowed_childs:
            if sub.type == c.type:
                return
        raise HirarchyErr("Can't add %s directly within %s" % (
            sub, cls))


# ----------------------------------------------------------------------------

class TypeMetaClass(type):
    def __new__(cls, name, bases, attrs):
        try:
            attrs = dict(attrs) # 0
        except:
            pass
        attrs['name'] = name
        if not bases:
            bases = (object,)
        b0, orig_bases = bases[0], None
        if b0 in mixins and not hirarchy_built:
            raise Exception("First Base Class can't be a mixin", b0)
        if hirarchy_built:
            out ('adding a class', name, bases)
            if not '_parent' in attrs:
                while not b0 in hirarchy:
                    # a helper class (like class Node(Node, Role.Foo) and inner
                    # Node is
                    # hirarchy type -> act as if we'd defined all:
                    inh = classes[b0]
                    b0, bases = inh[0], inh + bases[1:]
                attrs['type'] = b0.__name__
                attrs['cls'] = name
                out ('adding {0}({1})'.format(name, b0.__name__))
                bases, orig_bases, mxins, attrs = set_bases_as_attrs(name, b0, bases, attrs)
                if name == 'App':
                    out('app bases', len(bases))
        else:
            out ('registring type', name)
            attrs['type'] = name
            attrs.setdefault('_allowed_childs', [])

        global cls_counter
        cls_counter += 1
        attrs['_nr'] = cls_counter
        #ncls = super(TypeMetaClass, cls).__new__(cls, name, bases, attrs)
        ncls = type.__new__(cls, name, bases, attrs)
        if orig_bases:
            out(ncls.__name__, 'orig_bases', out_clses(orig_bases))
        if hirarchy_built:
            if '_parent' in attrs:
                out('returning', ncls.__name__)
                return ncls
        if orig_bases:
            classes[ncls] = orig_bases
            for m in mxins:
                if hasattr(m, 'func_name'):
                    m(ncls)
                else:
                    out ('Adding %s to %s %s' % (
                            name, mixins[m]['orig'].__name__, m.__name__))
                    mixins[m]['members'].append(ncls)
            out('built class', ncls.__name__, out_clses(classes[ncls]))
            return ncls
        if not hirarchy_built and len(hirarchy):
            hirarchy[-1:][0]._allowed_childs.append(ncls)
            ncls._hirarchy = len(hirarchy)
        hirarchy.append(ncls)
        return ncls
    def __repr__(cls):
        p = getattr(cls, '_parent', None)
        if isinstance(p, pystring):
            p = 'prebound(%s)' % p
        elif isinstance(p, type(None)):
            p = 'type'
        else:
            if not getattr(p, '_parent', None):
                p = ''
            else:
                p = ' [%s]' % p
        return '%s.%s.%s%s' % (cls.type, getattr(cls, 'name', ''),
                getattr(cls, '_nr', 0), p)

T = cls_with_meta(TypeMetaClass, {'descr': None})

def has_parent(c):
    return getattr(c, '_parent', None)


def descr(c):
    'up the mro to find any descr or __doc__'
    d = ()
    for b in c.mro()[:-2]:
        dd = getattr(b, 'descr', None)
        if dd and not dd in d:
            d += (dd,)
        else:
            dd = b.__doc__
            if dd and not dd in d:
                d += (dd,)
    return '.'.join(d)

def postconfigure(ncls):
    """ configure all prebound classes """
    global hirarchy_built
    def pre(c, r, hir, cfg):
        #c.descr = descr(c)
        childs = unordered_childs(c, has_parent)
        for cc in childs:
            cc._parent = c
    out('walking tree', ncls.__name__)
    out(ncls.C.__name__)
    walk_tree(ncls, pre, None, has_parent)
    hirarchy_built = 2

oltens = []

def set_bases_as_attrs(name, b0, orig_bases, attrs):
    ''' type hirarchy is built at this point
    Our job now is to pop those bases (and add them as subclasses which
    registered already).
    If registered as mixin then we create new mixins or resolve - see below
    '''

    def add_local(b0, name, sub, parent_attrs, lms=None):
        '''
        '''
        debugger
        try:
            check_allow_add(b0, sub)
        except HirarchyErr:
            if not lms:
                raise Exception('no lms')
            for i in range(len(lms), 0, -1):
                l = lms[i-1]
                try:
                    t = add_cls(l, sub)
                    return t
                except HirarchyErr:
                    lms.pop(i-1)
            raise Exception('add_cls failed')
        bn = sub.__name__
        out ('    adding {0} to {1}'.format(sub.__name__, name))
        #parent_attrs[bn] = t = type(bn, (sub,), {'_parent': name})
        # won't work with mixins, is basically creating a class also for python=0:
        parent_attrs[bn] = t = TypeMetaClass.__new__(TypeMetaClass, bn, (sub,), {'_parent': name})
        parent_attrs.setdefault('_from_mro', []).append(t)
        out('parent_attrs', len(parent_attrs), parent_attrs.keys())
        return t

    mxins = []
    def new_mixin(ncls, b0, orig_mixin):
        if ncls in mixins:
            raise Exception("mixin %s already defined" % ncls)
        mixins[ncls] = {'members_cls': b0, 'members': [], 'orig': orig_mixin}

    mx = mixins.get(b0)
    if mx:
        # class NBI(AXESS) where AXESS is a group
        # -> we set group='AXESS' and b0 to Service
        mxins.append(b0) # this will add this new cls to b0's members,
        # we don't have it yet
        attrs[mx['orig'].__name__] = b0.__name__
        b0 = mx['members_cls']
        attrs['type'] = b0.type
    if name == 'xApp':
        try:
            debugger
        except:
            import pdb; pdb.set_trace()
    new_bases = [b0]
    lms = [] # last added mro based sub
    out('orig_bases', out_clses(orig_bases))
    out('orig_bases1', out_clses(orig_bases[1:]))
    for b in orig_bases[1:]:
        out('base', b.__name__)
        if b in mixins:
            # a mixin type in the bases: class AXESS(Service, Group)
            # -> we'll create a NEW mixin, but with a members_type,
            # e.g. Service
            # if such a members_cls is alredy set, then we resolve
            # e.g. class Foo(Role, AXESS), AXESS is such a mx-> resolve
            mx = mixins[b]
            if 'members_cls' in mx:
                out ('    resolving all members of %s' % b.__name__)
                for m in mx['members']:
                    #lms = [add_local(b0, name, sub=m, parent_attrs=attrs)]
                    lms = [add_local(b0, name, m, attrs)]
            else:
                # create new
                mxins.append(lambda ncls: new_mixin(ncls, b0, b))
        elif b in hirarchy:
            out('you should instantiate your inner classes from hirarchy type classes')
            import pdb; pdb.set_trace()
        elif classes.get(b):
            #lms.append(add_local(b0, name, sub=b, parent_attrs=attrs, lms=lms))
            lms.append(add_local(b0, name, b, attrs, lms))
        else:
            # normal superclass:
            new_bases.append(b)
    return tuple(new_bases), orig_bases, mxins, attrs

#def allow(*what, **on):
def allow(what):
    'allow((this, that), (onthis, onthat))'
    what, on = what[0], what[1]
    for o in on:
        for w in what:
            o._allowed_childs.append(w)

def _list(s):
    if s and isinstance(s, tuple) and isinstance(s[0], (tuple, list)):
        s = s[0]
    return list(s) if isinstance(s, (list, tuple)) else [s]


# ------------------------------------------------------------ Explore the Tree

def props(c):
    return [(k, getattr(c, k)) for k in dir(c) if not k.startswith('_')]


def walk_tree(cur, pre=None, post=None, match=None, res=None, cfg=None, hir=0):
    '''
    Recursing the class tree, starting from a given root node (cur).
    the result is mutated
    pre, post, match = callables
    '''
    if res is None:
        res=[]
    hir += 1
    if pre:
        pre(cur, res, hir, cfg)
    if not cfg or cfg.get('stop_recursion') != cur.type:
        for k in ordered_childs(cur, match):
            walk_tree(k, pre, post, match, res, cfg, hir=hir)
    if post:
        post(cur, res, hir, cfg)
    return res

def unordered_childs(parent, match):
    c = getattr(parent, '_childs', None)
    if c:
        return c
    childs = []
    for k, v in props(parent):
        if match(v):
            childs.append(v)
    parent._childs = childs
    parent._childs_ordered = False
    return childs

def ordered_childs(parent, match):
    childs = unordered_childs(parent, match)
    if getattr(parent, '_childs_ordered', 0):
        return childs
    childs.sort(key=lambda x: x._nr)
    parent._childs_ordered = True
    return childs



