"""Generates a Cython wrappers from description dictionaries.
This module relies heavily on the type system to convert between C/C++, Cython, and
Python types in a seamless way.  While this module does not explicitly rely on the
auto-describer, it sure helps!  The functions in this module are conceptually
easy to understand -- given class descriptions they generate strings of Cython
code -- their implementations do a lot of heavy lifting.

This module is available as an xdress plugin by the name ``xdress.cythongen``. 
Note that while the module does not rely on the autodescriber the plugin does.

:author: Anthony Scopatz <scopatz@gmail.com>

Cython Generation API
=====================
"""
from __future__ import print_function
import os
import sys
import math
from copy import deepcopy
from pprint import pprint

from .utils import indent, indentstr, expand_default_args, isclassdesc, isfuncdesc, \
    isvardesc, guess_language, newoverwrite, sortedbytype
from .plugins import Plugin
from .typesystem import TypeSystem, TypeMatcher, MatchAny

if sys.version_info[0] >= 3:
    basestring = str

MATCH_REF = TypeMatcher((MatchAny, '&'))

AUTOGEN_WARNING = \
"""################################################
#                 WARNING!                     #
# This file has been auto-generated by xdress. #
# Do not modify!!!                             #
#                                              #
#                                              #
#                    Come on, guys. I mean it! #
################################################
"""

def gencpppxd(env, exceptions=True, ts=None):
    """Generates all cpp_*.pxd Cython header files for an environment of modules.

    Parameters
    ----------
    env : dict
        Environment dictonary mapping target module names to module description
        dictionaries.
    exceptions : bool or str, optional
        Cython exception annotation.  Set to True to automatically detect exception
        types, False for when exceptions should not be included, and a str (such as
        '+' or '-1') to apply to everywhere.
    ts : TypeSystem, optional
        A type system instance.

    Returns
    -------
    cpppxds : dict
        Maps environment target names to Cython cpp_*.pxd header files strings.

    """
    ts = ts or TypeSystem()
    cpppxds = {}
    for name, mod in env.items():
        if mod['srcpxd_filename'] is None:
            continue
        cpppxds[name] = modcpppxd(mod, exceptions, ts=ts)
    return cpppxds

def _addotherclsnames(t, classes, name, others, ts):
    if t is None or t == (None, '*'):
        return
    spt = ts.strip_predicates(t)
    if spt in classes:
        others[name].add(spt)
    elif ts.isfunctionpointer(spt):
        for subt in spt[1:]:
            spsubt = ts.strip_predicates(subt)
            if spsubt in classes:
                others[name].add(spsubt)

def cpppxd_sorted_names(mod, ts):
    """Sorts the variable names in a cpp_*.pxd module so that C/C++ 
    declarations happen in the proper order.
    """
    classes = set([name for name, desc in  mod.items() if isclassdesc(desc)])
    clssort = sorted(c for c in classes if isinstance(c, basestring))
    clssort += sorted(c for c in classes if not isinstance(c, basestring))
    othercls = {}
    for name in clssort:
        desc = mod[name]
        othercls[name] = set()
        for aname, atype in desc['attrs'].items():
            _addotherclsnames(atype, classes, name, othercls, ts)
        for mkey, mtype in desc['methods'].items():
            mname, margs = mkey[0], mkey[1:]
            _addotherclsnames(mtype, classes, name, othercls, ts)
            for marg in margs:
                _addotherclsnames(marg[1], classes, name, othercls, ts)
    clssort.sort(key=lambda x: len(othercls[x]))
    names = clssort[:1]
    for name in clssort[1:]:
        if name in names:
            continue
        for i, n in enumerate(names[:]):
            if name in othercls[n]:
                names.insert(i, name)
                break
            if othercls[name] <= set(names[:i+1] + [name]):
                names.insert(i+1, name)
                break
        else:
            names.append(name)
    names += sortedbytype([name for name, desc in  mod.items() if isvardesc(desc)])
    names += sortedbytype([name for name, desc in  mod.items() if isfuncdesc(desc)])
    return names


def modcpppxd(mod, exceptions=True, ts=None):
    """Generates a cpp_*.pxd Cython header file for exposing a C/C++ module to
    other Cython wrappers based off of a dictionary description of the module.

    Parameters
    ----------
    mod : dict
        Module description dictonary.
    exceptions : bool or str, optional
        Cython exception annotation.  Set to True to automatically detect exception
        types, False for when exceptions should not be included, and a str (such as
        '+' or '-1') to apply to everywhere.
    ts : TypeSystem, optional
        A type system instance.

    Returns
    -------
    cpppxd : str
        Cython cpp_*.pxd header file as in-memory string.

    """
    ts = ts or TypeSystem()
    m = {'extra': mod.get('extra', ''),
         "srcpxd_filename": mod.get("srcpxd_filename", "")}
    attrs = []
    cimport_tups = set()
    classnames = _classnames_in_mod(mod, ts)
    with ts.local_classes(classnames, frozenset(['c'])):
        for name in cpppxd_sorted_names(mod, ts):
            desc = mod[name]
            if isvardesc(desc):
                ci_tup, attr_str = varcpppxd(desc, exceptions, ts)
            elif isfuncdesc(desc):
                ci_tup, attr_str = funccpppxd(desc, exceptions, ts)
            elif isclassdesc(desc):
                ci_tup, attr_str = classcpppxd(desc, exceptions, ts)
            else:
                continue
            cimport_tups |= ci_tup
            attrs.append(attr_str)
    if mod.get('language', None) == 'c':
        cimport_tups.discard((ts.stlcontainers,))
    m['cimports'] = "\n".join(sorted(ts.cython_cimport_lines(cimport_tups)))
    m['attrs_block'] = "\n".join(attrs)
    t = '\n\n'.join([AUTOGEN_WARNING, '{cimports}', '{attrs_block}', '{extra}'])
    cpppxd = t.format(**m)
    return cpppxd

_cpppxd_var_template = \
"""# function signatures
cdef extern from "{header_filename}" {namespace}:

{variables_block}

{extra}
"""

def varcpppxd(desc, exceptions=True, ts=None):
    """Generates a cpp_*.pxd Cython header snippet for exposing a C/C++ variable
    to other Cython wrappers based off of a dictionary description.

    Parameters
    ----------
    desc : dict
        Function description dictonary.
    exceptions : bool or str, optional
        Cython exception annotation.  Set to True to automatically detect exception
        types, False for when exceptions should not be included, and a str (such as
        '+' or '-1') to apply to everywhere.
    ts : TypeSystem, optional
        A type system instance.

    Returns
    -------
    cimport_tups : set of tuples
        Set of Cython cimport tuples for cpp_*.pxd header file.
    cpppxd : str
        Cython cpp_*.pxd header file as in-memory string.

    """
    ts = ts or TypeSystem()
    d = {}
    t = ts.canon(desc['type'])
    copy_from_desc = ['name', 'header_filename']
    for key in copy_from_desc:
        d[key] = desc[key]
    d['namespace'] = _format_ns(desc)

    inc = set(['c'])
    cimport_tups = set()
    ts.cython_cimport_tuples(t, cimport_tups, inc)

    vlines = []
    if ts.isenum(t):
        vlines.append("cdef enum {0}:".format(d['name']))
        enames = [name for name, val in t[1][2][2]]
        vlines += indent(enames, 4, join=False)
    else:
        ct = ts.cython_ctype(t)
        vlines.append("{0} {1}".format(ct, d['name']))
    d['variables_block'] = indent(vlines, 4)
    if 0 == len(d['variables_block'].strip()):
        return set(), ''

    d['extra'] = desc.get('extra', {}).get('cpppxd', '')
    cpppxd = _cpppxd_var_template.format(**d)
    if 'srcpxd_filename' not in desc:
        desc['srcpxd_filename'] = 'cpp_{0}.pxd'.format(d['name'].lower())
    return cimport_tups, cpppxd

