#!/usr/bin/env python

import sys
from string import Template

class argument:
  def __init__(self, fields):
    self.ty = fields[0]
    self.nm = fields[1]
    self.flags = ""
    self.init = None

    if len(fields) > 2:
      if fields[2][0] == '/':
        self.flags = fields[2][1:].split(",")
      else:
        self.init = fields[2]

api = []
for l in open("%s/api" % sys.argv[1]):
  if l[0] == '#':
    continue
  l = l.rstrip()
  if (not l.startswith(' ')) and ('/' in l):
    (l, flags) = l.split('/')
  else:
    flags = ""
  f = l.split()
  if len(f) != 0:
    if l[0] != ' ':
      if len(f) > 1:
        ty = f[1]
      else:
        ty = None
      api.append((f[0], [], ty, flags))
    else:
      api[-1][1].append(argument(f))

# Validation: check that any optional arguments are last
had_error = False
for (f, args, ty, flags) in api:
    has_init = [(a.init != None) for a in args if not 'O' in a.flags]
    if True in has_init and not all(has_init[has_init.index(True):]):
        print 'Error in definition for "%s", optional arguments must be last' % f
        had_error = True

if had_error:
    sys.exit(1)

def cname(n):
  if n.startswith("CV"):
    return '_' + n
  elif n[0].isdigit():
    return '_' + n
  else:
    return n

# RHS is how the aggregate gets expanded in the C call
aggregate = {
  'pts_npts_contours' :  '!.pts,!.npts,!.contours',
  'cvarr_count' :        '!.cvarr,!.count',
  'cvarr_plane_count' :  '!.cvarr,!.count',
  'floats' :             '!.f',
  'ints' :               '!.i',
  'ints0' :              '!.i',
  'CvPoints' :           '!.p,!.count',
  'CvPoint2D32fs' :      '!.p,!.count',
  'CvPoint3D32fs' :      '!.p,!.count',
  'cvarrseq' :           '!.seq',
  'CvArrs' :             '!.ims',
  'IplImages' :          '!.ims',
  'intpair' :            '!.pairs,!.count',
  'cvpoint2d32f_count' : '!.points,&!.count'
}
conversion_types = [
'char',
'CvArr',
'CvArrSeq',
'CvBox2D', # '((ff)(ff)f)',
'CvBox2D*',
'CvCapture*',
'CvStereoBMState*',
'CvStereoGCState*',
'CvKalman*',
'CvVideoWriter*',
'CvContourTree*',
'CvFont',
'CvFont*',
'CvHaarClassifierCascade*',
'CvHistogram',
'CvMat',
'CvMatND',
'CvMemStorage',
'CvMoments',
'CvMoments*',
'CvNextEdgeType',
'CvPoint',
'CvPoint*',
'CvPoint2D32f', # '(ff)',
'CvPoint2D32f*',
'CvPoint3D32f*',
'CvPoint2D64f',
'CvPOSITObject*',
'CvRect',
'CvRect*',
'CvRNG*',
'CvScalar',
'CvSeq',
'CvSeqOfCvConvexityDefect',
'CvSize',
'CvSlice',
'CvStarDetectorParams',
'CvSubdiv2D*',
'CvSubdiv2DEdge',
'CvTermCriteria',
'generic',
'IplConvKernel*',
'IplImage',
'PyObject*',
'PyCallableObject*'
]

def safename(s):
  return s.replace('*', 'PTR').replace('[', '_').replace(']', '_')

def has_optional(al):
    """ return true if any argument is optional """
    return any([a.init for a in al])

