#! /usr/bin/env python

"""
Use Python's string formatting features to substitute values in various
templated C++ wrappers, as Cython has no support for templates (and fused
types are currently useless).

TODO: is the situation better now?
"""

import itertools
from os.path import join, abspath
import os, sys, inspect

PATH  = abspath(os.path.dirname(inspect.getsourcefile(lambda:0)))


##############


def getTemplate(fname):
  return ''.join(open(PATH+'/templates/'+fname).readlines())


##############


class TemplateConfig(object):

  def __init__(self):
      self.config = dict()
      self.segments = dict()
      self.mixin = [ 'x', 'y', 'z' ]
      self.pyfunc = { 'd' : 'float', 'i' : 'int', 's' : 'str' }

  def set(self, key, value):
      self.config[key] = value

  def get(self, key):
      assert(key in self.config)
      return self.config[key]

  def setMixin(self, dim):
      assert(dim < len(self.mixin))
      self.set('MIXINDIM', dim)
      self.set('mixin', self.mixin[dim])
      self.set('MIXIN', self.mixin[dim].upper())

  def apply(self, template, **kwargs):
      self.config.update(kwargs)
      return template.format_map(self.config)

  def init(self, *args):
      for seg in args:
        self.segments[seg] = list()

  def append(self, key, template, suffix = ''):
      assert(key in self.segments)
      self.segments[key].append( self.apply(template) + suffix )

  def mkSegment(self, key, delim = '\n', **kwargs):
      assert(key in self.segments)
      self.config.update(kwargs)
      return delim.join(self.apply(seg) for seg in self.segments[key])


##############


def make_core():

    includes = [ sub + f for sub in [ 'include/', 'include/generated/' ]
                         for f in os.listdir(PATH + '/' + sub) if f.endswith('.pyx') ]

    header = getTemplate('header.dat')
    base_template = getTemplate('core.dat')

    with open(PATH + '/core.pyx', 'w') as f:
        f.write(header)
        f.write(base_template)
        for inc in sorted(includes):
          f.write('include "{0}"\n'.format(inc))


##############

def make_declarations(op_defs):

    header = getTemplate('header.dat')
    base_template = getTemplate('declarations.dat')

    generated_defs = 'cdef extern from "merge.hh":' + ''.join(op_defs)

    with open(PATH + '/declarations.pxd', 'w') as f:
        f.write(header)
        f.write(base_template)
        f.write(generated_defs)



##############