_cpppxd_func_template = \
"""# function signatures
cdef extern from "{header_filename}" {namespace}:

{functions_block}

{extra}
"""

def funccpppxd(desc, exceptions=True, ts=None):
    """Generates a cpp_*.pxd Cython header snippet for exposing a C/C++ function
    to other Cython wrappers based off of a dictionary description.

    Parameters
    ----------
    desc : dict
        Function description dictonary.
    exceptions : bool or str, optional
        Cython exception annotation.  Set to True to automatically detect exception
        types, False for when exceptions should not be included, and a str (such as
        '+' or '-1') to apply to everywhere.
    ts : TypeSystem, optional
        A type system instance.

    Returns
    -------
    cimport_tups : set of tuples
        Set of Cython cimport tuples for cpp_*.pxd header file.
    cpppxd : str
        Cython cpp_*.pxd header file as in-memory string.

    """
    ts = ts or TypeSystem()
    d = {}
    copy_from_desc = ['name', 'header_filename']
    for key in copy_from_desc:
        d[key] = desc[key]
    d['namespace'] = _format_ns(desc)
    inc = set(['c'])
    cimport_tups = set()

    flines = []
    funcitems = sorted(expand_default_args(desc['signatures'].items()))
    for fkey, frtn in funcitems:
        fname, fargs = fkey[0], fkey[1:]
        fbasename = fname if isinstance(fname, basestring) else fname[0]
        cppname = ts.cpp_funcname(fname)
        cyname = ts.cython_funcname(fname)
        if fbasename.startswith('_'):
            continue  # private
        if any([a[1] is None or a[1][0] is None for a in fargs + (frtn,)]):
            continue
        argfill = ", ".join([ts.cython_ctype(a[1]) for a in fargs])
        for a in fargs:
            ts.cython_cimport_tuples(a[1], cimport_tups, inc)
        estr = _exception_str(exceptions, desc['source_filename'], frtn, ts)
        if fname == cppname == cyname:
            line = "{0}({1}) {2}".format(fname, argfill, estr)
        else:
            line = '{0} "{1}" ({2}) {3}'.format(cyname, cppname, argfill, estr)
        rtype = ts.cython_ctype(frtn)
        ts.cython_cimport_tuples(frtn, cimport_tups, inc)
        line = rtype + " " + line
        if line not in flines:
            flines.append(line)
    d['functions_block'] = indent(flines, 4)
    if 0 == len(d['functions_block'].strip()):
        return set(), ''

    d['extra'] = desc.get('extra', {}).get('cpppxd', '')
    cpppxd = _cpppxd_func_template.format(**d)
    if 'srcpxd_filename' not in desc:
        desc['srcpxd_filename'] = 'cpp_{0}.pxd'.format(d['name'].lower())
    return cimport_tups, cpppxd

_cpppxd_class_template = \
"""cdef extern from "{header_filename}" {namespace}:

    cdef {construct_kind} {name}{parents}{alias}:
        # constructors
{constructors_block}

        # attributes
{attrs_block}

        # methods
{methods_block}
        pass

{extra}
"""

def classcpppxd(desc, exceptions=True, ts=None):
    """Generates a cpp_*.pxd Cython header snippet for exposing a C/C++ class or
    struct to other Cython wrappers based off of a dictionary description of the
    class or struct.

    Parameters
    ----------
    desc : dict
        Class description dictonary.
    exceptions : bool or str, optional
        Cython exception annotation.  Set to True to automatically detect exception
        types, False for when exceptions should not be included, and a str (such as
        '+' or '-1') to apply to everywhere.
    ts : TypeSystem, optional
        A type system instance.

    Returns
    -------
    cimport_tups : set of tuples
        Set of Cython cimport tuples for cpp_*.pxd header file.
    cpppxd : str
        Cython cpp_*.pxd header file as in-memory string.

    """
    ts = ts or TypeSystem()
    pars = ', '.join([ts.cython_ctype(p) for p in desc['parents'] or ()])
    d = {'parents': pars if 0 == len(pars) else '('+pars+')',
         'header_filename': desc['header_filename'],}
    d['namespace'] = _format_ns(desc)
    name = desc['name']
    if isinstance(desc['type'], basestring):
        d['name'] = ts.cython_ctype(name)
        d['alias'] = ''
    else:
        d['name'] = ts.cython_classname(name)[1]
        d['alias'] = ' ' + _format_alias(desc, ts)
    #construct_kinds = {'struct': 'struct', 'class': 'cppclass'}
    #d['construct_kind'] = construct_kinds[desc.get('construct', 'class')]
    lang = guess_language(desc['source_filename'])
    construct_kinds = {'c': 'struct', 'c++': 'cppclass'}
    d['construct_kind'] = construct_kinds[lang]
    inc = set(['c'])

    cimport_tups = set()
    for parent in desc['parents'] or ():
        ts.cython_cimport_tuples(parent, cimport_tups, inc)

    alines = []
    attritems = sorted(desc['attrs'].items())
    for aname, atype in attritems:
        if aname.startswith('_'):
            continue
        actype = ts.cython_ctype(atype)
        if '{type_name}' in actype:
            aline = actype.format(type_name=aname)
        else:
            aline = "{0} {1}".format(actype, aname)
        alines.append(aline)
        ts.cython_cimport_tuples(atype, cimport_tups, inc)
    d['attrs_block'] = indent(alines, 8)

    mlines = []
    clines = []
    dargs = expand_default_args(desc['methods'].items())
    methitems = sorted(x for x in dargs if isinstance(x[0][0], basestring))
    methitems += sorted(x for x in dargs if not isinstance(x[0][0], basestring))
    for mkey, mrtn in methitems:
        mname, margs = mkey[0], mkey[1:]
        mbasename = mname if isinstance(mname, basestring) else mname[0]
        mcppname = ts.cpp_funcname(mname)
        mcyname = ts.cython_funcname(mname)
        if mbasename.startswith('_') or mbasename.startswith('~'):
            continue  # private or destructor
        argfill = ", ".join([ts.cython_ctype(a[1]) for a in margs])
        for a in margs:
            ts.cython_cimport_tuples(a[1], cimport_tups, inc)
        estr = _exception_str(exceptions, desc['source_filename'], mrtn, ts)
        if mname == mcppname == mcyname:
            line = "{0}({1}) {2}".format(mname, argfill, estr)
        else:
            line = '{0} "{1}" ({2}) {3}'.format(mcyname, mcppname, argfill, estr)
        if mrtn is None:
            # this must be a constructor
            line = "{0}({1}) {2}".format(d['name'], argfill, estr)
            if line not in clines:
                clines.append(line)
        else:
            # this is a normal method
            if MATCH_REF.matches(mrtn):
                mrtn = mrtn[0]
            rtype = ts.cython_ctype(mrtn)
            ts.cython_cimport_tuples(mrtn, cimport_tups, inc)
            line = rtype + " " + line
            if line not in mlines:
                mlines.append(line)
    d['methods_block'] = indent(mlines, 8)
    d['constructors_block'] = indent(clines, 8)

    d['extra'] = desc.get('extra', {}).get('cpppxd', '')
    cpppxd = _cpppxd_class_template.format(**d)
    if 'srcpxd_filename' not in desc:
        desc['srcpxd_filename'] = 'cpp_{0}.pxd'.format(d['name'].lower())
    return cimport_tups, cpppxd


def genpxd(env, classes=(), ts=None, max_callbacks=8):
    """Generates all pxd Cython header files for an environment of modules.

    Parameters
    ----------
    env : dict
        Environment dictonary mapping target module names to module description
        dictionaries.
    classes : sequence, optional
        Listing of all class names that are handled by cythongen.  This may be
        the same dictionary as in genpyx()
    ts : TypeSystem, optional
        A type system instance.
    max_callbacks : int, optional
        The default maximum number of callbacks for function pointers.

    Returns
    -------
    pxds : str
        Maps environment target names to Cython pxd header files strings.

    """
    ts = ts or TypeSystem()
    pxds = {}
    for name, mod in env.items():
        if mod['pxd_filename'] is None:
            continue
        pxds[name] = modpxd(mod, classes, ts=ts, max_callbacks=max_callbacks)
    return pxds


