#!/usr/bin/env python
# -*- python -*-
import re
import string
import os
import textwrap
import sys
#from docutils import core
class ParseError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

def ExceptionsRewrite(d):
    """renames some ambiguous commands for python.

    for example, mesh_set('pts') and mesh_get(pts) => problem with mesh.pts().
    """
    d = string.replace(d, "MESH:SET('pts'", "MESH:SET('set_pts'")
    d = string.replace(d, "SLICE:SET('pts'", "SLICE:SET('set_pts'")
    d = string.replace(d, "MESHFEM:SET('qdim'", "MESHFEM:SET('set_qdim'")
    d = string.replace(d, "MESHFEM:SET('fem'", "MESHFEM:SET('set_fem'")
    d = string.replace(d, "MESHIM:SET('integ'", "MESHIM:SET('set_integ'")
    d = re.sub("(MESHFEM:SET\('classical[ _]fem')",
               "MESHFEM:SET('set_classical_fem'", d)
    d = re.sub("(MESHFEM:SET\('classical[ _]discontinuous[ _]fem')",
               "MESHFEM:SET('set_classical_discontinuous_fem'", d)
    d = string.replace(d, "MESH:SET('region'", "MESH:SET('set_region'")

    d = string.replace(d, "MDBRICK:SET('param'", "MDBRICK:SET('set_param'")
    d = string.replace(d, "MDBRICK:SET('constraints'", "MDBRICK:SET('set_constraints'")
    d = string.replace(d, "MDBRICK:SET('constraints_rhs'", "MDBRICK:SET('set_constraints_rhs'")
    return d;

def RemoveSpacesInFNames(d):
    """Replace MESH:GET('max pid') with MESH:GET('max_pid')."""
    n=1
    while n:
        (d,n)=re.subn(r"(:[A-Z]+ *\('[a-z_]+) ([a-z_ ]+')", r"\1_\2",d)
    return d

def find_closing_brace(s):
    lev = 0
    cnt = 0
    for c in s:
        if (c == '{'): lev+=1;
        if (c == '}'):
            lev-=1;
            if (lev == -1): return cnt;
        cnt += 1
    raise ParseError, s

def CellToTuples(d):
    while 1:
        s=''
        p = d.find('@CELL{')
        #sys.stderr.write('p=%d\n' % (p,))
        if (p == -1):
            s+=d
            break
        s+=d[:p]+'('; d = d[p+6:];
        p2=find_closing_brace(d)
        #sys.stderr.write(d[:p2]+'| p2=%d\n' % (p2,))
        s+=d[:p2]+')'+d[p2+1:]
        d = s
    return d

def FilterMatlabOrPythonSections(d, what):
    if (what == 'MATLAB'):
        w = 1
    else: w = 0
    markup = ['@MATLAB{', '@PYTHON{']
    ll=d.split(markup[w])
    d = ll[0];
    for l in ll[1:]:
        d += l[(find_closing_brace(l)+1):]
    ll=d.split(markup[1-w])
    d = ll[0];
    for l in ll[1:]:
        p = find_closing_brace(l)        
        d += l[:p] + l[p+1:]
    return d

def SubstituteTypes(d):
    transtypes = { '@dmat':'mat',
                   '@mat':'mat',
                   '@ivec':'ivec',
                   '@dvec':'vec',
                   '@vec':'vec',
                   '@int' :'int',
                   '@real' :'real',
                   '@scalar' :'scalar',
                   '@str' :'string',
                   '@imat':'imat',
                   '@tmesh':'mesh',
                   '@tmim':'meshim',
                   '@tmf':'meshfem',
                   '@tbrick':'mdbrick'}
    for (k,v) in transtypes.items():
        d = string.replace(d, k, v)
    return d


def flush_par(par, state,indent):
    if (state == 1):
        return indent+'* '+par+'\n'
    elif (state == 2):
        return indent+par+'\n'
    elif (state == 3):
        return '<P>'+par+'</P>\n'
    return ''