def make_BinnedTemplates(AOType, n, edge_types, op_defs):

    config = TemplateConfig()

    header = getTemplate('header.dat')
    base_template  = getTemplate('binnedAO.dat')

    config.init('init2', 'init4', 'cdef', 'tryex', 'edgeGen', 'edgeRec', 'func',
                'mixin', 'clone', 'caxis', 'binGlobal', 'operator', 'ioperator',
                'div', 'eff', 'estimate', 'histo', 'scatter', 'typeRed', 'merge',
                'maskBin', 'maskSlice', 'comparator')

    config.set('dim', n)
    config.set('dimPlusOne', n + 1)
    config.set('AOType', AOType)
    config.set('AOTypeMini', AOType[0])
    config.set('AOSHORT', 'B'+AOType[0])
    config.set('AOLONG', 'Binned'+AOType)
    config.set('binCoords', ', '.join([ config.mixin[i] for i in range(n) ]))
    config.set('fillCoords', ', '.join([ config.mixin[i] for i in range(n+1 if 'Profile' in AOType else n) ]))
    config.set('contentType', 'Estimate' if 'Estimate' in AOType else 'Dbn%dD' % (n if 'Histo' in AOType else n+1))
    config.set('contentPtr', 'estptr' if 'Estimate' in AOType else 'd%dptr' % (n if 'Histo' in AOType else n+1))
    config.set('axisConfig', ','.join([ 'AxisT%d' % i for i in range(1,n+1) ]) if n > 1 else 'AxisT')
    config.set('defaultAxisT', ','.join('double' for _ in range(n)))
    config.set('defaultAxisTmini', ''.join('d' for _ in range(n)))
    config.set('edgeSetup', ', '.join([ 'edgesA%d' % i for i in range(1,n+1) ]))
    config.set('fwdEdges', ', '.join(['edges[%d]' % i for i in range(n)]))
    config.set('mkScatCustom', 'pat_match="",' if 'Estimate' in AOType else 'binwidthdiv=True,useFocus=False,')

    tmp = '        assert( hasattr({0}, "__iter__") and len({0}) )'
    config.append('init2', '\n'.join([ tmp.format('edgesA%d' % (i+1)) for i in range(n) ]))

    tmp = '        cdef vector[vector[{0}]] edges_{1}'
    config.append('init2', '\n'.join([ tmp.format(edge, edge[0]) for edge in edge_types.split(',') ]))

    for i in range(n if n < 4 else 0):
       config.init('func_caxis%d' % i, 'func_daxis%d' % i)

    tmp_init2  = '        {logic} {axisCombi}:\n{edgeMaking}'
    tmp_init2 += '          cutil.set_owned_ptr(self, new c.Binned{AOType}{dim}D[{axisT}]'
    tmp_init2 += '({triggeredEdges}, <string>path, <string>title))\n'
    tmp_init2 += '          self._set_config({triggeredEdges})'

    tmp_init4  = '        {logic} other._config == "{axisMini}":\n'
    tmp_init4 += '          cutil.set_owned_ptr(self, <c.Binned{AOType}{dim}D[{axisT}]*> other.binned_{axisMini}_ptr().newclone())'

    tmp_cdef  = '    cdef inline c.{AOLONG}{dim}D[{axisT}]* binned_{axisMini}_ptr(self) except NULL:\n'
    tmp_cdef += '        return <c.{AOLONG}{dim}D[{axisT}]*> self.ptr()\n\n'

    tmp_tryex  = '          try:\n'
    tmp_tryex += '            if str((<c.Binned{AOType}{dim}D[{axisT}]*> self.ptr())._config()) != "{axisMini}":\n'
    tmp_tryex += '                raise RuntimeError("Dynamic ptr cast failed. Next...")\n'
    tmp_tryex += '            edges = tuple([\n'
    tmp_tryex += '{edgeReco}\n'
    tmp_tryex += '            ])\n'
    tmp_tryex += '            self._config = "{axisMini}"\n'
    tmp_tryex += '          except: pass'

    tmp_func  = '        if self._config == "{axisMini}":\n'
    tmp_func += '          return self.binned_{axisMini}_ptr().'

    tmp_axis_mixin  = '    def {method}(self{mixinArgs}):\n'
    tmp_axis_mixin += '        """{method} on {mixin} axis."""\n'
    tmp_axis_mixin += '{rtn}\n'

    tmp_rebin  = '    def rebin{MIXIN}(self, arg, *args, **kwargs):\n'
    tmp_rebin += '        if hasattr(arg, "__iter__"):\n'
    tmp_rebin += '            self.rebin{MIXIN}To(arg)\n' #, *args, **kwargs)\n'
    tmp_rebin += '        else:\n'
    tmp_rebin += '            self.rebin{MIXIN}By(arg, *args, **kwargs)\n'

    tmp_content_mixin  = '    def {method}(self{mixinArgs}):\n'
    tmp_content_mixin += '{rtn}'

    tmp_clone  = '        {logic} self._config == "{axisMini}":\n'
    tmp_clone += '          rtn = cutil.new_owned_cls(Binned{AOType}{dim}D, '
    tmp_clone += '<c.Binned{AOType}{dim}D[{axisT}]*> self.binned_{axisMini}_ptr().newclone())'

    config.append('binGlobal', '        b = Bin{dim}D('+config.get('contentType')+')')
    tmp_glob_bin  = '        {logic} self._config == "{axisMini}":\n'
    tmp_glob_bin += '          rtn = cutil.new_borrowed_cls(b, &self.binned_{axisMini}_ptr().bin(i), self)\n'
    tmp_glob_bin += '          rtn.setIndices(i, self.binned_{axisMini}_ptr()._global2local(i), st)'

    tmp_operator  = '        {logic} self._config == "{axisMini}":\n'
    tmp_operator += '          tmp = c.B{AOTypeMini}_OTYPE_{axisMini}(self.binned_{axisMini}_ptr(), other.binned_{axisMini}_ptr())\n'
    tmp_operator += '          rtn = cutil.new_owned_cls(Binned{AOType}{dim}D, tmp)'

    tmp_ioperator  = '        {logic} self._config == "{axisMini}":\n'
    tmp_ioperator += '          c.B{AOTypeMini}_iOTYPE_{axisMini}(self.binned_{axisMini}_ptr(), other.binned_{axisMini}_ptr())'

    tmp_comparator  = '        {logic} self._config == "{axisMini}":\n'
    tmp_comparator += '          return c.B{AOTypeMini}_OTYPE_{axisMini}(self.binned_{axisMini}_ptr(), other.binned_{axisMini}_ptr())'

    tmp_operator_mixin  = '    def __OTYPE__(Binned{AOType}{dim}D self, Binned{AOType}{dim}D other):\n'
    tmp_operator_mixin += '        if self._binning is None and other._binning is None:\n'
    tmp_operator_mixin += '            return self\n'
    tmp_operator_mixin += '        assert(self._config == other._config)\n'
    tmp_operator_mixin += '        rtn = None\n'
    tmp_operator_mixin += '{operator}\n'
    tmp_operator_mixin += '        rtn._set_config(self._binning)\n'
    tmp_operator_mixin += '        return rtn\n\n'
    tmp_operator_mixin += '    def __iOTYPE__(Binned{AOType}{dim}D self, Binned{AOType}{dim}D other):\n'
    tmp_operator_mixin += '        if self._binning is None and other._binning is None:\n'
    tmp_operator_mixin += '            return self\n'
    tmp_operator_mixin += '        assert(self._config == other._config)\n'
    tmp_operator_mixin += '{ioperator}\n'
    tmp_operator_mixin += '        return self\n'

    tmp_comparison_mixin  = '    def __OTYPE__(Binned{AOType}{dim}D self, Binned{AOType}{dim}D other):\n'
    tmp_comparison_mixin += '{comparator}\n'
    tmp_comparison_mixin += '        print("WARNING: method not supported for this axis type!")\n'

    tmp_maskBin  = '        {logic} self._config == "{axisMini}":\n'
    tmp_maskBin += '          return self.binned_{axisMini}_ptr().maskBin(idx, status)'

    tmp_maskSlice  = '        {logic} self._config == "{axisMini}":\n'
    tmp_maskSlice += '          return self.binned_{axisMini}_ptr().maskSlice(dim, idx, status)'

    tmp_div  = '          {logic} self._config == "{axisMini}":\n'
    tmp_div  = '          {logic} self._config == "{axisMini}":\n'
    tmp_div += '            tmp = c.{AOSHORT}_div_{axisMini}(self.binned_{axisMini}_ptr(), other.binned_{axisMini}_ptr())\n'
    tmp_div += '            rtn = cutil.new_owned_cls(BinnedEstimate{dim}D, tmp)'

    tmp_eff  = '          {logic} self._config == "{axisMini}":\n'
    tmp_eff += '            tmp = c.{AOSHORT}_eff_{axisMini}(self.binned_{axisMini}_ptr(), other.binned_{axisMini}_ptr())\n'
    tmp_eff += '            rtn = cutil.new_owned_cls(BinnedEstimate{dim}D, tmp)'

    tmp_mkEstimate  = '        {logic} self._config == "{axisMini}":\n'
    tmp_mkEstimate += '          tmp = c.{AOSHORT}_est_{axisMini}(self.binned_{axisMini}_ptr(), path, source, divbyvol)\n'
    tmp_mkEstimate += '          rtn = cutil.new_owned_cls(BinnedEstimate{dim}D, tmp)'

    tmp_mkHisto  = '        {logic} self._config == "{axisMini}":\n'
    tmp_mkHisto += '          tmp = c.{AOSHORT}_hist_{axisMini}(self.binned_{axisMini}_ptr(), path)\n'
    tmp_mkHisto += '          rtn = cutil.new_owned_cls(BinnedHisto{dim}D, tmp)'

    mkScatCustom = 'pat_match,' if 'Estimate' in AOType else 'binwidthdiv,useFocus,'
    tmp_mkScatter  = '        {logic} self._config == "{axisMini}":\n'
    tmp_mkScatter += '          s = <c.Scatter{dimPlusOne}D> self.binned_{axisMini}_ptr()'
    tmp_mkScatter += '.mkScatter(path,%sincludeOverflows,includeMaskedBins)\n' % mkScatCustom
    tmp_mkScatter += '          return cutil.new_owned_cls(Scatter{dimPlusOne}D, s.newclone())'

    tmp  = '    void {AOSHORT}_iOTYPE_{axisMini} "cython_iOTYPE" ({AOLONG}{dim}D[{axisT}]*, {AOLONG}{dim}D[{axisT}]*)\n'
    tmp += '    {AOLONG}{dim}D[{axisT}]* {AOSHORT}_OTYPE_{axisMini} "cython_OTYPE" ({AOLONG}{dim}D[{axisT}]*, {AOLONG}{dim}D[{axisT}]*)'
    tmp_merge_def  = '\n'.join([ tmp.replace('OTYPE', otype) for otype in [ 'add', 'sub'] ])
    tmp = '    bool {AOSHORT}_OTYPE_{axisMini} "cython_OTYPE" ({AOLONG}{dim}D[{axisT}]*, {AOLONG}{dim}D[{axisT}]*)'
    tmp_merge_def += '\n' + '\n'.join([ tmp.replace('OTYPE', otype) for otype in [ 'eq', 'ne'] ])
    tmp_merge_def += '\n    BinnedEstimate{dim}D[{axisT}]* {AOSHORT}_div_{axisMini} "cython_div" '
    tmp_merge_def += '({AOLONG}{dim}D[{axisT}]*, {AOLONG}{dim}D[{axisT}]*)'
    tmp_merge_def += '\n    BinnedEstimate{dim}D[{axisT}]* {AOSHORT}_eff_{axisMini} "cython_div" '
    tmp_merge_def += '({AOLONG}{dim}D[{axisT}]*, {AOLONG}{dim}D[{axisT}]*)'
    if not 'Estimate' in AOType:
        tmp_merge_def += '\n    BinnedEstimate{dim}D[{axisT}]* {AOSHORT}_est_{axisMini} "cython_est" '
        tmp_merge_def += '({AOLONG}{dim}D[{axisT}]*, string&, string&, bool)'
        if 'Profile' in AOType:
            tmp_merge_def += '\n    BinnedHisto{dim}D[{axisT}]* {AOSHORT}_hist_{axisMini} "cython_hist" '
            tmp_merge_def += '({AOLONG}{dim}D[{axisT}]*, string&)'

    axis_set = [ ','.join(p) for p in itertools.product(edge_types.split(','), repeat=n) ]
    for axis_idx, axisT in enumerate(axis_set):

        config.set('axisT', axisT)
        config.set('axisMini', ''.join([ a[0] for a in axisT.split(',') ]))
        config.set('logic', 'elif' if axis_idx else 'if')

        config.append('cdef', tmp_cdef)

        tmp = [ ]
        for i in range(n if n < 4 else 0):
            config.setMixin(i)
            tmp.append(config.apply('              (<c.Binned{AOType}{dim}D[{axisT}]*> self.ptr()).{mixin}Edges(False),'))
        config.set('edgeReco', '\n'.join(tmp))
        config.append('tryex', tmp_tryex)

        axisCombi = [ "isinstance(edgesA"+str(i+1)+"[0], "+config.pyfunc[a]+")" for i,a in enumerate(config.get('axisMini')) ]
        config.set('axisCombi', ' and '.join(axisCombi))

        edgeMaking = [ '' for _ in edge_types.split(',') ]
        for a,i in sorted([ (a,i) for i,a in enumerate(config.get('axisMini')) ]):
            pyfunc = config.pyfunc[a]
            thisa = config.get('axisMini').find(a)
            if edgeMaking[thisa] == '':
                edgeMaking[thisa] += "          edges_"+a+" = [ list("+pyfunc+"(x) for x in edgesA"+str(i+1)+"), "
            else:
                edgeMaking[thisa] += "\n                      list("+pyfunc+"(x) for x in edgesA"+str(i+1)+"), "
        for i in range(len(edgeMaking)):
            if edgeMaking[i] != '':
                edgeMaking[i] += ']\n'
        config.set('edgeMaking', ''.join(edgeMaking))

        triggeredEdges = [ ]
        trig_count = dict((edge[0],0) for edge in edge_types.split(','))
        for i,a in enumerate(config.get('axisMini')):
            triggeredEdges.append('edges_'+a+'['+str(trig_count[a])+']')
            trig_count[a] += 1
        config.set('triggeredEdges', ', '.join(triggeredEdges))

        config.append('div',        tmp_div)
        config.append('eff',        tmp_eff)
        config.append('init2',      tmp_init2)
        config.append('init4',      tmp_init4)
        config.append('clone',      tmp_clone)
        config.append('histo',      tmp_mkHisto)
        config.append('estimate',   tmp_mkEstimate)
        config.append('scatter',    tmp_mkScatter)
        config.append('binGlobal',  tmp_glob_bin)
        config.append('operator',   tmp_operator)
        config.append('ioperator',  tmp_ioperator)
        config.append('comparator', tmp_comparator)
        config.append('merge',      tmp_merge_def)
        config.append('maskBin',    tmp_maskBin)
        config.append('maskSlice',  tmp_maskSlice)

        config.append('func', tmp_func, suffix = '{method}')

        for i in range(n if n < 4 else 0):
            atype = 'caxis' if config.get('axisMini')[i] == 'd' else 'daxis'
            config.append('func_%s%d' % (atype, i), tmp_func, suffix = '{method}')

    warn = '        print("WARNING: method not supported for this axis type!")\n'
    config.append('func', warn)
    for i in range(n if n < 4 else 0):
        config.append('func_caxis%d' % i, warn)
        config.append('func_daxis%d' % i, warn)

    config.append('binGlobal', '        rtn.setBinning(self._binning)')
    config.append('binGlobal', '        return rtn')

    # content-specific mixin
    if 'Estimate' in AOType:

      tmp  = "    def vals(self):\n"
      tmp += "        return self._mknp([b.val() for b in self.bins()])\n\n"
      tmp += "    def auc(self,includeBinVol=True,includeOverflows=False,includeMaskedBins=False):\n"
      tmp += "        return self.areaUnderCurve(includeBinVol,includeOverflows,includeMaskedBins)\n\n"

      config.append('mixin', config.apply(tmp))
      for func, fargs in [
            ('scale', ', factor'),
            ('indexAt', ', {binCoords}'),
            ('maskedBins', ''),
            ('sources', ''),
            ('areaUnderCurve', ', includeBinVol=True, includeOverflows=False, includeMaskedBins=False'),
            ('covarianceMatrix', ', ignoreOffDiagonalTerms=False, includeOverflows=False, includeMaskedBins=False, pat_uncorr="^stat|^uncor"'),
          ]:
          cppfunc = func+'('+','.join([ p.split('=')[0] for p in fargs.split(', ') if p ])+')'
          config.set('rtn', config.mkSegment('func', method = cppfunc))
          config.append('mixin', config.apply(tmp_content_mixin, method = func, mixinArgs = fargs))

    else: # Dbn-like bin content

      tmp  = '    def mkEstimate(self, str path = "", str source = "", divbyvol = True):\n'
      tmp += '        """None -> Estimate.\n\n'
      tmp += '        Convert this {AOLONG}{dim}D to an BinnedEstimate{dim}D."""\n'
      tmp += '        rtn = None\n'
      tmp += config.mkSegment('estimate') + '\n'
      tmp += '        rtn._set_config(self._binning)\n'
      tmp += '        return rtn\n'
      if 'Profile' in AOType:
        tmp += '\n    def mkHisto(self, str path = ""):\n'
        tmp += '        """None -> BinnedHisto{dim}D.\n\n'
        tmp += '        Convert this {AOLONG}{dim}D to a BinnedHisto{dim}D."""\n'
        tmp += '        rtn = None\n'
        tmp += config.mkSegment('histo') + '\n'
        tmp += '        rtn._set_config(self._binning)\n'
        tmp += '        return rtn\n'
      config.append('typeRed', tmp)

      tmp  = '    def fill(self, {fillCoords}, weight = 1.0, fraction = 1.0):\n'
      tmp += '        """\n'
      tmp += '        ([coords], float weight=1.0, float fraction=1.0) -> int\n\n'
      tmp += '        Fills the distribution with the given weight at given [coords].\n'
      tmp += '        """\n'
      tmp += config.mkSegment('func', method = config.apply('fill({fillCoords}, weight, fraction)'))
      tmp += '\n'

      config.append('mixin', config.apply(tmp))
      for func, fargs in [
            ('indexAt', ', {binCoords}'),
            ('fillDim', ''),
            ('normalize', ', normto=1.0, includeOverflows=True'),
            ('scaleW', ', factor'),
            ('sumW', ', includeOverflows=True'),
            ('sumW2', ', includeOverflows=True'),
            ('sumWA', ', dim, includeOverflows=True'),
            ('sumWA2', ', dim, includeOverflows=True'),
            ('numEntries', ', includeOverflows=True'),
            ('effNumEntries', ', includeOverflows=True'),
            ('integral', ', includeOverflows=True'),
            ('integralError', ', includeOverflows=True'),
            ('dVol', ', includeOverflows=True'),
            ('density', ', includeOverflows=True'),
            ('densityError', ', includeOverflows=True'),
            ('densitySum', ', includeOverflows=True'),
            ('maxDensity', ', includeOverflows=True'),
            ('maskedBins', ''),
          ]:
          cppfunc = func+'('+','.join([ p.split('=')[0] for p in fargs.split(', ') if p ])+')'
          config.set('rtn', config.mkSegment('func', method = cppfunc))
          config.append('mixin', config.apply(tmp_content_mixin, method = func, mixinArgs = fargs))

    tmp_mpi  = '\n    def lengthContent(self, fixed_length = False):\n'
    tmp_mpi += '        """Length of serialisation data vector for MPI communication."""\n'
    tmp_mpi += config.mkSegment('func', method = 'lengthContent(fixed_length)')
    tmp_mpi += '\n    def serializeContent(self, fixed_length = False):\n'
    tmp_mpi += '        """Data serialisation for MPI communication."""\n'
    tmp_mpi += config.mkSegment('func', method = 'serializeContent(fixed_length)')
    tmp_mpi += '\n    def deserializeContent(self, data):\n'
    tmp_mpi += '        """Data deserialisation for MPI communication."""\n'
    tmp_mpi += '        cdef vector[double] cdata\n'
    tmp_mpi += '        cdata = [ float(x) for x in data ]\n'
    tmp_mpi += config.mkSegment('func', method = 'deserializeContent(data)')
    tmp_mpi += '\n    def lengthMeta(self, skipPath = True, skipTitle = True):\n'
    tmp_mpi += '        """Length of serialisation meta-data vector for MPI communication."""\n'
    tmp_mpi += config.mkSegment('func', method = 'lengthMeta(skipPath, skipTitle)')
    tmp_mpi += '\n    def serializeMeta(self, skipPath = True, skipTitle = True):\n'
    tmp_mpi += '        """Meta-data serialisation for MPI communication."""\n'
    tmp_mpi += config.mkSegment('func', method = 'serializeMeta(skipPath, skipTitle)')
    tmp_mpi += '\n    def deserializeMeta(self, data, resetPath = False, resetTitle = False):\n'
    tmp_mpi += '        """Meta-data deserialisation for MPI communication."""\n'
    tmp_mpi += '        cdef vector[string] cdata\n'
    tmp_mpi += '        cdata = [ str(x) for x in data ]\n'
    tmp_mpi += config.mkSegment('func', method = 'deserializeMeta(cdata, resetPath, resetTitle)')
    config.set('MPICOMM', tmp_mpi)

    # axis-specific mixin
    for i in range(n if n < 4 else 0):
        config.setMixin(i)
        # set variables for CAxis constructor arguments
        config.append('caxis', 'nbins{MIXIN}, lower{MIXIN}, upper{MIXIN}')
        config.append('edgeGen', '            linspace(nbins{MIXIN}, lower{MIXIN}, upper{MIXIN}),')
        # set axis-specific methods
        mixin_methods = [
          ('numBins{MIXIN}', 'func',             ', includeOverflows=False'),
          ('{mixin}Edges',   'func',             ', includeOverflows=False'),
          ('{mixin}Widths',  'func_caxis%d' % i, ', includeOverflows=True'),
          ('{mixin}Min',     'func_caxis%d' % i, ''),
          ('{mixin}Max',     'func_caxis%d' % i, ''),
          ('rebin{MIXIN}By', 'func_caxis%d' % i, ', n, begin=1, end=sys.maxsize'),
          ('rebin{MIXIN}To', 'func_caxis%d' % i, ', edges'),
        ]
        if 'Estimate' not in AOType:
            for f in ['{mixin}Mean', '{mixin}Variance', '{mixin}StdDev', '{mixin}StdErr', '{mixin}RMS']:
                mixin_methods.append( (f, 'func_caxis%d' % i, ', includeOverflows=True') )
        for func, ftype, fargs in mixin_methods:
            cppfunc = func+'('+','.join([ p.split('=')[0] for p in fargs.split(', ') if p ])+')'
            config.set('rtn', config.mkSegment(ftype, method = cppfunc))
            config.append('mixin', config.apply(tmp_axis_mixin, method = config.apply(func), mixinArgs = fargs))
        config.append('mixin', config.apply(tmp_rebin))


    for otype in [ 'add', 'sub' ]:
        config.set('operator',  config.mkSegment('operator'))
        config.set('ioperator', config.mkSegment('ioperator'))
        config.append('mixin', config.apply(tmp_operator_mixin).replace('OTYPE', otype))

    for otype in [ 'eq', 'ne' ]:
        config.set('comparator', config.mkSegment('comparator'))
        config.append('mixin', config.apply(tmp_comparison_mixin).replace('OTYPE', otype))

    config.set('AOMixin', config.mkSegment('mixin')) # inserts empty string if not applicable
    config.set('caxisConfig', config.mkSegment('caxis', delim = ', '))

    config.set('cdefConfig',  config.mkSegment('cdef')) #, delim = '\n\n'))
    config.set('tryExec',     config.mkSegment('tryex'))
    config.set('maskBin',     config.mkSegment('maskBin'))
    config.set('maskSlice',   config.mkSegment('maskSlice'))
    config.set('init2Config', config.mkSegment('init2'))
    config.set('init4Config', config.mkSegment('init4'))
    config.set('edgeMaking',  config.mkSegment('edgeGen'))
    config.set('cloneConfig', config.mkSegment('clone'))
    config.set('globalBin',   config.mkSegment('binGlobal'))
    config.set('division',    config.mkSegment('div'))
    config.set('efficiency',  config.mkSegment('eff'))
    config.set('AO2Scatter',  config.mkSegment('scatter'))
    config.set('AO2Binned',   config.mkSegment('typeRed'))

    for i, func in enumerate([
          '_local2global(localIndices)',
          config.apply('set(idx, cont.{contentPtr}()[0])'),
          config.apply('set(self.localToGlobalIndex(*indices), cont.{contentPtr}()[0])'),
          'reset()',
          'numBins(includeOverflows, includeMaskedBins)',
          'isMasked(index)',
          'isVisible(index)',
          'binDim()',
        ]):
        config.set('func%d' % i, config.mkSegment('func', method = func))

    foot = '## Convenience alias\n{AOType}{dim}D = Binned{AOType}{dim}D' # Histo1D, Profile2D etc.
    if 'Estimate' not in AOType:
        foot += '\n{AOTypeMini}{dim}D = {AOType}{dim}D' # H1D, P2D etc.

    with open(PATH + '/include/generated/Binned%s%dD.pyx' % (AOtype, n), 'w') as f:
        f.write(header)
        f.write(config.apply(base_template))
        f.write(config.apply(foot))

    op_defs.append( '\n\n' + config.mkSegment('merge'))