def gen(name, args, ty, flags):
  yield ""
  if has_optional(args):
      yield "static PyObject *pycv%s(PyObject *self, PyObject *args, PyObject *kw)" % cname(name)
  else:
      yield "static PyObject *pycv%s(PyObject *self, PyObject *args)" % cname(name)
  if 'doconly' in flags:
    yield ";"
  else:
    yield "{"

    destinations = []
    for a in args:
      remap = {
       'CvArr' : 'CvArr*',
       'CvMat' : 'CvMat*',
       'CvMatND' : 'CvMatND*',
       'IplImage' : 'IplImage*',
       'CvMemStorage' : 'CvMemStorage*',
       'CvHistogram':'CvHistogram*',
       'CvSeq':'CvSeq*',
       'CvHaarClassifierCascade' : 'CvHaarClassifierCascade*'
      }
      ctype = remap.get(a.ty, a.ty)
      if a.init:
        init = " = %s" % a.init
      else:
        init = ''
      yield "  %s %s%s;" % (ctype, a.nm, init)
      if 'O' in a.flags:
        continue
      if a.ty in (conversion_types + aggregate.keys()):
        yield '  PyObject *pyobj_%s = NULL;' % (a.nm)
        destinations.append('&pyobj_%s' % (a.nm))
      elif a.ty in [ 'CvPoint2D32f' ]:
        destinations.append('&%s.x, &%s.y' % (a.nm, a.nm))
      elif a.ty in [ 'CvTermCriteria' ]:
        destinations.append('&%s.type, &%s.max_iter, &%s.epsilon' % ((a.nm,)*3))
      elif a.ty in [ 'CvSURFParams' ]:
        destinations.append('&%s.extended, &%s.hessianThreshold, &%s.nOctaves, &%s.nOctaveLayers' % ((a.nm,)*4))
      elif a.nm in [ 'CvBox2D' ]:
        s = ", ".join([('&' + a.nm +'.' + fld) for fld in [ 'center.x', 'center.y', 'size.width', 'size.height', 'angle' ] ])
        destinations.append(s)
      else:
        destinations.append('&%s' % a.nm)
    fmap = {
      'CvSURFParams' : '(idii)',
      'double' : 'd',
      'float' : 'f',
      'int' : 'i',
      'int64' : 'L',
      'char*' : 's',
    }
    for k in (conversion_types + aggregate.keys()):
      fmap[k] = 'O'
    in_args = [ a for a in args if not 'O' in a.flags ]
    fmt0 = "".join([ fmap[a.ty] for a in in_args if not a.init])
    fmt1 = "".join([ fmap[a.ty] for a in in_args if a.init])

    yield ''
    if len(fmt0 + fmt1) > 0:
      if len(fmt1) > 0:
        yield '  const char *keywords[] = { %s };' % (", ".join([ '"%s"' % arg.nm for arg in args if not 'O' in arg.flags ] + ['NULL']))
        yield '  if (!PyArg_ParseTupleAndKeywords(args, kw, "%s|%s", %s))' % (fmt0, fmt1, ", ".join(['(char**)keywords'] + destinations))
        if '(' in (fmt0 + fmt1):
          print "Tuple with kwargs is not allowed, function", name
          sys.exit(1)
      else:
        yield '  if (!PyArg_ParseTuple(args, "%s", %s))' % (fmt0, ", ".join(destinations))
      yield '    return NULL;'

    # Do the conversions:
    for a in args:
      joinwith = [f[2:] for f in a.flags if f.startswith("J:")]
      if len(joinwith) > 0:
        yield 'preShareData(%s, &%s);' % (joinwith[0], a.nm)
      if 'O' in a.flags:
        continue
      if a.ty in (conversion_types + aggregate.keys()):
        if a.init:
          pred = '(pyobj_%s != NULL) && ' % a.nm
        else:
          pred = ''
        yield '  if (%s!convert_to_%s(pyobj_%s, &%s, "%s")) return NULL;' % (pred, safename(a.ty), a.nm, a.nm, a.nm)

    yield '#ifdef CVPY_VALIDATE_%s' % name
    yield 'CVPY_VALIDATE_%s();' % name
    yield '#endif'

    def invokename(a):
      if 'K' in a.flags:
        prefix = "(const CvArr **)"
      elif 'O' in a.flags and not 'A' in a.flags:
        prefix = "&"
      else:
        prefix = ""
      if a.ty in aggregate:
        return prefix + aggregate[a.ty].replace('!', a.nm)
      else:
        return prefix + a.nm

    def funcname(s):
      # The name by which the function is called, in C
      if s.startswith("CV"):
        return s
      else:
        return "cv" + s
    tocall = '%s(%s)' % (funcname(name), ", ".join(invokename(a) for a in args))
    if 'stub' in flags:
      yield '  return stub%s(%s);' % (name, ", ".join(invokename(a) for a in args))
    elif ty == None:
      yield '  ERRWRAP(%s);' % tocall
      yield '  Py_RETURN_NONE;'
    else:
      Rtypes = [
        'int',
        'int64',
        'double',
        'CvCapture*',
        'CvVideoWriter*',
        'CvPOSITObject*',
        'CvScalar',
        'CvSize',
        'CvRect',
        'CvSeq*',
        'CvBox2D',
        'CvSeqOfCvAvgComp*',
        'CvSeqOfCvConvexityDefect*',
        'CvSeqOfCvStarKeypoint*',
        'CvSeqOfCvSURFPoint*',
        'CvSeqOfCvSURFDescriptor*',
        'CvContourTree*',
        'IplConvKernel*',
        'IplImage*',
        'CvMat*',
        'constCvMat*',
        'ROCvMat*',
        'CvMatND*',
        'CvPoint2D32f_4',
        'CvRNG',
        'CvSubdiv2D*',
        'CvSubdiv2DPoint*',
        'CvSubdiv2DEdge',
        'ROIplImage*',
        'CvStereoBMState*',
        'CvStereoGCState*',
        'CvKalman*',
        'float',
        'generic',
        'unsigned' ]

      if ty in Rtypes:
        yield '  %s r;' % (ty)
        yield '  ERRWRAP(r = %s);' % (tocall)
        yield '  return FROM_%s(r);' % safename(ty)
      else:
        all_returns = ty.split(",")
        return_value_from_call = len(set(Rtypes) & set(all_returns)) != 0
        if return_value_from_call:
          yield '  %s r;' % list(set(Rtypes) & set(all_returns))[0]
          yield '  ERRWRAP(r = %s);' % (tocall)
        else:
          yield '  ERRWRAP(%s);' % (tocall)
        typed = dict([ (a.nm,a.ty) for a in args])
        for i in range(len(all_returns)):
          if all_returns[i] in Rtypes:
            typed['r'] = all_returns[i]
            all_returns[i] = "r"
        if len(all_returns) == 1:
          af = dict([ (a.nm,a.flags) for a in args])
          joinwith = [f[2:] for f in af.get(all_returns[0], []) if f.startswith("J:")]
          if len(joinwith) > 0:
              yield '  return shareData(pyobj_%s, %s, %s);' % (joinwith[0], joinwith[0], all_returns[0])
          else:
              yield '  return FROM_%s(%s);' % (safename(typed[all_returns[0]]), all_returns[0])
        else:
          yield '  return Py_BuildValue("%s", %s);' % ("N" * len(all_returns), ", ".join(["FROM_%s(%s)" % (safename(typed[n]), n) for n in all_returns]))

    yield '}'