def WrapParagraphs(d, indent):
    doc = ''
#    ll = [l.rstrip() for l in d.split('\n')]
#    #print ll
#    par = ''
#    in_list = 1
#    in_par = 2
#    in_pre = 3
#    state = 0
#    for l in ll:
#        #sys.stderr.write(('%d: '+l+'\n') % (state));
#        if l.startswith('**'):
#            doc+=flush_par(par,state, indent)
#            doc+=flush_par('** ' + l[2:].strip(),2,indent)
#            state = in_list; par = ''
#        elif l.startswith('*'):
#            doc+=flush_par(par, state,indent)
#            par = l[1:].strip() + ' '
#            state = in_list
#        elif l.startswith(' '):
#            doc+=flush_par(par, state,indent)
#            par = l
#            state = in_pre
#        else:            
#            if (len(l.strip()) == 0):
#                doc+=flush_par(par, state,indent)
#                par = ''
#                state = in_par
#            else:
#                if (state == 0): state = in_par
#                par +=l.strip()+' '
                
#    doc+=flush_par(par,state,indent)
    
    for l in re.split(r'(<[Pp]ar>)', d):        
        if l.startswith('<Par>'):
            doc += textwrap.fill(l[5:],78)
            doc += '\n\n'
        elif l.startswith('<par>'):
            doc += textwrap.fill(l[5:],78)
            doc += '\n'
        else: doc += textwrap.fill(l, 78)
    doc = doc.strip('\n')
    return doc

def PythonRewriteDoc(d):
    d = d.strip('\n')
    d = CellToTuples(d)
    d = RemoveSpacesInFNames(FilterMatlabOrPythonSections(d,'PYTHON'))
    d = ExceptionsRewrite(d)
    trans = {'MESH:INIT':r"Mesh('\1'\2\3)", \
             'MESH:GET':r'Mesh.\1(\3)', \
             'MESH:SET':r'Mesh.\1(\3)', \
             'MESHFEM:GET':r'MeshFem.\1(\3)', \
             'MESHFEM:SET':r'MeshFem.\1(\3)', \
             'MESHIM:GET':r'MeshIm.\1(\3)', \
             'MESHIM:SET':r'MeshIm.\1(\3)', \
             'SLICE:GET':r'Slice.\1(\3)', \
             'SLICE:SET':r'Slice.\1(\3)', \
             'MDBRICK:GET':r'MdBrick.\1(\3)', \
             'MDBRICK:SET':r'MdBrick.\1(\3)', \
             'MDSTATE:GET':r'MdState.\1(\3)', \
             'MDSTATE:SET':r'MdState.\1(\3)', \
             'CVSTRUCT:GET': r"CvStruct.\1(\3)", \
             'GEOTRANS:GET': r"CvStruct.\1(\3)", \
             'INTEG:GET': r"Integ.\1(\3)", \
             'FEM:GET': r"Fem.\1(\3)", \
             'SPMAT:GET': r"Spmat.\1(\3)", \
             'SPMAT:SET': r"Spmat.\1(\3)", \
             'PRECOND:GET': r"Precond.\1(\3)", \
             '::COMPUTE':r'compute_\1(mf, U, \3)', \
             '::ASM':r'asm_\1(\3)'}
    for retry in [1,2,3]: #pour les appels imbriques
        for (k,v) in trans.items():
            d = re.sub(k+r"\( *'([^)']*)' *(,?)([^\)]*)\)",v, d)
    d = string.replace(d,'SLICE:INIT','Slice')
    d = string.replace(d,'MDBRICK:INIT','MdBrick')
    d = string.replace(d,'MDSTATE:INIT','MdState')
    d = string.replace(d,'MESHFEM:INIT','MeshFem')
    d = string.replace(d,'MESHIM:INIT','MeshIm')
    d = string.replace(d,'GEOTRANS:INIT','GeoTrans')
    d = string.replace(d,'INTEG:INIT','Integ')
    d = string.replace(d,'FEM:INIT','Fem')
    d = string.replace(d,'ELTM:INIT','Eltm')
    d = string.replace(d,'SPMAT:INIT','Spmat')
    d = string.replace(d,'PRECOND:INIT','Precond')
    d = string.replace(d,'LEVELSET:INIT','LevelSet')
    d = string.replace(d,'MESHLEVELSET:INIT','MeshLevelSet')
    d = string.replace(d,'::LINSOLVE','linsolve_')
    d = string.replace(d,'::UTIL','util_')

    d = SubstituteTypes(d)
    d = WrapParagraphs(d, "    ")
    return d