##############


def make_SNDtemplates(n):

    config = TemplateConfig()

    header = getTemplate('header.dat')
    base_template  = getTemplate('SND.dat')
    mixin_template = getTemplate('SND_MIXIN.dat')

    config.set('dim', n)

    foot = '## Convenience alias\nS{dim}D = Scatter{dim}D'

    with open(PATH + '/include/generated/Scatter%dD.pyx' % n, 'w') as f:
        f.write(header)
        f.write(config.apply(base_template))
        if n < 4:
          for i in range(n):
            config.setMixin(i)
            f.write(config.apply(mixin_template))
        f.write(config.apply(foot))


##############


def make_PNDtemplates(n):

    config = TemplateConfig()

    header = getTemplate('header.dat')
    base_template  = getTemplate('PND.dat')
    mixin_template = getTemplate('PND_MIXIN.dat')

    config.init('val', 'errs', 'errsdn', 'errsup', 'symerrs')

    tmp  = "        return '<Point%dD(" % n
    tmp += ', '.join([ 'v'+str(i)+'=%g' for i in range(1,n+1) ])
    tmp += ")>' % ("
    tmp += ', '.join([ 'self.val('+str(i)+')' for i in range(n) ])
    tmp += ")"

    for i in range(n if n < 4 else 0):
        config.setMixin(i)
        config.append('val', '{mixin}Val')
        config.append('errs', '{mixin}Err')
        config.append('errsdn', '{mixin}ErrDn')
        config.append('errsup', '{mixin}ErrUp')
        config.append('symerrs', '{mixin}ErrDn, {mixin}ErrUp')

    config.set('dim', n)
    config.set('REPR', tmp)

    config.set('valParams', config.mkSegment('val', delim = ', '))
    config.set('errParams', config.mkSegment('errs', delim = ', '))
    config.set('errdnParams', config.mkSegment('errsdn', delim = ', '))
    config.set('errupParams', config.mkSegment('errsup', delim = ', '))
    config.set('symerrParams', config.mkSegment('symerrs', delim = ', '))

    init3Arg = ''
    init3Config = ''
    if n < 4:
        init3Arg = ', self.__init3, self.__init4'
        init3Config  = '    def __init3(self, {valParams}, {symerrParams}):\n'
        init3Config += '        vals = [ {valParams} ]\n'
        init3Config += '        errm = [ {errdnParams} ]\n'
        init3Config += '        errp = [ {errupParams} ]\n'
        init3Config += '        self.__init2(vals, errm, errp)\n\n'
        init3Config += '    def __init4(self, {valParams}, {errParams}):\n'
        init3Config += '        vals = [ {valParams} ]\n'
        init3Config += '        errm = [ {errParams} ]\n'
        init3Config += '        errp = [ {errParams} ]\n'
        init3Config += '        self.__init2(vals, errm, errp)'
    config.set("init3arg", config.apply(init3Arg))
    config.set("init3config", config.apply(init3Config))

    foot = '## Convenience alias\nP{dim}D = Point{dim}D'

    with open(PATH + '/include/generated/Point%dD.pyx' % n, 'w') as f:
        f.write(header)
        f.write(config.apply(base_template))
        for i in range(n if n < 4 else 0):
            config.setMixin(i)
            f.write(config.apply(mixin_template))
        f.write(config.apply(foot))