def modpxd(mod, classes=(), ts=None, max_callbacks=8):
    """Generates a pxd Cython header file for exposing C/C++ data to
    other Cython wrappers based off of a dictionary description.

    Parameters
    ----------
    mod : dict
        Module description dictonary.
    classes : sequence, optional
        Listing of all class names that are handled by cythongen.  This may be
        the same dictionary as in modpyx().
    ts : TypeSystem, optional
        A type system instance.
    max_callbacks : int, optional
        The default maximum number of callbacks for function pointers.

    Returns
    -------
    pxd : str
        Cython .pxd header file as in-memory string.

    """
    ts = ts or TypeSystem()
    m = {'extra': mod.get('extra', ''),
         "pxd_filename": mod.get("pxd_filename", "")}
    attrs = []
    cimport_tups = set()
    classnames = _classnames_in_mod(mod, ts)
    with ts.local_classes(classnames):
        for name, desc in mod.items():
            if isclassdesc(desc):
                ci_tup, attr_str = classpxd(desc, classes, ts=ts, 
                                            max_callbacks=max_callbacks)
            # no need to wrap functions again
            else:
                continue
            cimport_tups |= ci_tup
            attrs.append(attr_str)
    cimport_tups.discard((mod["name"],))
    if mod.get('language', None) == 'c':
        cimport_tups.discard((ts.stlcontainers,))
    m['cimports'] = "\n".join(sorted(ts.cython_cimport_lines(cimport_tups)))
    m['attrs_block'] = "\n".join(attrs)
    t = '\n\n'.join([AUTOGEN_WARNING, '{cimports}', '{attrs_block}', '{extra}'])
    pxd = t.format(**m)
    return pxd


_pxd_class_template = \
"""{function_pointer_block}

cdef class {name}{parents}:
{body}
    pass

{extra}
"""


def classpxd(desc, classes=(), ts=None, max_callbacks=8):
    """Generates a ``*pxd`` Cython header snippet for exposing a C/C++ class to
    other Cython wrappers based off of a dictionary description.

    Parameters
    ----------
    desc : dict
        Class description dictonary.
    classes : sequence, optional
        Listing of all class names that are handled by cythongen.  This may be
        the same dictionary as in modpyx().
    ts : TypeSystem, optional
        A type system instance.
    max_callbacks : int, optional
        The default maximum number of callbacks for function pointers.

    Returns
    -------
    cimport_tups : set of tuples
        Set of Cython cimport tuples for .pxd header file.
    pxd : str
        Cython ``*.pxd`` header snippet for class.

    """
    ts = ts or TypeSystem()
    if 'pxd_filename' not in desc:
        desc['pxd_filename'] = '{0}.pxd'.format(desc['name'].lower())
    pars = ', '.join([ts.cython_cytype(p) for p in desc['parents'] or ()])
    d = {'parents': pars if 0 == len(pars) else '('+pars+')'}
    name = desc['name']
    d['name'] = name if isinstance(name, basestring) else ts.cython_classname(name)[1]
    max_callbacks = desc.get('extra', {}).get('max_callbacks', max_callbacks)
    mczeropad = int(math.log10(max_callbacks)) + 1

    cimport_tups = set()
    for parent in desc['parents'] or ():
        ts.cython_cimport_tuples(parent, cimport_tups, set(['cy']))

    from_cpppxd = desc['srcpxd_filename'].rsplit('.', 1)[0]
    d['name_type'] = ts.cython_ctype(desc['name'])
    ts.cython_cimport_tuples(desc['name'], cimport_tups, set(['c']))

    parentless_body = ['cdef void * _inst', 'cdef public bint _free_inst']
    body = parentless_body if desc['parents'] is None else []
    attritems = sorted(desc['attrs'].items())
    fplines = []
    for aname, atype in attritems:
        if aname.startswith('_'):
            continue  # skip private
        _, _, cachename, iscached = ts.cython_c2py(aname, atype, cache_prefix=None)
        if iscached:
            ts.cython_cimport_tuples(atype, cimport_tups)
            if _isclassptr(atype, classes):
                atype_nopred = ts.strip_predicates(atype)
                cyt = ts.cython_cytype(atype_nopred)
            elif _isclassdblptr(atype, classes):
                cyt = 'list'
            else:
                cyt = ts.cython_cytype(atype)
            decl = "cdef public {0} {1}".format(cyt, cachename)
            body.append(decl)
        if ts.isfunctionpointer(atype):
            apyname, acname = _mangle_function_pointer_name(aname, desc['name'])
            acdecl = "cdef public " + ts.cython_ctype(('function',)+ atype[1:])
            for i in range(max_callbacks):
                suffix = "{0:0{1}}".format(i, mczeropad)
                apyname_i, acname_i = apyname + suffix, acname + suffix
                fplines.append("cdef public object " + apyname_i)
                fplines.append(acdecl.format(type_name=acname_i))
            body.append("cdef unsigned int _{0}_vtab_i".format(aname))
            fplines.append("cdef unsigned int _current_{0}_vtab_i".format(apyname))

    if len(fplines) > 0:
        fplines.append("cdef unsigned int _MAX_CALLBACKS_" + desc['name'])

    d['body'] = indent(body or ['pass'])
    d['function_pointer_block'] = '\n'.join(fplines)
    d['extra'] = desc.get('extra', {}).get('pxd', '')
    pxd = _pxd_class_template.format(**d)
    return cimport_tups, pxd


def genpyx(env, classes=None, ts=None, max_callbacks=8):
    """Generates all pyx Cython implementation files for an environment of modules.

    Parameters
    ----------
    env : dict
        Environment dictonary mapping target module names to module description
        dictionaries.
    classes : dict, optional
        Dictionary which maps all class names that are required to
        their own descriptions.  This is required for resolving class heirarchy
        dependencies. If None, this will be computed here.
    ts : TypeSystem, optional
        A type system instance.
    max_callbacks : int, optional
        The default maximum number of callbacks for function pointers.

    Returns
    -------
    pyxs : str
        Maps environment target names to Cython pxd header files strings.

    """
    ts = ts or TypeSystem()
    if classes is None:
        # get flat namespace of class descriptions
        classes = {}
        for envname, mod in env.items():
            for modname, desc in mod.items():
                if isclassdesc(desc):
                    classes[desc['name']] = desc
    # gen files
    pyxs = {}
    for name, mod in env.items():
        if mod['pyx_filename'] is None:
            continue
        pyxs[name] = modpyx(mod, classes=classes, ts=ts, max_callbacks=max_callbacks)
    return pyxs


_pyx_mod_template = AUTOGEN_WARNING + \
'''"""{docstring}
"""
{cimports}

{imports}

{attrs_block}

{extra}
'''

