#!/usr/bin/python3
# -*- coding: utf-8 -*-

#  Copyright © 2012-2015  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 sys
from os.path import basename, join, isfile, isdir
import re


vendors = 'ARB NV NVX ATI 3DLABS SUN SGI SGIX SGIS INTEL 3DFX IBM MESA GREMEDY OML OES PGI I3D INGR MTX'.split()
vendors += 'KHR AMD APPLE EXT HP MESAX SUNX WIN'.split()

def parse_lineno(headers):
    hlines = {}
    for unused_abspath, relpath, source in headers:
        glversion = '0.0'
        lines = []
        for lineno, match in enumerate(re.finditer(r'^[^\n]*$', source, re.MULTILINE | re.DOTALL)):
            posa = match.start()
            pose = match.end()
            line = match.group()
            match = re.match(r'\s*#\s*define\s+GL_VERSION_([0-9_]*).*', line)
            if match is not None:
                glversion = match.group(1).replace('_', '.')
                if glversion in ['1.0', '1.1', '1.2']:
                    glversion = '1.3'
            lines.append((posa, pose, lineno, glversion))
        hlines[relpath] = lines
    return hlines
    
def find_lineno(lines, pos):
    for posa, pose, lineno, glversion in lines:
        assert pos >= posa
        if pos < pose:
            return lineno, glversion
    assert False
    
def convert_symbols_define(headers, code, symbols, hlines):
    for abspath, relpath, source in headers:
        yield '\n\n# from {}:\n'.format(abspath)
        yield "cdef extern from '{}':".format(relpath)
        for match in re.finditer(r'^\#define[ \t]+(GL_\w+).*?$', source, re.MULTILINE | re.DOTALL):
            sym = match.group(1)
            ssym = sym.split('_')
            if code and re.search(r'\b%s\b' % re.escape(sym), code) is None:
                continue
            if sym.upper() != sym or ssym[-1] in vendors:
                continue
            unused_lineno, glversion = find_lineno(hlines[relpath], match.start())
            if (glversion, sym) not in symbols:
                symbols.append((glversion, sym))
                print('.', end='', file=sys.stderr, flush=True)
                yield match.expand(r'    enum: \1')
    print(file=sys.stderr)
                
def convert_symbols_typedef(headers, code, symbols, hlines):
    for abspath, relpath, source in headers:
        yield '\n\n# from {}:\n'.format(abspath)
        for match in re.finditer(r'^(typedef(\s+(\w+))*);.*?$', source, re.MULTILINE | re.DOTALL):
            mg1 = match.group(1)
            mg1 = mg1.replace('khronos_float_t', 'float')
            mg1 = mg1.replace('khronos_', '')
            if not mg1.endswith(('_t', '64')) and not (mg1[-2:].isalpha() and mg1[-2:].isupper()):
                sym = match.group(3)
                unused_lineno, glversion = find_lineno(hlines[relpath], match.start())
                if (glversion, sym) not in symbols:
                    if code and re.search(r'\b%s\b' % re.escape(sym), code) is None:
                        continue
                    symbols.append((glversion, sym))
                    print('.', end='', file=sys.stderr, flush=True)
                    yield 'c'+mg1
    print(file=sys.stderr)
            
def const_types(types, code):
    yield '\n\ncdef extern from *:'
    for type in types.split():
        consttype = 'const_{0}_ptr'.format(type)
        if code and re.search(r'\b%s\b' % re.escape(consttype), code) is None:
            continue
        print('.', end='', file=sys.stderr, flush=True)
        yield '    ctypedef {0}* {1} "const {0}*"'.format(type, consttype)
    print(file=sys.stderr)
                           
def convert_symbols_functions(headers, code, symbols, hlines):
    for abspath, relpath, source in headers:
        yield '\n\n# from {} with:\n'.format(abspath)
        yield "cdef extern from '{}':".format(relpath)
        for match in re.finditer(
                r'^(?:GLAPI|GL_APICALL)([\s\w*]*?)(?:GLAPIENTRY|APIENTRY|GL_APIENTRY)([^(]*)\(([^)]*)\);(.*?)$',
                source, re.MULTILINE | re.DOTALL):
            mg2s2 = match.group(2).strip()[-2:]
            if mg2s2.isalpha() and mg2s2.isupper():
                continue
            for mgf in (match.group(1).find, match.group(3).find):
                if not mgf('64') == mgf('GLsync') == mgf('GLDEBUGPROC') == -1:
                    break
            else:
                if match.group(3).strip() == 'void':
                    template = r'    cdef\1\2()\4'
                else:
                    template = r'    cdef\1\2(\3)\4'
                sym = match.group(2).strip()
                unused_lineno, glversion = find_lineno(hlines[relpath], match.start())
                if (glversion, sym) not in symbols:
                    if code and re.search(r'\b%s\b' % re.escape(sym), code) is None:
                        continue
                    symbols.append((glversion, sym))
                    print('.', end='', file=sys.stderr, flush=True)
                    yield match.expand(template).replace('const ', '').replace('const*', '*') \
                                            .replace(' in,', ' in_,').replace('/*', '#/*')
    print(file=sys.stderr)
    