def MatlabRewriteDoc(d):
    d = d.strip('\n')
    d = RemoveSpacesInFNames(FilterMatlabOrPythonSections(d,'MATLAB'))
    trans = {'MESH:INIT': r"gf_mesh('\1'\2", \
             'MESH:GET': r"gf_mesh_get(M,'\1'\2", \
             'MESH:SET': r"gf_mesh_set(M,'\1'\2", \
             'MESHFEM:GET': r"gf_mesh_fem_get(MF,'\1\2'", \
             'MESHFEM:SET': r"gf_mesh_fem_set(MF,'\1\2'", \
             'MESHIM:GET': r"gf_mesh_im_get(MIM,'\1\2'", \
             'MESHIM:SET': r"gf_mesh_im_set(MIM,'\1\2'", \
             'SLICE:GET': r"gf_slice_get(SL,'\1'\2", \
             'SLICE:SET': r"gf_slice_set(SL,'\1'\2", \
             'MDBRICK:GET': r"gf_mdbrick_get(b,'\1'\2", \
             'MDBRICK:SET': r"gf_mdbrick_set(b,'\1'\2", \
             'MDSTATE:GET': r"gf_mdstate_get(mds,'\1'\2", \
             'MDSTATE:SET': r"gf_mdstate_set(mds,'\1'\2", \
             'CVSTRUCT:GET': r"gf_cvstruct_get(CVS,'\1'\2", \
             'GEOTRANS:GET': r"gf_geotrans_get(CVS,'\1'\2", \
             'INTEG:GET': r"gf_integ_get(IM,'\1'\2", \
             'FEM:GET': r"gf_fem_get(fe,'\1'\2", \
             'SPMAT:GET': r"gf_spmat_get(M,'\1'\2", \
             'SPMAT:SET': r"gf_spmat_set(M,'\1'\2", \
             'PRECOND:GET': r"gf_precond_get(P,'\1'\2", \
             '::COMPUTE' : r"gf_compute(MF, U,'\1'\2", \
             '::ASM' : r"gf_asm('\1'\2"}
    for (k,v) in trans.items():
        d = re.sub(k+r"\( *'([^)']*)' *(,?)",v, d)
    d = string.replace(d,'SLICE:INIT','gf_slice')
    d = string.replace(d,'MDBRICK:INIT','gf_mdbrick')
    d = string.replace(d,'MDSTATE:INIT','gf_mdstate')
    d = string.replace(d,'MESHFEM:INIT','gf_mesh_fem')
    d = string.replace(d,'MESHIM:INIT','gf_mesh_im')
    d = string.replace(d,'GEOTRANS:INIT','gf_geotrans')
    d = string.replace(d,'INTEG:INIT','gf_integ')
    d = string.replace(d,'FEM:INIT','gf_fem')
    d = string.replace(d,'ELTM:INIT','gf_eltm')
    d = string.replace(d,'SPMAT:INIT','gf_spmat')
    d = string.replace(d,'PRECOND:INIT','gf_precond')
    d = string.replace(d,'LEVELSET:INIT','gf_levelset')
    d = string.replace(d,'MESHLEVELSET:INIT','gf_mesh_levelset')
    d = string.replace(d,'::LINSOLVE','gf_linsolve')
    d = string.replace(d,'::UTIL','gf_util')

    d = SubstituteTypes(d)
    d = string.replace(d, '@CELL','')
    d = WrapParagraphs(d, "% ")
    return d