def modpyx(mod, classes=None, ts=None, max_callbacks=8):
    """Generates a pyx Cython implementation file for exposing C/C++ data to
    other Cython wrappers based off of a dictionary description.

    Parameters
    ----------
    mod : dict
        Module description dictonary.
    classes : dict, optional
        Dictionary which maps all class names that are required to
        their own descriptions.  This is required for resolving class heirarchy
        dependencies.
    ts : TypeSystem, optional
        A type system instance.
    max_callbacks : int, optional
        The default maximum number of callbacks for function pointers.

    Returns
    -------
    pyx : str
        Cython pyx header file as in-memory string.

    """
    ts = ts or TypeSystem()
    m = {'extra': mod.get('extra', ''),
         'docstring': mod.get('docstring', "no docstring, please file a bug report!"),
         "pyx_filename": mod.get("pyx_filename", "")}
    attrs = []
    import_tups = set()
    cimport_tups = set()
    classnames = _classnames_in_mod(mod, ts)
    with ts.local_classes(classnames):
        for name, desc in mod.items():
            if isvardesc(desc):
                i_tup, ci_tup, attr_str = varpyx(desc, ts=ts)
            elif isfuncdesc(desc):
                i_tup, ci_tup, attr_str = funcpyx(desc, ts=ts)
            elif isclassdesc(desc):
                i_tup, ci_tup, attr_str = classpyx(desc, classes=classes, ts=ts, 
                                                   max_callbacks=max_callbacks)
            else:
                continue
            import_tups |= i_tup
            cimport_tups |= ci_tup
            attrs.append(attr_str)
        # Add dispatcher for template functions
        template_funcs = _template_funcnames_in_mod(mod)
        template_dispatcher = _gen_template_func_dispatcher(template_funcs, ts)
        attrs.append(template_dispatcher)
        # Add dispatcher for template classes
        template_classes = _template_classnames_in_mod(mod)
        template_dispatcher = _gen_template_class_dispatcher(template_classes, ts)
        attrs.append(template_dispatcher)
    import_tups.discard((mod["name"],))
    #cimport_tups.discard((mod["name"],))  # remain commented for decls
    if mod.get('language', None) == 'c':
        import_tups.discard((ts.stlcontainers,))
        cimport_tups.discard((ts.stlcontainers,))
    m['imports'] = "\n".join(sorted(ts.cython_import_lines(import_tups)))
    m['cimports'] = "\n".join(sorted(ts.cython_cimport_lines(cimport_tups)))
    if 'numpy' in m['cimports']:
        m['imports'] += "\n\nnp.import_array()"
    m['attrs_block'] = "\n".join(attrs)
    t = '\n\n'.join([AUTOGEN_WARNING, '{cimports}', '{attrs_block}', '{extra}'])
    pyx = _pyx_mod_template.format(**m)
    return pyx

def _gen_template_func_dispatcher(templates, ts):
    """Generates a dictionary-based dispacher for template functions.
    """
    if 0 == len(templates):
        return ""
    templates = sorted(templates)
    disp = ['', "#", "# Function Dispatchers", "#",]
    alreadyinitd = set()
    for t in templates:
        initline = "{0} = {{}}".format(t[0])
        if initline not in alreadyinitd:
            disp.append("")
            disp.append("# {0} dispatcher".format(t[0]))
            disp.append(initline)
            alreadyinitd.add(initline)
        args = t[1:]
        pytype = ts.cython_funcname(t)
        if 0 == len(args):
            raise ValueError("type {0!r} not a template".format(t))
        elif 1 == len(args):
            disp.append("{0}[{1!r}] = {2}".format(t[0], t[1], pytype))
            disp.append("{0}[{1}] = {2}".format(t[0], ts.cython_pytype(t[1]), pytype))
        else:
            rs = [repr(_) for _ in t[1:]]
            pyts = [ts.cython_pytype(x) for x in t[1:]]
            disp.append("{0}[{1}] = {2}".format(t[0], ", ".join(rs), pytype))
            disp.append("{0}[{1}] = {2}".format(t[0], ", ".join(pyts), pytype))
    return "\n".join(disp)

def _gen_template_class_dispatcher(templates, ts):
    """Generates a dictionary-based dispacher for template classes.
    """
    if 0 == len(templates):
        return ""
    templates = sorted(templates)
    disp = ['', "#", "# Class Dispatchers", "#",]
    alreadyinitd = set()
    for t in templates:
        initline = "{0} = {{}}".format(t[0])
        if initline not in alreadyinitd:
            disp.append("")
            disp.append("# {0} Dispatcher".format(t[0]))
            disp.append(initline)
            alreadyinitd.add(initline)
        args = t[1:-1]
        pytype = ts.cython_pytype(t)
        if 0 == len(args):
            raise ValueError("type {0!r} not a template".format(t))
        elif 1 == len(args):
            disp.append("{0}[{1!r}] = {2}".format(t[0], t[1], pytype))
            disp.append("{0}[{1}] = {2}".format(t[0], ts.cython_pytype(t[1]), pytype))
        else:
            rs = [repr(_) for _ in t[1:-1]]
            pyts = [ts.cython_pytype(x) for x in t[1:-1]]
            disp.append("{0}[{1}] = {2}".format(t[0], ", ".join(rs), pytype))
            disp.append("{0}[{1}] = {2}".format(t[0], ", ".join(pyts), pytype))
    return "\n".join(disp)

def _gen_property_get(name, t, ts, cached_names=None, inst_name="self._inst",
                      classes=()):
    """This generates a Cython property getter for a variable of a given
    name and type."""
    lines = ['def __get__(self):']
    decl, body, rtn, iscached = ts.cython_c2py(name, t, inst_name=inst_name)
    if decl is not None:
        if _isclassptr(t, classes):
            decl, _, _, _ = ts.cython_c2py(name, t[0], inst_name=inst_name)
        lines += indent(decl, join=False)
    if body is not None:
        lines += indent(body, join=False)
    if iscached and cached_names is not None:
        cached_names.append(rtn)
    lines += indent("return {0}".format(rtn), join=False)
    return lines

def _gen_property_set(name, t, ts, inst_name="self._inst", cached_name=None, 
                      classes=()):
    """This generates a Cython property setter for a variable of a given
    name and type."""
    lines = ['def __set__(self, value):']
    decl, body, rtn = ts.cython_py2c('value', t)
    if decl is not None:
        lines += indent(decl, join=False)
    if body is not None:
        lines += indent(body, join=False)
    lines += indent("{0}.{1} = {2}".format(inst_name, name, rtn), join=False)
    if cached_name is not None:
        lines += indent("{0} = None".format(cached_name), join=False)
    return lines

def _gen_property(name, t, ts, doc=None, cached_names=None, inst_name="self._inst", 
                  classes=()):
    """This generates a Cython property for a variable of a given name and type."""
    lines  = ['property {0}:'.format(name)]
    lines += [] if doc is None else indent('\"\"\"{0}\"\"\"'.format(doc), join=False)
    oldcnlen = 0 if cached_names is None else len(cached_names)
    lines += indent(_gen_property_get(name, t, ts, cached_names=cached_names,
                    inst_name=inst_name, classes=classes), join=False)
    lines += ['']
    newcnlen = 0 if cached_names is None else len(cached_names)
    cached_name = cached_names[-1] if newcnlen == 1 + oldcnlen else None
    lines += indent(_gen_property_set(name, t, ts, inst_name=inst_name,
                    cached_name=cached_name, classes=classes), join=False)
    lines += ['', ""]
    return lines

