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

#    Copyright © 2009, 2011-2014, 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 sys, os, re
from contextlib import contextmanager

from .pxmakros import PxMakros
from pybiklib.debug import DEBUG_NOLINENO


class Py2pyxError (Exception): pass

class OpenWriteLazy:
    def __init__(self, path):
        self.path = path
        self.delayed_text = ''
        self.file = None
        
    def write_delayed(self, text):
        self.delayed_text += text
        
    def write(self, text):
        if self.file is None:
            if self.path:
                print('generating', self.path)
                self.file = open(self.path, 'wt', encoding='utf-8')
            else:
                self.file = sys.stdout
        if self.delayed_text:
            self.file.write(self.delayed_text)
            self.delayed_text = ''
        self.file.write(text)
        
    def close(self):
        if self.file is not None:
            self.file.close()
            
            
class PxParser:
    # groups in match objects and patterns
    #   1: leading space
    #   2: px command (includes 3 and 4)
    #   3: pxd flag
    #   4: px key
    #   5: px argument (includes 6 and 7)
    #   6: makro name
    #   7: makro argument
    re_pxline = re.compile(r'^( *)(#px *([a-z]?)(.)|)((\w*)\s*(.*?))\n$', flags=re.ASCII)
    
    # key: px command
    # value:
    #      pattern for pyx-file or None (copy line),
    #      count next lines or "count" (use px argument),
    fdict = {
                '+':    (r'\5',    0),
                '/':    (r'\5',    1),
                '>':    (r'#\2\5',    0),
                '-':    (r'#\2\5',    1),
                ':':    (r'#\2\5',    '*'),
                '.':    (r'#\2\5',    0),
            }
    fdict['#'] = fdict['>']
    
    char_to_makro = {   'd': 'PXDFILE',
                        'h': 'HFILE',
                        'c': 'CFILE',
                        '': '',
                    }
                    
    def __init__(self):
        self.next_cntlines = 0
        
    def parse_line(self, line):
        makro = None
        makrospec = None
        makroarg = None
        match = self.re_pxline.match(line)
        if match.group(2):
            if self.next_cntlines == 0:
                # start px command
                makro = match.group(3)
                if makro == 'm':
                    makro = match.group(6)
                    makrospec = match.expand(r'\2\6')
                    if not makro:
                        raise Py2pyxError('makro name expected')
                    makroarg = match.group(7)
                else:
                    makro = self.char_to_makro[makro]
                    makrospec = match.group(2)
                    makroarg = match.group(5)
            elif self.next_cntlines == 1:
                raise Py2pyxError('#px-command found, normal line expected')
            elif self.next_cntlines == '*':
                # end px command
                if match.group(4) != '.':
                    raise Py2pyxError('closing #px-command or normal line expected')
                if match.group(3):
                    raise Py2pyxError('closing #px-command with makro')
                makro = ''
            else:
                assert False
            pyxpat, self.next_cntlines = self.fdict[match.group(4)]
        elif self.next_cntlines == 0:
            # echo normal line
            makro = ''
            pyxpat = r'\2\5'
            self.next_cntlines = 0
        elif self.next_cntlines == 1:
            # oneline px block
            makroarg = match.group(5)
            pyxpat = r'#\2\5'
            self.next_cntlines = 0
        elif self.next_cntlines == '*':
            # multiline px block
            makroarg = match.group(5)
            pyxpat = r'#\2\5'
        else:
            assert False
        indent = match.group(1)
        pyxline = match.expand(pyxpat)
        
        return indent, makro, makrospec, makroarg, pyxline
        