##############


def make_DBNtemplates(n):

    config = TemplateConfig()

    header = getTemplate('header.dat')
    base_template  = getTemplate('DBN.dat')
    mixin_template = getTemplate('DBN_MIXIN.dat')

    crossTermMaker = ''
    if n > 1:
      crossTermMaker  = '        cdef vector[double] sWcross\n'
      crossTermMaker += '        sWcross = [ float(w) for w in sumWcross ]'

    config.set('dim', n)
    config.set('coords', ', '.join([ config.mixin[i] for i in range(n) ]))
    config.set('floatCoords', ', '.join([ 'float ' + config.mixin[i] for i in range(n) ]))
    config.set('crossTerm', ', sumWcross' if n > 1 else '')
    config.set('cppCrossTerm', ', sWcross' if n > 1 else '')
    config.set('crossTermMaker', crossTermMaker)

    with open(PATH + '/include/generated/Dbn%dD.pyx' % n, 'w') as f:
        f.write(header)
        f.write(config.apply(base_template))
        if n < 4:
          for i in range(n):
            config.setMixin(i)

            tmp = ''
            if i > 0:
              tmp  = '    def crossTerm(self, i, j):\n'
              tmp += '        """sum(weights is * js)"""\n'
              tmp += '        return self.d%dptr().crossTerm(i,j)\n\n' % (i+1)
            config.set('crossTerms', tmp)

            f.write(config.apply(mixin_template))