gen_c = [ open("generated%d.i" % i, "w") for i in range(5) ]

print "Generated %d functions" % len(api)
for nm,args,ty,flags in sorted(api):

  # Figure out docstring into ds_*
  ds_args = []
  mandatory = [a.nm for a in args if not ('O' in a.flags) and not a.init]
  optional = [a.nm for a in args if not ('O' in a.flags) and a.init]
  ds_args = ", ".join(mandatory)
  def o2s(o):
    if o == []:
        return ""
    else:
        return ' [, %s%s]' % (o[0], o2s(o[1:]))
  ds_args += o2s(optional)

  ds = "%s(%s) -> %s" % (nm, ds_args, str(ty))
  #print ds

  if has_optional(args):
      entry = '{"%%s", (PyCFunction)pycv%s, METH_KEYWORDS, "%s"},' % (cname(nm), ds)
  else:
      entry = '{"%%s", pycv%s, METH_VARARGS, "%s"},' % (cname(nm), ds)
  print >>gen_c[1], entry % (nm)
  if nm.startswith('CV_'):
    print >>gen_c[1], entry % (nm[3:])
  for l in gen(nm,args,ty,flags):
    print >>gen_c[0], l

for l in open("%s/defs" % sys.argv[1]):
  print >>gen_c[2], "PUBLISH(%s);" % l.split()[1]

########################################################################
# Generated objects.
########################################################################

# gen_c[3] is the code, gen_c[4] initializers