def convert_symbols(headers, code, print_symbols=False):
    yield '\nfrom libc.stddef cimport ptrdiff_t'
    yield 'from libc.stdint cimport int32_t, intptr_t, int8_t, uint8_t'
    
    code = '\n'.join(c for abspath, relpath, c in code)
    symbols = []
    
    hlines = parse_lineno(headers)
    
    yield from convert_symbols_define(headers, code, symbols, hlines)
    yield from convert_symbols_typedef(headers, code, symbols, hlines)
    yield from const_types('GLubyte GLboolean GLvoid GLchar GLfloat GLint GLshort '
                           'GLbyte GLuint GLushort GLclampf GLsizei GLenum void', code)
    yield from convert_symbols_functions(headers, code, symbols, hlines)
                           
    assert len(symbols) == len(set(symbols))
    assert len([s for v,s in symbols]) == len(set(s for v,s in symbols))
    symbols = sorted(symbols)
    yield '\n# GL version {} needed\n'.format(symbols[-1][0])
    
    if print_symbols:
        for glversion, sym in symbols:
            print('{}  {}'.format(glversion, sym))
                
def read_url(url, filename):
    import urllib.request
    text = urllib.request.urlopen(url).read().decode('utf-8')
    return [(url, filename, text)]
    
def read_files(path, filenames):
    code = []
    for filename in filenames:
        absfilename = join(path, filename) if isdir(path) else path
        with open(absfilename, 'rt', encoding='utf-8') as file:
            code.append((absfilename, filename, file.read()))
    return code
    
def create_pxd(headers, outfilename, code, **kwargs):
    with open(outfilename, 'wt', encoding='utf-8') as file:
        print('# {}'.format(outfilename), file=file)
        print('# generated with:', *sys.argv, file=file)
        for token in convert_symbols(headers, code, **kwargs):
            print(token, file=file)
            
def usage(status):
    print('usage:\n'
          '{0} [-h|--help]\n'
          '{0} [-s] HEADER PXDFILE [FILENAMES]\n'
          '{0} update PXDFILE\n'
          'HEADER is gl, gles2, gles3, a filename or a url.\n'
          'If FILENAMES are given, the generated gl.pxd contains only symbols found in the files.'
          .format(basename(sys.argv[0])))
    sys.exit(status)
    
def main():
    def parse_options():
        args = sys.argv[1:]
        if not args or args[0] in ['-h', '--help']:
            usage(0)
        kwargs = {}
        if args[0] == '-s':
            kwargs['print_symbols'] = True
            args = args[1:]
        return args, kwargs
    args, kwargs = parse_options()
    if len(args) < 2:
        print('wrong number of arguments')
        usage(1)
    pxdfile = args[1]
    if args[0] == 'update':
        import shlex
        args = re.search(r'^# generated with: (.*)$', read_files(pxdfile, [None])[0][-1], re.MULTILINE).group(1)
        sys.argv[1:] = shlex.split(args)[1:]
        sys.argv[2] = pxdfile
        args, unused_kwargs = parse_options()
    code = read_files('.', args[2:])
    includedir = '/usr/include'
    if args[0] == 'gl':
        headers = read_files(includedir, ['GL/gl.h', 'GL/glext.h'])
        create_pxd(headers, pxdfile, code, **kwargs)
    elif args[0] == 'gles2':
        headers = read_files(includedir, ['GLES2/gl2.h', 'GLES2/gl2ext.h'])
        create_pxd(headers, pxdfile, code, **kwargs)
    elif args[0] == 'gles3':
        headers = read_files(includedir, ['GLES3/gl3.h'])
        create_pxd(headers, pxdfile, code, **kwargs)
    elif isfile(args[0]):
        headers = read_files(args[0], ['GL/gl.h'])
        create_pxd(headers, pxdfile, code, **kwargs)
    elif args[0].startswith('http://') or args[0].startswith('https://'):
        headers = read_url(args[0], 'GL/gl.h')
        create_pxd(headers, pxdfile, code, **kwargs)
    else:
        print('wrong argument:', args[0])
        usage(1)
    

if __name__ == '__main__':
    main()
    