##############

def make_axis(edge_types):

    config = TemplateConfig()

    header = getTemplate('header.dat')
    base_template  = getTemplate('Axis.dat')

    config.init('init2', 'ptr', 'func', 'func_caxis', 'func_daxis', 'methods')

    tmp_ptr  = '    cdef inline c.Axis[{axisT}]* axis_{axisTmini}_ptr(self) except NULL:\n'
    tmp_ptr += '        return <c.Axis[{axisT}]*> self.ptr()'

    tmp_func  = "        if self.axisT == '{axisTmini}':\n"
    tmp_func += "          return self.axis_{axisTmini}_ptr()."

    tmp = '        cdef vector[{0}] edges_{1}'
    config.append('init2', '\n'.join([ tmp.format(edge, edge[0]) for edge in edge_types.split(',') ]))
    config.append('init2', '        if not len(edges):  edges.append(0.0, 1.0) # assume CAxis is default')

    tmp_init2  = "        {logic} {axisCombi}:\n{edgeMaking}"
    tmp_init2 += "          cutil.set_owned_ptr(self, new c.Axis[{axisT}]"
    tmp_init2 += "({triggeredEdges}))\n"
    tmp_init2 += "          self.axisT = '{axisTmini}'"

    for axis_idx, axisT in enumerate(edge_types.split(',')):
        config.set('axisT', axisT)
        config.set('axisTmini', axisT[0])
        config.set('logic', 'elif' if axis_idx else 'if')
        config.set('triggeredEdges', 'edges_'+axisT[0])
        config.set('axisCombi', "isinstance(edges[0], "+config.pyfunc[axisT[0]]+")")
        config.set('edgeMaking', "          edges_"+axisT[0]+" = list("+config.pyfunc[axisT[0]]+"(x) for x in edges)\n")

        config.append('ptr', tmp_ptr)
        config.append('func', tmp_func, suffix = '{method}')
        config.append('init2', tmp_init2)

        atype = 'caxis' if axisT[0] == 'd' else 'daxis'
        config.append('func_%s' % atype, tmp_func, suffix = '{method}')


    warn = '        print("WARNING: method not supported for this axis type!")\n'
    config.append('func', warn)
    config.append('func_caxis', warn)
    config.append('func_daxis', warn)

    config.set('ptrConfig',  config.mkSegment('ptr'))
    config.set('init2Config', config.mkSegment('init2'))

    for func, funcArgs, ftype in [
          ('numBins', ', includeOverflows=False', 'func'),
          ('type',    '',        'func'),
          ('index',   ', edge',  'func'),
          ('width',   ', index', 'func_caxis'),
          ('max',     ', index', 'func_caxis'),
          ('min',     ', index', 'func_caxis'),
          ('mid',     ', index', 'func_caxis'),
          ('edge',    ', index', 'func_daxis'),
        ]:
        cppfunc = func+'('+','.join([ p.split('=')[0] for p in funcArgs.split(', ') if p ])+')'
        fdef = config.apply('    def {method}(self{methodArgs}):', method = func, methodArgs = funcArgs)
        config.append('methods', fdef)
        config.append('methods', config.mkSegment(ftype, method = cppfunc))
    config.set('methods', config.mkSegment('methods'))

    with open(PATH + '/include/generated/Axis.pyx', 'w') as f:
        f.write(header)
        f.write(config.apply(base_template))