def SynopsisToArgs(s0):
    s = s0
    if (s.find('=') != -1):
        s = s.split('=',1)[1]
    if (s.find('(') != -1):
        s = s.split('(',1)[1].split(')',1)[0]
    cro1 = s.find('{')
    need_star_args = 0
    if (cro1 != -1):
        s = s[:cro1]
        need_star_args = 1
    if (s.find('...') != -1):
        need_star_args = 1
        s = s[:s.find('...')]
    cro1 = s.find('[')
    if (cro1 != -1):
        cro2 = s[cro1+1:].find('[')
        s = s[:cro2]
    else: cro2 = -1
    if (cro2 != -1):
        need_star_args = 1
    
    ll = [re.sub('@[a-z]*','',l).strip() for l in s.split(',')]
    args = []
    in_opt_arg = 0
    for l0 in ll:
        c = 0
        for l in l0.split('['):
            in_opt_arg += c
            l = l.strip();
            if (len(l)):
                if (l[0] != "'"):
                    if not in_opt_arg:
                        args += [l]
                    else: args += [l+'=None']
                elif (in_opt_arg):
                    cro2 = 1
                    need_star_args = 0
            c = 1
    if (need_star_args == 1):
        args += ['*args']
    #sys.stderr.write('to_args(%s)\n %d,%d -> %s\n' % (s0, need_star_args, cro2, str(args)))
    return args
    
class ExtractDoc:
    def __init__(self, s):
        (self.type, self.synopsis)=s.split('\n',1)[0].split(' ',1)
        m = re.search(r"([A-Z:]+) *\( *'([a-z0-9A-Z _]*)'(.*)\)",self.synopsis)
        if (not m):
            raise ParseError, s
        self.categ= m.group(1) # 'MESH:GET' etc
        self.name = m.group(2) # 'pts', 'optimize_structure',...
        self.args = m.group(3) # ', int dim, ...'

        l=s.split('\n',1)
        if (len(l)==2):
            self.rawdoc = textwrap.dedent(l[1])
        else:
            self.doc = "Undocumented."
    def __repr__(self):
        return "cat='%s' type='%s', name='%s', args='%s'" % \
               (self.categ, self.type,self.name,self.args)

def ExtractDocFromCppFile(fname):
    f=open(fname)
    all=f.readlines()
    
    docs = {}

    sys.stderr.write('reading %s:' % (fname))
    in_doc = 0;
    for l in all:
        if (l.strip().startswith('/*@')):
            in_doc = 1
            docline = l.strip()[3:].strip()+'\n'
        elif (l.find('@*/') != -1):
            
            docline += l[:l.find('@*/')]
            in_doc = 0
            ed = ExtractDoc(docline)
            docs[ed.name] = ed
            sys.stderr.write(' '+ed.name)
        elif (in_doc):
            docline+=l

    sys.stderr.write('\n')
    return docs;

def CommaIf(cond):
    return cond and ", " or ""