gensimple = Template("""
/*
  ${cvtype} is the OpenCV C struct
  ${ourname}_t is the Python object
*/

struct ${ourname}_t {
  PyObject_HEAD
  ${cvtype} v;
};

static PyObject *${ourname}_repr(PyObject *self)
{
  ${ourname}_t *p = (${ourname}_t*)self;
  char str[1000];
  sprintf(str, "<${ourname} %p>", p);
  return PyString_FromString(str);
}

${getset_funcs}

static PyGetSetDef ${ourname}_getseters[] = {

  ${getset_inits}
  {NULL}  /* Sentinel */
};

static PyTypeObject ${ourname}_Type = {
  PyObject_HEAD_INIT(&PyType_Type)
  0,                                      /*size*/
  MODULESTR".${ourname}",              /*name*/
  sizeof(${ourname}_t),                /*basicsize*/
};

static void ${ourname}_specials(void)
{
  ${ourname}_Type.tp_repr = ${ourname}_repr;
  ${ourname}_Type.tp_getset = ${ourname}_getseters;
}

static PyObject *FROM_${cvtype}(${cvtype} r)
{
  ${ourname}_t *m = PyObject_NEW(${ourname}_t, &${ourname}_Type);
  m->v = r;
  return (PyObject*)m;
}

static int convert_to_${cvtype}PTR(PyObject *o, ${cvtype}** dst, const char *name = "no_name")
{
  ${allownull}
  if (PyType_IsSubtype(o->ob_type, &${ourname}_Type)) {
    *dst = &(((${ourname}_t*)o)->v);
    return 1;
  } else {
    (*dst) = (${cvtype}*)NULL;
    return failmsg("Expected ${cvtype} for argument '%s'", name);
  }
}

""")

genptr = Template("""
/*
  ${cvtype} is the OpenCV C struct
  ${ourname}_t is the Python object
*/

struct ${ourname}_t {
  PyObject_HEAD
  ${cvtype} *v;
};

static void ${ourname}_dealloc(PyObject *self)
{
  ${ourname}_t *p = (${ourname}_t*)self;
  cvRelease${ourname}(&p->v);
  PyObject_Del(self);
}

static PyObject *${ourname}_repr(PyObject *self)
{
  ${ourname}_t *p = (${ourname}_t*)self;
  char str[1000];
  sprintf(str, "<${ourname} %p>", p);
  return PyString_FromString(str);
}

${getset_funcs}

static PyGetSetDef ${ourname}_getseters[] = {

  ${getset_inits}
  {NULL}  /* Sentinel */
};

static PyTypeObject ${ourname}_Type = {
  PyObject_HEAD_INIT(&PyType_Type)
  0,                                      /*size*/
  MODULESTR".${ourname}",              /*name*/
  sizeof(${ourname}_t),                /*basicsize*/
};

static void ${ourname}_specials(void)
{
  ${ourname}_Type.tp_dealloc = ${ourname}_dealloc;
  ${ourname}_Type.tp_repr = ${ourname}_repr;
  ${ourname}_Type.tp_getset = ${ourname}_getseters;
}

static PyObject *FROM_${cvtype}PTR(${cvtype} *r)
{
  ${ourname}_t *m = PyObject_NEW(${ourname}_t, &${ourname}_Type);
  m->v = r;
  return (PyObject*)m;
}

static int convert_to_${cvtype}PTR(PyObject *o, ${cvtype}** dst, const char *name = "no_name")
{
  ${allownull}
  if (PyType_IsSubtype(o->ob_type, &${ourname}_Type)) {
    *dst = ((${ourname}_t*)o)->v;
    return 1;
  } else {
    (*dst) = (${cvtype}*)NULL;
    return failmsg("Expected ${cvtype} for argument '%s'", name);
  }
}

""")

getset_func_template = Template("""
static PyObject *${ourname}_get_${member}(${ourname}_t *p, void *closure)
{
  return ${rconverter}(p->v${accessor}${member});
}

static int ${ourname}_set_${member}(${ourname}_t *p, PyObject *value, void *closure)
{
  if (value == NULL) {
    PyErr_SetString(PyExc_TypeError, "Cannot delete the ${member} attribute");
    return -1;
  }

  if (! ${checker}(value)) {
    PyErr_SetString(PyExc_TypeError, "The ${member} attribute value must be a ${typename}");
    return -1;
  }

  p->v${accessor}${member} = ${converter}(value);
  return 0;
}

""")

getset_init_template = Template("""
  {(char*)"${member}", (getter)${ourname}_get_${member}, (setter)${ourname}_set_${member}, (char*)"${member}", NULL},
""")