class PxGenerator (PxMakros):
    def __init__(self, pyxf, pxdf, pxhf, pxcf):
        self.pyxf = pyxf
        self.pxdf = pxdf
        self.pxhf = pxhf
        self.pxcf = pxcf
        self.pyx_indent = ''
        self.pxd_indent = ''
        self.px_skiplines = False
        self.makro = None
        self.makrolineno = None
        self.pyxline = None
        self.namespace = None
        
    def PYXFILE(self, line):
        if self.pyxline is None:
            self.pyxline = line
        elif not line.startswith('#'):
            if self.pyxline.startswith('#'):
                self.pyxline = line
            else:
                raise Py2pyxError('multiple pyx-file lines for source line not allowed: {!r}'.format(line))
                
    def PYXFILE_finalize(self, indent):
        assert self.pyxline is not None
        if not self.pyxline or self.pyxline[0] == '#':
            self.makrospec = ''
        self.pyxf.write(self.pyx_indent + indent + self.pyxline + self.makrospec +'\n')
        self.pyxline = None
            
    def generate_lines(self, lineno, indent, makro, makrospec, makroarg, pyxline):
        if DEBUG_NOLINENO:
            lineno = 'X'
        self.makrospec =  (' '+makrospec) if makrospec else ''
        if self.px_skiplines:
            pyxline = '#~ ' + pyxline
        self.PYXFILE(pyxline)
        
        if makro is not None:
            self.makro = getattr(self, makro, makro)
            self.makrolineno = 0
            self.makrodata = None
        else:
            self.makrolineno += 1
        if self.makro == 'IF':
            bvalue = eval(makroarg)
            assert type(bvalue) is bool
            self.px_skiplines = not bvalue
        elif self.makro == 'IF_END':
            self.px_skiplines = False
        elif self.px_skiplines:
            pass
        elif self.makro == 'INDENT':
            self.pyx_indent = '    '
        elif self.makro == 'DEDENT':
            self.pyx_indent = ''
        elif self.makro == 'DINDENT':
            self.pxd_indent = '    '
        elif self.makro == 'DDEDENT':
            self.pxd_indent = ''
        elif callable(self.makro):
            if self.makro(indent, makroarg, lineno):
                self.makro = None
        elif self.makro:
            raise Py2pyxError('unknown makro: {!s}({!r})'.format(self.makro, makroarg))
                
        self.PYXFILE_finalize(indent)
            
def create_pyx(src_path, pyx_path, pxd_path, pxh_path, pxc_path):
    errors = 0
    openr = lambda path: open(path, 'rt', encoding='utf-8')
    @contextmanager
    def openw(path):
        owl = OpenWriteLazy(path)
        try:
            yield owl
        finally:
            owl.close
            
    with openr(src_path) as srcf, openw(pyx_path) as pyxf, openw(pxd_path) as pxdf, openw(pxh_path) as pxhf, openw(pxc_path) as pxcf:
        pxdf.write_delayed('#generated from: {}\n\n'.format(src_path))
        pxhf.write_delayed('//generated from: {}\n\n'.format(src_path))
        pxcf.write_delayed('//generated from: {}\n\n'.format(src_path))
        pxparser = PxParser()
        pxgenerator = PxGenerator(pyxf, pxdf, pxhf, pxcf)
        for lineno, line in enumerate(srcf):
            try:
                indent, makro, makrospec, makroarg, pyxline = pxparser.parse_line(line)
            except Py2pyxError as e:
                print('%s:%d:'%(src_path, lineno+1), e, file=sys.stderr)
                print('  invalid line:', repr(line), file=sys.stderr)
                indent = ''
                makro = makrospec = makroarg = None
                pyxline = ''
                errors += 1
            except Exception as e:
                print('%s:%d:'%(src_path, lineno+1), e, file=sys.stderr)
                print('  line:', repr(line), file=sys.stderr)
                raise
            try:
                pxgenerator.generate_lines(lineno+1, indent, makro, makrospec, makroarg, pyxline)
            except Py2pyxError as e:
                print('%s:%d:'%(src_path, lineno+1), e, file=sys.stderr)
                print('  invalid line:', repr(line), file=sys.stderr)
                errors += 1
            except Exception as e:
                print('%s:%d:'%(src_path, lineno+1), e, file=sys.stderr)
                print('  line:', repr(line), file=sys.stderr)
                raise
    if errors:
        raise Py2pyxError('create_pyx failed with %s errors' % errors)
        
    
def main(argv):
    if not (1 < len(argv) <= 5):
        print('usage:', os.path.basename(__file__), 'python-file [pyx-filename [pxd-filename [h-filename [c-filename]]]]',
            file=sys.stderr)
        return 1
        
    arg_src = argv[1]
    arg_dst = argv[2] if len(argv) > 2 else None
    arg_pxd = argv[3] if len(argv) > 3 else None
    arg_pxh = argv[4] if len(argv) > 4 else None
    arg_pxc = argv[5] if len(argv) > 5 else None
    if not os.path.exists(arg_src):
        print('error:', arg_src, 'does not exist', file=sys.stderr)
        return 1
    create_pyx(arg_src, arg_dst, arg_pxd, arg_pxh, arg_pxc)
    return 0
    
if __name__ == '__main__':
    sys.exit(main(sys.argv))
    