def _gen_function_pointer_property(name, t, ts, doc=None, cached_names=None,
        inst_name="self._inst", classname='', max_callbacks=8):
    """This generates a Cython property for a function pointer variable."""
    lines  = ['property {0}:'.format(name)]

    # get section
    lines += [] if doc is None else indent('\"\"\"{0}\"\"\"'.format(doc), join=False)
    oldcnlen = 0 if cached_names is None else len(cached_names)
    lines += indent(_gen_property_get(name, t, ts, cached_names=cached_names,
                                      inst_name=inst_name), join=False)

    # set section
    mczeropad = int(math.log10(max_callbacks)) + 1
    lines += [""]
    newcnlen = 0 if cached_names is None else len(cached_names)
    cached_name = cached_names[-1] if newcnlen == 1 + oldcnlen else None
    setlines = indent(_gen_property_set(name, ('void', '*'), ts, inst_name=inst_name,
                                        cached_name=cached_name), join=False)

    lines += setlines[:1]
    lines += indent(indent(['if not callable(value):',
                ('    raise ValueError("{0!r} is not callable but ' + classname +
                 '.' + name + ' is a function pointer!".format(value))')],
                join=False), join=False)
    #lines += setlines[1:]
    pyname, cname = _mangle_function_pointer_name(name, classname)
    pynames = [pyname + "{0:0{1}}".format(i, mczeropad) for i in \
                                                    range(max_callbacks)]
    cnames = [cname + "{0:0{1}}".format(i, mczeropad) for i in \
                                                    range(max_callbacks)]
    if max_callbacks == 1:
        suffix = '0'
        extraset = ('global {pyname}\n'
                    '{cached_name} = value\n'
                    '{pyname} = value\n'
                    '{inst_name}.{name} = {cname}\n'
                    ).format(name=name, pyname=pyname + suffix, cname=cname + suffix,
                             cached_name=cached_name, inst_name=inst_name)
    elif max_callbacks > 1:
        extraset = ['cdef unsigned int vtab_i',
                    '{cached_name} = value'.format(cached_name=cached_name),
                    "global " + ', '.join(pynames) + \
                                ', _current_{0}_vtab_i'.format(pyname),]
        selectlines = []
        for i, pyname_i in enumerate(pynames):
            selectlines.append("elif {0} is None:".format(pyname_i))
            selectlines.append("    vtab_i = {0}".format(i))
        selectlines[0] = selectlines[0][2:]
        extraset += selectlines
        extraset += ['else:',
                     ('    warnings.warn("Ran out of available callbacks for '
                      '{0}.{1}, overriding existing callback.", RuntimeWarning)'
                      ).format(classname, name),
                     '    vtab_i = _current_{0}_vtab_i'.format(pyname),
                     '    _current_{0}_vtab_i = (_current_{0}_vtab_i+1)%{1}'.format(
                                                            pyname, max_callbacks),
                     'self._{0}_vtab_i = vtab_i'.format(name),]
        setvallines = []
        for i, (pyname_i, cname_i) in enumerate(zip(pynames, cnames)):
            setvallines.append("elif vtab_i == {0}:".format(i))
            setvallines.append("    {pyname} = value".format(pyname=pyname_i))
            setvallines.append("    {inst_name}.{name} = {cname}".format(
                                inst_name=inst_name, name=name, cname=cname_i))
        setvallines[0] = setvallines[0][2:]
        extraset += setvallines
    else:
        msg = "The max number of callbacks for {0} must be >=1, got {1}."
        raise RuntimeError(msg.format(classname, max_callbacks))
    lines += indent(indent(extraset, join=False), join=False)
    lines.append('')
    lines += ["def _deref_{0}_callback(self):".format(name),
              '    "Warning: this can have dangerous side effects!"',
              '    cdef unsigned int vtab_i',
              '    {cached_name} = None'.format(cached_name=cached_name),
              "    if self._{0}_vtab_i < {1}:".format(name, max_callbacks+1),
              '        vtab_i = self._{0}_vtab_i'.format(name),
              "        self._{0}_vtab_i = {1}".format(name, max_callbacks+1), ]
    dereflines = []
    for i, pyname_i in enumerate(pynames):
        dereflines.append("elif vtab_i == {0}:".format(i))
        dereflines.append("    global {0}".format(pyname_i))
        dereflines.append("    {0} = None".format(pyname_i))
    dereflines[0] = dereflines[0][2:]
    lines += indent(indent(dereflines, join=False), join=False)
    lines += ['', ""]
    return lines

def _gen_function_pointer_wrapper(name, t, ts, classname='', max_callbacks=8):
    """This generates a Cython wrapper for a function pointer variable."""
    pyname, cname = _mangle_function_pointer_name(name, classname)
    mczeropad = int(math.log10(max_callbacks)) + 1
    lines = ["#\n# Function pointer helpers for {1}.{0}\n#".format(name, classname),
             "_current_{0}_vtab_i = 0".format(pyname), ""]
    for i in range(max_callbacks):
        suffix = "{0:0{1}}".format(i, mczeropad)
        pyname_i, cname_i = pyname + suffix, cname + suffix
        decl, body, rtn = ts.cython_py2c(pyname_i, t, proxy_name=cname_i)
        lines += [pyname_i + " = None", '']
        lines += rtn.splitlines()
        lines.append('')
    lines += ['', ""]
    return lines

def _gen_function(name, name_mangled, args, rtn, ts, doc=None, inst_name="self._inst",
                  is_method=False):
    if is_method:
        argfill = ", ".join(['self'] + [a[0] for a in args if 2 == len(a)] + \
                        ["{0}={1}".format(a[0], a[2]) for a in args if 3 == len(a)])
    else:
        argfill = ", ".join([a[0] for a in args if 2 == len(a)] + \
                        ["{0}={1}".format(a[0], a[2]) for a in args if 3 == len(a)])
    lines  = ['def {0}({1}):'.format(name_mangled, argfill)]
    lines += [] if doc is None else indent('\"\"\"{0}\"\"\"'.format(doc), join=False)
    decls = []
    argbodies = []
    argrtns = {}
    for a in args:
        adecl, abody, artn = ts.cython_py2c(a[0], a[1])
        if adecl is not None:
            decls += indent(adecl, join=False)
        if abody is not None:
            argbodies += indent(abody, join=False)
        argrtns[a[0]] = artn
    rtype_orig = ts.cython_ctype(rtn)
    rtype = rtype_orig.replace('const ', "").replace(' &', '')
    hasrtn = rtype not in set(['None', None, 'NULL', 'void'])
    argvals = ', '.join([argrtns[a[0]] for a in args])
    fcall = '{0}.{1}({2})'.format(inst_name, name, argvals)
    if hasrtn:
        fcdecl, fcbody, fcrtn, fccached = ts.cython_c2py('rtnval', rtn, cached=False)
        decls += indent("cdef {0} {1}".format(rtype, 'rtnval'), join=False)
        if 'const ' in rtype_orig:
            func_call = indent('rtnval = <{0}> {1}'.format(rtype, fcall), join=False)
        else:
            func_call = indent('rtnval = {0}'.format(fcall), join=False)
        if fcdecl is not None:
            decls += indent(fcdecl, join=False)
        if fcbody is not None:
            func_call += indent(fcbody, join=False)
        func_rtn = indent("return {0}".format(fcrtn), join=False)
    else:
        func_call = indent(fcall, join=False)
        func_rtn = []
    lines += decls
    lines += argbodies
    lines += func_call
    lines += func_rtn
    lines += ['', ""]
    return lines

def _gen_default_constructor(desc, attrs, ts, doc=None, srcpxd_filename=None):
    args = ['self'] + [a + "=None" for a, _ in attrs] + ['*args', '**kwargs']
    argfill = ", ".join(args)
    lines  = ['def __init__({0}):'.format(argfill)]
    lines += [] if doc is None else indent('\"\"\"{0}\"\"\"'.format(doc), join=False)
    ct = ts.cython_ctype(desc['type'])
    if desc['construct'] == 'class':
        fcall = 'self._inst = new {0}()'.format(ct)
    elif desc['construct'] == 'struct':
        fcall = ('self._inst = malloc(sizeof({0}))\n'
                 '(<{0} *> self._inst)[0] = {0}()').format(ct)
    else:
        raise ValueError('construct must be either "class" or "struct".')
    lines.extend(indent(fcall, join=False))
    for a, _ in attrs:
        lines.append(indent("if {0} is not None:".format(a)))
        lines.append(indent("self.{0} = {0}".format(a), 8))
    lines += ['', ""]
    return lines