def WritePythonFunction(ed):
#    sys.stderr.write('SYNOPSIS: %s\n  -> args=%s\n' % (ed.synopsis,SynopsisToArgs(ed.synopsis)))
    tmp = ExceptionsRewrite('@'+ed.type + ' ' +ed.categ + "('" + ed.name + "')")
    #sys.stderr.write('tmp = %s\n' % tmp)
    name = ExtractDoc(tmp).name
    arg_first_dic = { '::COMPUTE':'mf,U', '::ASM':'', '::LINSOLVE':'', '::UTIL':'' }
    arg_list = ", ".join(SynopsisToArgs(ed.synopsis))
    if (not ed.type == 'FUNC'):        
        
        print '    def %s(self%s%s):' % (string.replace(name,' ','_'), CommaIf(len(arg_list)), arg_list)
        print '       ',
    else:
        arg_first = arg_first_dic[ed.categ]
        print 'def %s(%s%s%s):' % (string.replace(ed.categ,'::','').lower() + '_' + string.replace(name,' ','_'),
                                   arg_first, CommaIf(len(arg_first) and len(arg_list)), arg_list)
        print '   ',

    arg_list = string.replace(arg_list, '=None','')
    d = ed.rawdoc.split('\n');
    while (len(d) and (len(d[0].strip()) == 0)):
        d = d[1:]
    if (len(d)>2 and (len(d[1].strip()) == 0)):
        doc = d[0] + '<Par>Synopsis: %s<par>' % (RemoveSpacesInFNames(ed.synopsis),)
        doc += '\n'.join(d[2:])
    else:
        doc = '\n'.join(d)
    print '"""%s"""' % (PythonRewriteDoc(doc),)
    if (ed.type == 'FUNC'):
        if (len(arg_first)):
            print "    return getfem('%s',%s,'%s'%s%s)" % (string.replace(ed.categ.lower(),'::',''),
                                                           arg_first,name,CommaIf(len(arg_list)), arg_list)
        else:
            print "    return getfem('%s','%s'%s%s)" % (string.replace(ed.categ.lower(),'::',''),name,CommaIf(len(arg_list)), arg_list)
    elif (ed.categ.endswith('GET')):
        print '        return self.get("%s"%s%s)\n' % (name, CommaIf(len(arg_list)), arg_list)
    else:
        print '        return self.set("%s"%s%s)\n' % (ed.name, CommaIf(len(arg_list)), arg_list)

        
def WritePythonSubConstructor(ed):
    if (ed.type != 'TEXT'):
        print PythonRewriteDoc(' * '+ed.synopsis+r'<par>  '+ed.rawdoc)
    else:
        print PythonRewriteDoc(ed.rawdoc)



def WriteMatlabSubFunctionDoc(ed):
    if (ed.type != 'TEXT'):
        d = '* '+MatlabRewriteDoc(ed.synopsis+r'<par>'+ed.rawdoc)
        print '\n'.join(['  '+s for s in d.split('\n')]) + '\n'
    else:
        print MatlabRewriteDoc(ed.rawdoc)

def LookupDoc(what):
    """return the ExtractDoc for what = @RDATTR MESH:GET('nbpts') etc"""
    what=what.strip()
    ed = ExtractDoc(what)
    if (not docs.has_key(ed.categ)):
        sys.stderr.write('Undocumented category : %s\n' % ed.categ)
        raise ParseError, what
    d = docs[ed.categ]
    if (not d.has_key(ed.name)):
        raise ParseError, 'CANNOT FIND KEY for for %s:%s\n' % (ed.categ,ed.name)
    if (d[ed.name].type != ed.type):
        raise ParseError, 'WRONG TYPE : for %s:%s\n  Found %s, expected %s' % (ed.categ,ed.name,ed.type,d[ed.name].type)
    return d[ed.name]


def UpdatePythonDoc():
    all=open(os.path.join(src_dir,'python/getfem.base.py')).readlines()
    currentclass = None;
    for l in all:
        if (l.strip().startswith("class ")):
            print l,
            currentclass = l.strip()[6:].split(':')[0].strip()
            sys.stderr.write("currentclass='%s'\n" % currentclass)
        elif (l.strip().startswith('#@')):
            print '\n'+l,
            WritePythonFunction(LookupDoc(l.strip()[2:]))
        elif l.strip().startswith('@'):            
            WritePythonSubConstructor(LookupDoc(l.strip()[1:]))
        else:
            print l,
