# -*- coding: utf-8 -*-

#  Copyright © 2016-2017  B. Clausius <barcc@gmx.de>
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

import re


pyxtypes = {'str': 'const QString &', 'bool': 'cbool'}
cpptypes = {'str': 'const QString &', 'object': 'PyObject *'}
pyxrtypes = {'str': 'QString', 'bool': 'cbool'}
cpprtypes = {'str': 'QString', 'object': 'PyObject *'}

cpptype = lambda t: cpptypes.get(t, t)

def bremove(l, value):
    try:
        l.remove(value)
    except ValueError:
        return False
    else:
        return True
        
def parse_cppclass(line):
    match = re.match(r'^class (\w+)\s*(\((.*?)\))?:$', line, re.ASCII)
    base = match.group(3) if match.group(2) else None
    return match.group(1), base
    
def parse_mods(mods):
    mods = mods.split()
    m_mods = mods.pop(0)
    p_mods = []
    c_mods = []
    for mod in mods:
        if mod in ('with', 'gil', 'nogil'):
            p_mods.append(mod)
        else:
            p_mods.append(mod)
            c_mods.append(mod)
    f_mod, *m_mods = m_mods
    p_mods = ' ' + ' '.join(p_mods) if p_mods else ''
    c_mods = ' ' + ' '.join(c_mods) if c_mods else ''
    return f_mod, m_mods, p_mods, c_mods

    
def parse_pfunc(line):
    import inspect
    def mkpyfunc(line):
        exec(line.rsplit(':', 1)[0]+': pass')
        l = locals()
        del l['line']
        assert len(l) == 1, l
        return list(l.values())[0]
    f = mkpyfunc(line)
    s = inspect.signature(f)
    returntype = 'void' if s.return_annotation == inspect._empty else s.return_annotation
    def parameters(s):
        for i, p in enumerate(s.parameters.values()):
            if i == 0 and p.name in ('self', 'this') and p.annotation == inspect._empty:
                continue
            yield ('' if p.annotation == inspect._empty else p.annotation), p.name
    return returntype, f.__name__, list(parameters(s))
    