objects = [
    ( 'IplConvKernel', ['allownull'], {
        "nCols" : 'i',
        "nRows" : 'i',
        "anchorX" : 'i',
        "anchorY" : 'i',
    }),
    ( 'CvCapture', [], {}),
    ( 'CvHaarClassifierCascade', [], {}),
    ( 'CvPOSITObject', [], {}),
    ( 'CvVideoWriter', [], {}),
    ( 'CvStereoBMState', [], {
        "preFilterType" : 'i',
        "preFilterSize" : 'i',
        "preFilterCap" : 'i',
        "SADWindowSize" : 'i',
        "minDisparity" : 'i',
        "numberOfDisparities" : 'i',
        "textureThreshold" : 'i',
        "uniquenessRatio" : 'i',
        "speckleWindowSize" : 'i',
        "speckleRange" : 'i',
    }),
    ( 'CvStereoGCState', [], {
        "Ithreshold" : 'i',
        "interactionRadius" : 'i',
        "K" : 'f',
        "lambda" : 'f',
        "lambda1" : 'f',
        "lambda2" : 'f',
        "occlusionCost" : 'i',
        "minDisparity" : 'i',
        "numberOfDisparities" : 'i',
        "maxIters" : 'i',
    }),
    ( 'CvKalman', [], {
        "MP" : 'i',
        "DP" : 'i',
        "CP" : 'i',
        "state_pre" : 'mr',
        "state_post" : 'mr',
        "transition_matrix" : 'mr',
        "control_matrix" : 'mr',
        "measurement_matrix" : 'mr',
        "control_matrix" : 'mr',
        "process_noise_cov" : 'mr',
        "measurement_noise_cov" : 'mr',
        "error_cov_pre" : 'mr',
        "gain" : 'mr',
        "error_cov_post" : 'mr',
    }),
    ( 'CvMoments', ['copy'], {
        "m00" : 'f',
        "m10" : 'f',
        "m01" : 'f',
        "m20" : 'f',
        "m11" : 'f',
        "m02" : 'f',
        "m30" : 'f',
        "m21" : 'f',
        "m12" : 'f',
        "m03" : 'f',
        "mu20" : 'f',
        "mu11" : 'f',
        "mu02" : 'f',
        "mu30" : 'f',
        "mu21" : 'f',
        "mu12" : 'f',
        "mu03" : 'f',
        "inv_sqrt_m00" : 'f',
    }),
]

checkers = {
    'i' : 'PyNumber_Check',
    'f' : 'PyNumber_Check',
    'm' : 'is_cvmat',
    'mr' : 'is_cvmat'
}
# Python -> C
converters = {
    'i' : 'PyInt_AsLong',
    'f' : 'PyFloat_AsDouble',
    'm' : 'PyCvMat_AsCvMat',
    'mr' : 'PyCvMat_AsCvMat'
}
# C -> Python
rconverters = {
    'i' : 'PyInt_FromLong',
    'f' : 'PyFloat_FromDouble',
    'm' : 'FROM_CvMat',
    'mr' : 'FROM_ROCvMatPTR'
}
# Human-readable type names
typenames = {
    'i' : 'integer',
    'f' : 'float',
    'm' : 'list of CvMat',
    'mr' : 'list of CvMat',
}

for (t, flags, members) in objects:
    map = {'cvtype' : t,
           'ourname' : t.replace('Cv', '')}
    # gsf is all the generated code for the member accessors
    if 'copy' in flags:
        a = '.'
    else:
        a = '->'
    gsf = "".join([getset_func_template.substitute(map, accessor = a, member = m, checker = checkers[t], converter = converters[t], rconverter = rconverters[t], typename = typenames[t]) for (m, t) in members.items()])
    # gsi is the generated code for the initializer for each accessor
    gsi = "".join([getset_init_template.substitute(map, member = m) for (m, t) in members.items()])
    # s is the template that pulls everything together
    if 'allownull' in flags:
        nullcode = """if (o == Py_None) { *dst = (%s*)NULL; return 1; }""" % map['cvtype']
    else:
        nullcode = ""
    if 'copy' in flags:
        print >>gen_c[3], gensimple.substitute(map, getset_funcs = gsf, getset_inits = gsi, allownull = nullcode)
    else:
        print >>gen_c[3], genptr.substitute(map, getset_funcs = gsf, getset_inits = gsi, allownull = nullcode)
    print >>gen_c[4], "MKTYPE(%s);" % map['ourname']

for f in gen_c:
  f.close()