def UpdateMatlabDoc(fname):
#    sys.stderr.write("$$$update: %s\n" % (fname));
    Cfname=string.replace(fname,'.m','.cc')
    all=open(os.path.join(src_dir,Cfname)).read().split('/*MLABCOM',1)[1].split('MLABCOM*/')[0].strip()
    for l in all.split('\n'):
        if (l.strip().startswith('@')):
            ed = LookupDoc(l.strip()[1:])
            WriteMatlabSubFunctionDoc(ed)
        else:
            print l

if (len(sys.argv) < 2):
    sys.stderr.write("give the filename to update\n")
    exit

if (len(sys.argv) == 3):
    src_dir = sys.argv[2]
else:
    src_dir = ''
        
fmap = {'gf_mesh.cc':'MESH:INIT',
        'gf_mesh_get.cc':'MESH:GET',
        'gf_mesh_set.cc':'MESH:SET',
        'gf_slice.cc':'SLICE:INIT',
        'gf_slice_get.cc':'SLICE:GET',
        'gf_slice_set.cc':'SLICE:SET',
        'gf_mesh_fem.cc':'MESHFEM:INIT',
        'gf_mesh_fem_get.cc':'MESHFEM:GET',
        'gf_mesh_fem_set.cc':'MESHFEM:SET',
        'gf_mesh_im.cc':'MESHIM:INIT',
        'gf_mesh_im_get.cc':'MESHIM:GET',
        'gf_mesh_im_set.cc':'MESHIM:SET',
        'gf_fem.cc':'FEM:INIT',
        'gf_fem_get.cc':'FEM:GET',
        'gf_eltm.cc':'ELTM:INIT',
        'gf_cvstruct_get.cc':'CVSTRUCT:GET',
        'gf_geotrans.cc':'GEOTRANS:INIT',
        'gf_geotrans_get.cc':'GEOTRANS:GET',
        'gf_integ.cc':'INTEG:INIT',
        'gf_integ_get.cc':'INTEG:GET',
        'gf_mdbrick.cc':'MDBRICK:INIT',
        'gf_mdbrick_get.cc':'MDBRICK:GET',
        'gf_mdbrick_set.cc':'MDBRICK:SET',
        'gf_mdstate.cc':'MDSTATE:INIT',
        'gf_mdstate_get.cc':'MDSTATE:GET',
        'gf_mdstate_set.cc':'MDSTATE:SET',
        'gf_spmat.cc':'SPMAT:INIT',
        'gf_spmat_get.cc':'SPMAT:GET',
        'gf_spmat_set.cc':'SPMAT:SET',
        'gf_precond.cc':'PRECOND:INIT',
        'gf_precond_get.cc':'PRECOND:GET',
        'gf_levelset.cc':'LEVELSET:INIT',
        'gf_levelset_get.cc':'LEVELSET:GET',
        'gf_levelset_set.cc':'LEVELSET:SET',
        'gf_mesh_levelset.cc':'MESHLEVELSET:INIT',
        'gf_mesh_levelset_get.cc':'MESHLEVELSET:GET',
        'gf_mesh_levelset_set.cc':'MESHLEVELSET:SET',
        'gf_asm.cc':'::ASM',
        'gf_linsolve.cc':'::LINSOLVE',
        'gf_util.cc':'::UTIL',
        'gf_compute.cc':'::COMPUTE'}
docs = {}
for (fname,cat) in fmap.items():
    docs[cat] = ExtractDocFromCppFile(os.path.join(src_dir,fname))

if (len(sys.argv) < 2):
    sys.stderr.write("give the filename to update, or just 'python'\n")
else:
    if (len(sys.argv) == 3):
        src_dir = sys.argv[2]
    else:
        src_dir = ''

    
    if (sys.argv[1] == 'python'):
        UpdatePythonDoc()
    elif (sys.argv[1].endswith('.m')):
        UpdateMatlabDoc(sys.argv[1])
    else:
        sys.stderr.write("don't know file "+sys.argv[1]+"\n")