def _gen_constructor(name, name_mangled, classname, args, ts, doc=None,
                     srcpxd_filename=None, inst_name="self._inst", construct="class"):
    argfill = ", ".join(['self'] + [a[0] for a in args if 2 == len(a)] + \
                        ["{0}={1}".format(a[0], a[2]) for a in args if 3 == len(a)])
    lines  = ['def {0}({1}):'.format(name_mangled, argfill)]
    lines += [] if doc is None else indent('\"\"\"{0}\"\"\"'.format(doc), join=False)
    decls = []
    argbodies = []
    argrtns = {}
    for a in args:
        adecl, abody, artn = ts.cython_py2c(a[0], a[1])
        if adecl is not None:
            decls += indent(adecl, join=False)
        if abody is not None:
            argbodies += indent(abody, join=False)
        argrtns[a[0]] = artn
    argvals = ', '.join([argrtns[a[0]] for a in args])
    classname = classname if srcpxd_filename is None else \
                    "{0}.{1}".format(srcpxd_filename.rsplit('.', 1)[0], classname)
    if construct == 'class':
        fcall = 'self._inst = new {0}({1})'.format(classname, argvals)
    elif construct == 'struct':
        fcall = ('self._inst = malloc(sizeof({0}))\n'
                 '(<{0} *> self._inst)[0] = {0}({1})').format(classname, argvals)
    else:
        raise ValueError('construct must be either "class" or "struct".')
    func_call = indent(fcall, join=False)
    lines += decls
    lines += argbodies
    lines += func_call
    lines += ['', ""]
    return lines

def _gen_dispatcher(name, name_mangled, ts, doc=None, hasrtn=True, is_method=True):
    argfill = ", ".join(['self', '*args', '**kwargs'])
    if is_method is True:
        # string to format for arg checking
        arg_chk_str = "if types <= self.{0}_argtypes:"

        # string to format for dispatching and returning
        dispatch_str_ret = "return self.{0}(*args, **kwargs)"
        dispatch_str_no_ret = "self.{0}(*args, **kwargs)"

        # Make self a method argument or not
        argfill = ", ".join(['self', '*args', '**kwargs'])
    else:
        arg_chk_str = "if types <= {0}_argtypes:"
        dispatch_str_ret = "return {0}(*args, **kwargs)"
        dispatch_str_no_ret = "{0}(*args, **kwargs)"
        argfill = ", ".join(['*args', '**kwargs'])
    lines  = ['def {0}({1}):'.format(name, argfill)]
    lines += [] if doc is None else indent('\"\"\"{0}\"\"\"'.format(doc), join=False)
    types = ["types = set([(i, type(a)) for i, a in enumerate(args)])",
             "types.update([(k, type(v)) for k, v in kwargs.items()])",]
    lines += indent(types, join=False)
    refinenum = lambda x: (sum([int(ts.isrefinement(a[1])) for a in x[0][1:]]), 
                           len(x[0]), x[1])
    mangitems = sorted(name_mangled.items(), key=refinenum)
    mtypeslines = []
    lines += indent("# vtable-like dispatch for exactly matching types", join=False)
    for key, mangled_name in mangitems:
        cargs = key[1:]
        arang = range(len(cargs))
        anames = [ca[0] for ca in cargs]
        pytypes = [ts.cython_pytype(ca[1]) for ca in cargs]
        mtypes = ", ".join(
            ["({0}, {1})".format(i, pyt) for i, pyt in zip(arang, pytypes)] + \
            ['("{0}", {1})'.format(n, pyt) for n, pyt in zip(anames, pytypes)])
        mtups = '(' + mtypes + ')' if 0 < len(mtypes) else mtypes
        mtypeslines.append(mangled_name + "_argtypes = frozenset(" + mtups + ")")
        cond = [arg_chk_str.format(mangled_name),]
        if hasrtn:
            rline = dispatch_str_ret.format(mangled_name)
        else:
            rline = [dispatch_str_no_ret.format(mangled_name), "return"]
        cond += indent(rline, join=False)
        lines += indent(cond, join=False)
    lines = sorted(mtypeslines) + [''] +  lines
    lines += indent("# duck-typed dispatch based on whatever works!", join=False)
    refineopp = lambda x: (-1*sum([int(ts.isrefinement(a[1])) for a in x[0][1:]]), len(x[0]), x[1])
    mangitems = sorted(name_mangled.items(), key=refineopp)
    for key, mangled_name in mangitems:
        lines += indent('try:', join=False)
        if hasrtn:
            rline = dispatch_str_ret.format(mangled_name)
        else:
            rline = [dispatch_str_no_ret.format(mangled_name), "return"]
        lines += indent(indent(rline, join=False), join=False)
        lines += indent(["except (RuntimeError, TypeError, NameError):",
                         indent("pass", join=False)[0],], join=False)
    errmsg = "raise RuntimeError('method {0}() could not be dispatched')".format(name)
    lines += indent(errmsg, join=False)
    lines += ['']
    return lines


def _class_heirarchy(cls, ch, classes):
    if classes[cls]['parents'] is None:
        return
    if 0 == len(ch) or ch[0] != cls:
        ch.insert(0, cls)
    for p in classes[cls]['parents'][::-1]:
        ch.insert(0, p)
        _class_heirarchy(p, ch, classes)

def _method_instance_names(desc, classes, key, rtn, ts):
    classnames = []
    _class_heirarchy(desc['name'], classnames, classes)
    for classname in classnames:
        classrtn = classes.get(classname, {}).get('methods', {}).get(key, NotImplemented)
        if rtn != classrtn:
            continue
        #class_ctype = ts.cython_ctype(desc['name'])
        class_ctype = ts.cython_ctype(classname)
        inst_name = "(<{0} *> self._inst)".format(class_ctype)
        return inst_name, classname
    return "(<{0} *> self._inst)".format(ts.cython_ctype(desc['name'])), desc['name']


def _count0(x):
    c = {}
    for v in x:
        v0 = v[0]
        c[v0] = c.get(v0, 0) + 1
    return c

def _doc_add_sig(doc, name, args, ismethod=True):
    if doc.startswith(name):
        return doc
    sig = ['self'] if ismethod else []
    sig += [a[0] if len(a) < 3 else "{0}={2}".format(*a) for a in args]
    newdoc = "{0}({1})\n{2}".format(name, ", ".join(sig), doc)
    return newdoc


_pyx_class_template = \
'''{function_pointer_block}

cdef class {name}{parents}:
{class_docstring}

{cdefattrs}

    # constuctors
    def __cinit__(self, *args, **kwargs):
        self._inst = NULL
        self._free_inst = True

        # cached property defaults
{property_defaults}

{constructor_block}

    # attributes
{attrs_block}
    # methods
{methods_block}

    pass

{extra}
'''