class PxMakros:
    def PXDFILE(self, indent, line, lineno, *args):
        if line:
            pxdline = self.pxd_indent + indent + line.rstrip(':') + '    #line {}\n'.format(lineno)
            self.pxdf.write(pxdline)
            if self.makrolineno == 0:
                return True
        
    def HFILE(self, indent, line, lineno, *args):
        if line.strip():
            pxhline = indent + line + '    //line {}\n'.format(lineno)
        else:
            pxhline = '\n'
        self.pxhf.write(pxhline)
        return True
        
    def CFILE(self, indent, line, lineno, *args):
        if line.strip():
            line = indent + line + '    //line {}\n'.format(lineno)
        else:
            line = '\n'
        self.pxcf.write(line)
        return True
        
    def CPPCLASS(self, indent, line, lineno, *args, P=True, D=False, Q=False):
        if not line:
            return
        name, base = parse_cppclass(line)
        if base:
            self.HFILE(indent, 'class {} : public {}'.format(name, base), lineno)
            if P:
                self.PYXFILE('cdef cppclass {} ({}):'.format(name, base))
        else:
            self.HFILE(indent, 'class {}'.format(name), lineno)
            if P:
                self.PYXFILE('cdef cppclass {}:'.format(name))
        if D:
            self.PXDFILE(indent, 'cdef cppclass {}:#'.format(name), lineno)
        self.HFILE(indent, '{', lineno)
        if Q:
            self.HFILE(indent, '    Q_OBJECT', lineno)
            self.HFILE(indent, '', lineno)
        self.HFILE(indent, 'public:', lineno)
        self.namespace = name
        return True
        
    def C_CPPCLASS(self, indent, line, lineno, *args):
        return self.CPPCLASS(indent, line, lineno, *args, P=False)
        
    def QC_CPPCLASS(self, indent, line, lineno, *args):
        return self.CPPCLASS(indent, line, lineno, *args, P=False, Q=True)
        
    def QD_CPPCLASS(self, indent, line, lineno, *args):
        return self.CPPCLASS(indent, line, lineno, *args, D=True, Q=True)
        
    def CPPCLASS_END(self, indent, line, lineno, *args):
        self.namespace = None
        self.HFILE(indent, '};', lineno)
        self.HFILE(indent, '', lineno)
        return True
        
    def FUNC(self, indent, line, lineno, *args):
        def norm_type(t, types):
            t = t if type(t) is str else types.get(t.__name__, t.__name__)
            if t and t[-1].isalnum():
                t += ' '
            return t
        def norm_types(args, types):
            def gett(i, t):
                if type(t) is not int:
                    return t
                assert t == 1
                return gett(i+t, args[i+t][0])
            for i, (t, n) in enumerate(args):
                yield norm_type(gett(i, t), types), n
        argstr = lambda args: ', '.join((t+n) for t,n in args)
        if self.makrolineno == 0:
            self.makrodata = parse_mods(line)
        else:
            f_mod, m_mods, p_mods, c_mods = self.makrodata
            returntype, name, args = parse_pfunc(line)
            preturntype = norm_type(returntype, pyxrtypes)
            creturntype = norm_type(returntype, cpprtypes)
            if name.startswith('slot_'):
                creturntype = 'Q_INVOKABLE ' + creturntype
                name = name.split('_', 1)[1]
            elif name == '__init__':
                preturntype = ''
                creturntype = ''
                name = self.namespace
            if self.namespace is None and preturntype.split(' ')[0] not in ['cdef', 'cpdef']:
                preturntype = 'cdef ' + preturntype
            cname = name if self.namespace is None else self.namespace + '::' + name
            pyxargs = list(norm_types(args, pyxtypes))
            cppargs = list(norm_types(args, cpptypes))
            if f_mod == 'P':
                self.PYXFILE('{}{}({}){}:'.format(preturntype, name, argstr(pyxargs), p_mods))
            elif f_mod == 'H':
                self.HFILE(indent, '{}{}({}){}'.format(creturntype, name, argstr(cppargs), c_mods), lineno)
            elif f_mod == 'C':
                self.CFILE(indent, '{}{}({}){}'.format(creturntype, cname, argstr(cppargs), c_mods), lineno)
            else:
                assert False, f_mod
            if bremove(m_mods, 'H'):
                self.HFILE(indent, '{}{}({}){};'.format(creturntype, name, argstr(cppargs), c_mods), lineno)
            if bremove(m_mods, 'D'):
                self.PXDFILE(indent, '{}{}({}){}'.format(preturntype, name, argstr(pyxargs), p_mods), lineno)
            if bremove(m_mods, 'P'):
                self.PYXFILE('{}{}({}){}'.format(preturntype, name, argstr(pyxargs), p_mods))
            assert not m_mods, m_mods
            return True
            
    def Q_PROPERTY(self, indent, line, lineno, *args):
        if not line:
            return
        match = re.match(r"^prop_(\w+) = '(.*)'$", line.strip(), re.ASCII)
        pname = match.group(1)
        assert pname.isidentifier(), pname
        ptype, *flags = match.group(2).split()
        assert all(f.isupper() for f in flags), flags
        pargs = []
        def pop(flag):
            if flag in flags:
                flags.remove(flag)
                return True
            return False
        if pop('READ'):
            pargs += ['READ', 'get_' + pname]
        if pop('WRITE'):
            pargs += ['WRITE', 'set_' + pname]
        if pop('MEMBER'):
            fname = 'm_' + pname
            pargs += ['MEMBER', fname]
            self.HFILE(indent, '{} {};'.format(ptype, fname), lineno)
        if pop('NOTIFY'):
            fname = pname + '_changed'
            pargs += ['NOTIFY', fname]
            self.PXDFILE(indent, 'void {}() nogil'.format(fname), lineno)
            self.HFILE(indent, 'Q_SIGNAL void {}();'.format(fname), lineno)
        if flags:
            assert False, flags
        self.HFILE(indent, 'Q_PROPERTY ({} {} {})'.format(ptype, pname, ' '.join(pargs)), lineno)
        
    def Q_SIGNAL(self, indent, line, lineno, *args):
        if not line or line[0] == '#':
            return
        match = re.match(r"^(\w+) = pyqtSignal\((.*)\)$", line.strip(), re.ASCII)
        assert match is not None, line
        pname = match.group(1)
        assert pname.isidentifier(), pname
        types = ', '.join(cpptype(t.strip()) for t in match.group(2).split(','))
        self.HFILE(indent, 'Q_SIGNAL void {}({});'.format(pname, types), lineno)
        
    def ACTION(self, indent, line, lineno, *args):
        if not line:
            return
        match = re.match(r"^action_(\w+) = (\w+)$", line.strip(), re.ASCII)
        assert match is not None, line
        name = match.group(1)
        assert name.isidentifier(), name
        func = match.group(2)
        assert func.isidentifier(), func
        self.HFILE(indent, 'Q_INVOKABLE void on_action_{}_triggered();'.format(name), lineno)
        self.PYXFILE('void on_action_{0}_triggered() with gil:  {1}.app.on_action_{0}_triggered()'.format(name, func))
        
    