##############

def make_BINtemplates(n):

    config = TemplateConfig()

    header = getTemplate('header.dat')
    base_template  = getTemplate('Bin.dat')

    config.init('mixin', 'methods')
    config.set('dim', n)

    # axis-specific mixin
    for i in range(n if n < 4 else 0):
        config.setMixin(i)
        if n == 1 or n == 2:
            func = 'Len' if n == 1 else 'Area'
            fdef = config.apply('        def d{method}(self):', method = func)
            config.append('methods', fdef)
            config.append('methods', '            return self.dVol()\n')
        for func in [ 'Width', 'Max', 'Min', 'Mid', 'Edge' ]:
            fdef = config.apply('        def {mixin}{method}(self):', method = func)
            config.append('methods', fdef)
            rtn = config.apply('            return self.{method}({MIXINDIM})\n', method = func.lower())
            config.append('methods', rtn)
    config.set('axisMixin', config.mkSegment('methods'))

    with open(PATH + '/include/generated/Bin%dD.pyx' % n, 'w') as f:
        f.write(header)
        f.write(config.apply(base_template))


##############

if __name__ == "__main__":

    print ("Generating Cython input files for template classes...")

    gendir = PATH + "/include/generated"
    if not os.path.exists(gendir):
        os.makedirs(gendir)

    edge_types = 'double,int,string'
    make_axis(edge_types)

    N = 3 # number of dimensions to be considered
    op_defs = [ ]
    for n in range(1, N+1):

        make_BINtemplates(n)
        make_DBNtemplates(n)
        make_PNDtemplates(n)
        make_SNDtemplates(n)
        if n == N:
          make_PNDtemplates(n+1)
          make_SNDtemplates(n+1)

        for AOtype in [ 'Estimate', 'Histo', 'Profile' ]:
            if 'Profile' in AOtype and n > 2:
                continue # would need Dbn4D
            edges = 'double' if n == N and 'Histo' in AOtype else edge_types
            make_BinnedTemplates(AOtype, n, edges, op_defs)

    make_declarations(op_defs)
    make_core()