def classpyx(desc, classes=None, ts=None, max_callbacks=8):
    """Generates a ``*.pyx`` Cython wrapper implementation for exposing a C/C++
    class based off of a dictionary description.  The environment is a
    dictionary of all class names known to their descriptions.

    Parameters
    ----------
    desc : dict
        Class description dictonary.
    classes : dict, optional
        Dictionary which maps all class names that are required to
        their own descriptions.  This is required for resolving class heirarchy
        dependencies.
    ts : TypeSystem, optional
        A type system instance.
    max_callbacks : int, optional
        The default maximum number of callbacks for function pointers.

    Returns
    -------
    pyx : str
        Cython ``*.pyx`` implementation file as in-memory string.

    """
    ts = ts or TypeSystem()
    if classes is None:
        classes = {desc['name']: desc}
    nodocmsg = "no docstring for {0}, please file a bug report!"
    pars = ', '.join([ts.cython_cytype(p) for p in desc['parents'] or ()])
    d = {'parents': pars if 0 == len(pars) else '('+pars+')',
         'namespace': desc['namespace'],
         'header_filename': desc['header_filename'],
         }
    name = desc['name']
    d['name'] = name if isinstance(name, basestring) else ts.cython_classname(name)[1]
    class_doc = desc.get('docstrings', {}).get('class', nodocmsg.format(desc['name']))
    d['class_docstring'] = indent('\"\"\"{0}\"\"\"'.format(class_doc))

    class_ctype = ts.cython_ctype(desc['name'])
    inst_name = "(<{0} *> self._inst)".format(class_ctype)

    import_tups = set()
    cimport_tups = set()
    for parent in desc['parents'] or ():
        ts.cython_import_tuples(parent, import_tups)
        ts.cython_cimport_tuples(parent, cimport_tups)

    cdefattrs = []
    mc = desc.get('extra', {}).get('max_callbacks', max_callbacks)

    alines = []
    pdlines = []
    fplines = []
    cached_names = []
    attritems = sorted(desc['attrs'].items())
    for aname, atype in attritems:
        if aname.startswith('_'):
            continue  # skip private
        adoc = desc.get('docstrings', {}).get('attrs', {})\
                                         .get(aname, nodocmsg.format(aname))
        if ts.isfunctionpointer(atype):
            alines += _gen_function_pointer_property(aname, atype, ts, adoc,
                        cached_names=cached_names, inst_name=inst_name,
                        classname=desc['name'], max_callbacks=mc)
            fplines += _gen_function_pointer_wrapper(aname, atype, ts,
                        max_callbacks=mc, classname=desc['name'])
            pdlines.append("self._{0}_vtab_i = {1}".format(aname, mc+1))
        else:
            alines += _gen_property(aname, atype, ts, adoc, cached_names=cached_names,
                                    inst_name=inst_name, classes=classes)
        ts.cython_import_tuples(atype, import_tups)
        ts.cython_cimport_tuples(atype, cimport_tups)
    if len(fplines) > 0:
        fplines.append("_MAX_CALLBACKS_{0} = {1}".format(desc['name'], mc))
    d['attrs_block'] = indent(alines)
    d['function_pointer_block'] = "\n".join(fplines)
    pdlines += ["{0} = None".format(n) for n in cached_names]
    d['property_defaults'] = indent(indent(pdlines, join=False))

    mlines = []
    clines = []
    methcounts = _count0(desc['methods'])
    currcounts = dict([(k, 0) for k in methcounts])
    mangled_mnames = {}
    mitems = list(desc['methods'].items())
    methitems = sorted(x for x in mitems if isinstance(x[0][0], basestring))
    methitems += sorted(x for x in mitems if not isinstance(x[0][0], basestring))
    for mkey, mrtn in methitems:
        mname, margs = mkey[0], mkey[1:]
        mbasename = mname if isinstance(mname, basestring) else mname[0]
        mcyname = ts.cython_funcname(mname)
        if mbasename.startswith('_'):
            continue  # skip private
        if any([a[1] is None or a[1][0] is None for a in margs]):
            continue
        if 1 < methcounts[mname]:
            mname_mangled = "_{0}_{1}_{2:0{3}}".format(desc['name'], mcyname,
                    currcounts[mname], int(math.log(methcounts[mname], 10)+1)).lower()
        else:
            mname_mangled = mcyname
        currcounts[mname] += 1
        mangled_mnames[mkey] = mname_mangled
        for a in margs:
            ts.cython_import_tuples(a[1], import_tups)
            ts.cython_cimport_tuples(a[1], cimport_tups)
        minst_name, mcname = _method_instance_names(desc, classes, mkey, mrtn, ts)
        if mcname != desc['name']:
            ts.cython_import_tuples(mcname, import_tups)
            ts.cython_cimport_tuples(mcname, cimport_tups)
        if mrtn is None:
            # this must be a constructor
            if mname not in (desc['name'], '__init__'):
                continue  # skip destuctors
            if 1 == methcounts[mname]:
                mname_mangled = '__init__'
                mangled_mnames[mkey] = mname_mangled
            mdoc = desc.get('docstrings', {}).get('methods', {}).get(mname, '')
            mdoc = _doc_add_sig(mdoc, mcyname, margs)
            construct = desc['construct']
            if construct == 'struct':
                cimport_tups.add(('libc.stdlib', 'malloc'))
            clines += _gen_constructor(mcyname, mname_mangled, desc['name'], margs, 
                        ts, doc=mdoc, srcpxd_filename=desc['srcpxd_filename'],
                        inst_name=minst_name, construct=construct)
            if 1 < methcounts[mname] and currcounts[mname] == methcounts[mname]:
                # write dispatcher
                nm = dict([(k, v) for k, v in mangled_mnames.items() \
                           if k[0] == mbasename])
                clines += _gen_dispatcher('__init__', nm, ts, doc=mdoc, hasrtn=False)
        else:
            # this is a normal method
            ts.cython_import_tuples(mrtn, import_tups)
            ts.cython_cimport_tuples(mrtn, cimport_tups)
            mdoc = desc.get('docstrings', {}).get('methods', {})\
                                             .get(mname, nodocmsg.format(mname))
            mdoc = _doc_add_sig(mdoc, mcyname, margs)
            mlines += _gen_function(mcyname, mname_mangled, margs, mrtn, ts, mdoc,
                                  inst_name=minst_name, is_method=True)
            if 1 < methcounts[mname] and currcounts[mname] == methcounts[mname]:
                # write dispatcher
                nm = dict([(k, v) for k, v in mangled_mnames.items() \
                           if k[0] == mbasename])
                mlines += _gen_dispatcher(mcyname, nm, ts, doc=mdoc)
    if 0 == len(desc['methods']) or 0 == len(clines):
        # provide a default constructor
        mdocs = desc.get('docstrings', {}).get('methods', {})
        mdoc = mdocs.get(desc['name'], False) or mdocs.get('__init__', '')
        mdoc = _doc_add_sig(mdoc, '__init__',
                            [(_a, _t, "None") for _a, _t in attritems])
        clines += _gen_default_constructor(desc, attritems, ts, doc=mdoc)
        cimport_tups.add(('libc.stdlib', 'malloc'))
    if desc['parents'] is None:
        clines += ["def __dealloc__(self):"]
        clines += indent("if self._free_inst:", join=False)
        clines += indent(indent("free(self._inst)", join=False), join=False)
        cimport_tups.add(('libc.stdlib', 'free'))

    # Add dispatcher for templated methods
    template_meths = _template_method_names(desc['methods'])
    template_dispatcher = _gen_template_func_dispatcher(template_meths, ts)
    mlines.append(indent(template_dispatcher))

    d['methods_block'] = indent(mlines)
    d['constructor_block'] = indent(clines)

    d['extra'] = desc.get('extra', {}).get('pyx', '')
    d['cdefattrs'] = indent(cdefattrs)
    pyx = _pyx_class_template.format(**d)
    if 'pyx_filename' not in desc:
        desc['pyx_filename'] = '{0}.pyx'.format(d['name'].lower())
    return import_tups, cimport_tups, pyx

def varpyx(desc, ts=None):
    """Generates a ``*.pyx`` Cython wrapper implementation for exposing a C/C++
    variable based off of a dictionary description.

    Parameters
    ----------
    desc : dict
        Variable description dictonary.
    ts : TypeSystem, optional
        A type system instance.

    Returns
    -------
    pyx : str
        Cython ``*.pyx`` implementation as in-memory string.

    """
    ts = ts or TypeSystem()
    nodocmsg = "no docstring for {0}, please file a bug report!"
    inst_name = desc['srcpxd_filename'].rsplit('.', 1)[0]
    import_tups = set()
    cimport_tups = set(((inst_name,),))
    name = desc['name']
    t = ts.canon(desc['type'])

    vlines = []
    if ts.isenum(t):
        doc = '"' * 3 +  "{0} is enumeration {1} of {2}" + '"' * 3
        for ename, val in t[1][2][2]:
            vlines.append("{0} = {1}.{0}".format(ename, inst_name))
            vlines.append(doc.format(ename, val, name))
            vlines.append("")
    else:
        decl, body, rtn, iscached = ts.cython_c2py(name, t, view=False, cached=False,
                                                inst_name=inst_name)
        vlines.append(decl)
        vlines.append(body)
        vlines.append(name + " = " + rtn)
        docstring = desc.get('docstring', None)
        if docstring is None:
            docstring = '"' * 3 + nodocmsg.format(name) + '"' * 3
        vlines.append(docstring)

    vlines.append(desc.get('extra', {}).get('pyx', ''))
    pyx = '\n'.join(vlines)
    if 'pyx_filename' not in desc:
        desc['pyx_filename'] = '{0}.pyx'.format(desc['name'].lower())
    return import_tups, cimport_tups, pyx


def funcpyx(desc, ts=None):
    """Generates a ``*.pyx`` Cython wrapper implementation for exposing a C/C++
    function based off of a dictionary description.

    Parameters
    ----------
    desc : dict
        function description dictonary.
    ts : TypeSystem, optional
        A type system instance.

    Returns
    -------
    pyx : str
        Cython ``*.pyx`` implementation as in-memory string.

    """
    ts = ts or TypeSystem()
    nodocmsg = "no docstring for {0}, please file a bug report!"
    inst_name = desc['srcpxd_filename'].rsplit('.', 1)[0]

    import_tups = set()
    cimport_tups = set(((inst_name,),))

    # For renaming
    ftopname = desc['name']
    fcytopname = ts.cython_funcname(ftopname)

    flines = []
    funccounts = _count0(desc['signatures'])
    currcounts = dict([(k, 0) for k in funccounts])
    mangled_fnames = {}
    funcitems = sorted(desc['signatures'].items())
    for fkey, frtn in funcitems:
        fname, fargs = fkey[0], fkey[1:]
        fbasename = fname if isinstance(fname, basestring) else fname[0]
        #fcppname = ts.cpp_funcname(fname)
        fcyname = ts.cython_funcname(fname)
        if fbasename.startswith('_'):
            continue  # skip private
        if any([a[1] is None or a[1][0] is None for a in fargs + (frtn,)]):
            continue
        if 1 < funccounts[fname]:
            fname_mangled = "_{0}_{1:0{2}}".format(fcytopname, currcounts[fname],
                                        int(math.log(funccounts[fname], 10)+1)).lower()
        else:
            fname_mangled = fcytopname
        currcounts[fname] += 1
        mangled_fnames[fkey] = fname_mangled
        for a in fargs:
            ts.cython_import_tuples(a[1], import_tups)
            ts.cython_cimport_tuples(a[1], cimport_tups)
        ts.cython_import_tuples(frtn, import_tups)
        ts.cython_cimport_tuples(frtn, cimport_tups)
        fdoc = desc.get('docstring', nodocmsg.format(fcyname))
        fdoc = _doc_add_sig(fdoc, fcyname, fargs, ismethod=False)
        flines += _gen_function(fcyname, fname_mangled, fargs, frtn, ts, fdoc,
                                inst_name=inst_name, is_method=False)
        if 1 < funccounts[fname] and currcounts[fname] == funccounts[fname]:
            # write dispatcher
            nm = dict([(k, v) for k, v in mangled_fnames.items() if k[0] == fname])
            flines += _gen_dispatcher(fcytopname, nm, ts, doc=fdoc, is_method=False)
    flines.append(desc.get('extra', {}).get('pyx', ''))
    pyx = '\n'.join(flines)
    if 'pyx_filename' not in desc:
        desc['pyx_filename'] = '{0}.pyx'.format(desc['name'].lower())
    return import_tups, cimport_tups, pyx

#
# Plugin
#

class XDressPlugin(Plugin):
    """The main cython generator plugin.
    """

    requires = ('xdress.autodescribe',)
    """This plugin requires autodescribe."""

    defaultrc = {'max_callbacks': 8}

    rcdocs = {
        "max_callbacks": "The maximum number of callbacks for function pointers", 
        }

    def update_argparser(self, parser):
        parser.add_argument('--max-callbacks', type=int, dest="max_callbacks",
                    help=self.rcdocs["max_callbacks"])

    def setup(self, rc):
        if rc.max_callbacks < 1:
            raise ValueError("max_callbacks must be greater than or equal to 1")

    def execute(self, rc):
        print("cythongen: creating C/C++ API wrappers")
        env = rc.env
        classes = {}
        for modname, mod in env.items():
            for name, desc in mod.items():
                if isclassdesc(desc):
                    classes[name] = desc

        # generate all files
        cpppxds = gencpppxd(env, ts=rc.ts)
        pxds = genpxd(env, classes, ts=rc.ts, max_callbacks=rc.max_callbacks)
        pyxs = genpyx(env, classes, ts=rc.ts, max_callbacks=rc.max_callbacks)

        # write out all files
        for key, cpppxd in cpppxds.items():
            newoverwrite(cpppxd, os.path.join(rc.packagedir,
                         env[key]['srcpxd_filename']), rc.verbose)
        for key, pxd in pxds.items():
            newoverwrite(pxd, os.path.join(rc.packagedir,
                         env[key]['pxd_filename']), rc.verbose)
        for key, pyx in pyxs.items():
            newoverwrite(pyx, os.path.join(rc.packagedir,
                         env[key]['pyx_filename']), rc.verbose)


#
# Misc Helpers Below
#

def _format_ns(desc):
    ns = desc.get('namespace', None)
    if ns is None:
        return ""
    elif len(ns) == 0:
        return ""
    else:
        return 'namespace "{0}"'.format(ns)

def _format_alias(desc, ts):
    ns = desc.get('namespace', None)
    cpp_name = ts.cpp_type(desc['name'])
    if ns is None or len(ns) == 0:
        return '"{0}"'.format(cpp_name)
    else:
        return '"{0}::{1}"'.format(ns, cpp_name)

def _mangle_function_pointer_name(name, classname):
    pyref = "_xdress_{0}_{1}_proxy".format(classname, name)
    cref = pyref + "_func"
    return pyref, cref

def _isclassptr(t, classes):
    return (not isinstance(t, basestring) and t[1] == '*' and
            isinstance(t[0], basestring) and t[0] in classes)

def _isclassdblptr(t, classes):
    if 2 != len(t):
        return False
    return _isclassptr(t[0], classes) and t[1] == '*'

_exc_c_base = frozenset(['int16', 'int32', 'int64', 'int128',
                         'float32', 'float64', 'float128'])

_exc_ptr_matcher = TypeMatcher((MatchAny, '*'))
    
def _exception_str(exceptions, srcfile, rtntype, ts):
    if not exceptions:
        return ""
    if isinstance(exceptions, basestring):
        return "except " + exceptions
    lang = guess_language(srcfile)
    if lang == 'c':
        if rtntype is None:
            return "except -1"  # helpful when we accidentally mis-guessed C for C++
        rtntype = ts.canon(rtntype)
        if rtntype in _exc_c_base:
            return "except -1"
        elif ts.isenum(rtntype):
            return "except -1"
        elif _exc_ptr_matcher.matches(rtntype):
            return "except -1"
        else:
            return ""
    elif lang == 'c++':
        return "except +"
    else:
        return ""

def _template_method_names(methods):
    methnames = set()
    for sig, rtn in methods.items():
        name = sig[0]
        if isinstance(name, basestring):
            continue
        elif rtn is None:  # ignore constructor / destructors
            continue
        methnames.add(name)
    return methnames

def _template_funcnames_in_mod(mod):
    funcnames = set()
    for name, desc in mod.items():
        if isinstance(name, basestring) or not isfuncdesc(desc):
            continue
        funcnames.add(name)
    return funcnames

def _classnames_in_mod(mod, ts):
    classnames = set()
    for name, desc in mod.items():
        if not isclassdesc(desc):
            continue
        classnames.add(name)
        classnames.add(ts.basename(name))
        classnames.add(desc['type'])
    return classnames

def _template_classnames_in_mod(mod):
    classnames = set()
    for name, desc in mod.items():
        if isinstance(name, basestring) or not isclassdesc(desc):
            continue
        classnames.add(name)
    return classnames
