#! /usr/bin/env python

from __future__ import print_function

import copy
import fnmatch
import os
import os.path
import re

class error(Exception):
    pass

class converter(object):

    LF = os.linesep

    def __init__(self, source, verbose = False, write = True):
        self.verbose = verbose
        self.write = write
        self.source = source
        self.changes = { }
        self.greps = self.searches()
        for g in self.greps:
            self.changes[g] = []
        with open(source, 'r') as f:
            self.src = f.readlines()
        self.orig = copy.deepcopy(self.src)
        self.orig_changes = { }
        self.edits = ''

    def __del__(self):
        if self.write and self.src is not None:
            with open(self.source, 'w') as f:
                f.writelines(self.src)
            self.src = None

    def __str__(self):
        s = '-' * 20 + self.LF
        s += '%s: IN: %-5d' % (self.source, len(self.src))
        s += ' OUT: %-5d' % (len(self.src))
        s += ' DIFF:%-5d' % (len(self.orig) - len(self.src))
        s += ' EDITS: %s %s' % (self.has_edits(), self.edits)
        s += self.LF
        s += 'CHANGES:' + self.LF
        for c in self.orig_changes:
            if len(self.orig_changes[c]) > 0:
                s += ' %s: %d%s' % (c, len(self.orig_changes[c]),  self.LF)
                for l in self.orig_changes[c]:
                    s += ' %5d: %s%s' % (l, self.orig[l - 1][:-1], self.LF)
        s += 'DIFF:' + self.LF
        s += ''.join(self.diff())
        return s

    def diff(self):
        import difflib
        return difflib.unified_diff(self.orig, self.src)

    def has_changes(self):
        return True

    def has_edits(self):
        return len(self.edits) > 0

    def searches(self):
        return { }

    def get(self, pos, length = 1):
        if pos <= len(self.src):
            return [s[:-1] for s in self.src[pos - 1:pos - 1 + length]]
        return []

    def source_len(self):
        return len(self.src)

    def source_empty(self, pos):
         return len(self.src[pos - 1][:-1]) == 0

    def insert(self, pos, lines):
        if type(lines) is str:
            lines = [lines]
        i = len(self.src)
        self.src = self.src[:pos - 1] + \
                   [l + self.LF for l in lines] + \
                   self.src[pos - 1:]
        for t in self.changes:
            for p in range(0, len(self.changes[t])):
                if self.changes[t][p] >= pos:
                    self.changes[t][p] += len(lines)
        self.edits += ' add:%d/%d ' % (i, len(self.src))

    def remove(self, pos, count = 1):
        i = len(self.src)
        for l in range(0, count):
            del self.src[pos - 1]
        for t in self.changes:
            for p in range(0, len(self.changes[t])):
                if self.changes[t][p] >= pos and self.changes[t][p] < pos + count:
                    self.changes[t][p] = 0
                elif self.changes[t][p] >= pos:
                    self.changes[t][p] -= count
        self.edits += ' del:%d/%d' % (i, len(self.src))

    def replace(self, pos, lines):
        if type(lines) is str:
            lines = [lines]
        self.remove(pos, len(lines))
        self.insert(pos, lines)

    def scan(self):
        lc = 0
        for l in self.src:
            lc += 1
            if lc > len(self.src):
                print(']]', self.source, l)
            for g in self.greps:
                if self.greps[g].search(l):
                    if g not in self.changes:
                        self.changes[g] = [lc]
                    else:
                        self.changes[g] += [lc]
        self.orig_changes = copy.deepcopy(self.changes)

    def update(self):
        pass

    def convert(self):
        self.scan()
        if self.has_changes():
            self.update()

class jobs(object):

    def __init__(self, path, globs = None, excludes = [], verbose = False):
        self.verbose = verbose
        self.path = path
        self.globs = globs
        self.excludes = excludes
        self.files = []
        if globs is not None:
            if type(globs) is not list:
                globls = [globs]
            for g in globs:
                self.find_files(path, g)
        else:
            self.files = path

    def __str__(self):
        return 'Files: %d' % (len(self.files))

    def find_files(self, path, glob, reset = False):
        if reset:
            self.files = []
        for root, dirs, files in os.walk(path, followlinks = True):
            for f in files:
                if fnmatch.fnmatch(f, glob) and f not in self.excludes:
                    self.files += [os.path.join(root, f)]
        self.files = sorted(self.files)

    def run(self, changer, write = True):
        for f in self.files:
            c = changer(f, verbose = self.verbose, write = write)
            c.convert()
            if c.has_edits():
                if self.verbose:
                    print('%70s: updated' % (f))
                print(c)
            del c

class converter_5(converter):

    def searches(self):
        clean_until_empty  = 'preinstall-stamp'
        clean_until_empty += '|preinstall-recursive:'
        clean_until_empty += '|preinstall'
        clean_until_empty += '|^preinstall-targets'
        clean_until_empty += '|PREINSTALL_DIR'
        clean_until_empty += '|^AS_IF.*rtems_cv_gcc_isystem'
        clean_until_empty += '|^SUBDIR_TARGET'
        clean_until_empty += '|RTEMS_CPPFLAGS'
        with_rtems_tops    = '^for bsp in : \$RTEMS_BSP_LIST; do test "x\$bsp" = x: && continue'
        with_rtems_tops   += '|CONFIG_SHELL=\\\\\$\(SHELL\) RTEMS_BSP='
        return { 'rtems-top':             re.compile('RTEMS_TOP\(.*\)'),
                 'rtems-include':         re.compile('^AC_DEFUN\(\[RTEMS_PROG_CC_FOR_TARGET\]'),
                 'multilib':              re.compile('^RTEMS_ENABLE_MULTILIB|^AS_IF\(\[test x"\$enable_multilib"'),
                 'rtems-build-top':       re.compile('^AC_SUBST\(rtems_bsp_configure\)'),
                 'include':               re.compile('^include_.*\s|^nodist_include_.*\s'),
                 'cpuopts':               re.compile('^AC_DEFUN\(\[_RTEMS_CPUOPT_FINI|score/include/rtems/score'),
                 'gccspecs':              re.compile('GCCSPECS="-B|C.* = .* \$\(GCCSPECS\)'),
                 'libbsp-cppflags':       re.compile('^libbsp_a_CPPFLAGS ='),
                 'with-rtems-tops':       re.compile(with_rtems_tops),
                 'compile-bsp':           re.compile('^AM_CPPFLAGS =|CC = @CC@'),
                 'arflags':               re.compile('^ARFLAGS ='),
                 'compile-ar':            re.compile('^AM_CCASFLAGS = @RTEMS_CCASFLAGS@'),
                 'if-compile':            re.compile('^include \$\(top_srcdir\)/automake/compile.am'),
                 'all-local':             re.compile('^all-local:'),
                 'mkdir-project-lib':     re.compile('^clean-local:'),
                 'project-libs-a':        re.compile('project_lib_.*\.a$'),
                 'project-libs-linkcmds': re.compile('project_lib_.*linkcmds'),
                 'move-compile-am':       re.compile('^if LIBDEBUGGER|^if LIBDL'),
                 'bsp-linkcmds':          re.compile('^RTEMS_BSP_LINKCMDS'),
                 'testsuite-link':        re.compile('^AM_LDFLAGS  ='),
                 'preinstall-targets':    re.compile('\$\(preintstall_targets\)'),
                 'clean-until-empty':     re.compile(clean_until_empty) }

    def update_rtems_top(self):
        change = 'rtems-top'
        if not self.source.endswith('rtems-top.m4'):
            for c in range(0, len(self.changes[change])):
                l = self.changes[change][c]
                if l == 0:
                    continue
                s = self.get(l)[0]
                self.insert(l + 1, ['RTEMS_SOURCE_TOP', 'RTEMS_BUILD_TOP'])

    def update_rtems_include(self):
        change = 'rtems-include'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            depth = 0
            while l <= self.source_len():
                s = self.get(l)[0]
                for c in s:
                    if c == '[':
                        depth += 1
                    elif c == ']':
                        depth -= 1
                if s == '])' and depth == 0:
                    i = ['', 'RTEMS_INCLUDES']
                    if self.source != './cpukit/aclocal/prog-cc.m4':
                        i += ['RTEMS_BSP_INCLUDES']
                    if self.source == './testsuites/aclocal/prog-cc.m4':
                        i += ['RTEMS_BSP_LINKCMDS']
                    self.insert(l, i)
                    break
                l += 1

    def update_multilib(self):
        change = 'multilib'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            s = self.get(l)[0]
            if s.startswith('RTEMS_ENABLE_MULTILIB'):
                self.remove(l)
            else:
                self.remove(l, 3)

    def update_rtems_build_top(self):
        change = 'rtems-build-top'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            self.insert(l, ['rtems_bsp_configure="$rtems_bsp_configure \'--with-rtems-build-top=$(pwd)\'"'])

    def update_include(self):
        change = 'include'
        trace = self.source == 'xx'
        c = 0
        while c < len(self.changes[change]):
            if trace:
                print()
                print(']]]!', self.changes[change])
            l = self.changes[change][c]
            c += 1
            if trace:
                print(']]]> l=%d src=%d' % (l, self.source_len()))
            if l == 0:
                continue
            end = l
            if l > 1:
                empty_before = self.source_empty(l - 1)
            else:
                empty_before = True
            while end < self.source_len():
                if trace:
                    print(']]]  l=%d end=%d "%s"' % (l, end, self.get(end)[0]))
                if len(self.get(end)[0]) == 0:
                    end += 1
                    if trace:
                        print(']]] "" l=%d end=%d "%s"' % (l, end, self.get(end)[0]))
                    break
                if self.get(end)[0][-1] != '\\':
                    if end != self.source_len() and len(self.get(end + 1)[0]) == 0:
                        end += 1
                        if trace:
                            print(']]] E l=%d end=%d "%s"' % (l, end, self.get(end)[0]))
                    break
                end += 1
            count = end - l + 1
            if trace:
                print(']]]< pos=%d count=%d src=%d' % (l, count, self.source_len()))
            self.remove(l, count)

    def update_cpuopts(self):
        change = 'cpuopts'
        if self.source.endswith('acinclude.m4'):
            op = 'score/include/rtems/score'
            np = '${RTEMS_BUILD_ROOT}/include/rtems/score'
            for c in range(0, len(self.changes[change])):
                l = self.changes[change][c]
                if l == 0:
                    continue
                s = self.get(l)[0]
                if s.startswith('AC_DEFUN'):
                    self.insert(l + 1, 'AC_REQUIRE([RTEMS_BUILD_TOP])')
                else:
                    self.replace(l, s.replace(op, np))

    def update_gccspecs(self):
        change = 'gccspecs'
        nl = 'GCCSPECS="-B\$(RTEMS_SOURCE_ROOT)/c/src/lib/libbsp/\$(RTEMS_CPU)/\$(RTEMS_BSP_FAMILY)/"'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            s = self.get(l)[0]
            if 'GCCSPECS=' in s:
                s =  nl
                n = self.get(l + 1)[0]
                if n.startswith('AS_IF([test "${enable_project_root+set}"],['):
                    self.remove(l, 3)
                    l -= 1
            else:
                s = s.replace(' $(GCCSPECS)', '')
            self.replace(l, s)

    def update_libbsp_cppflags(self):
        change = 'libbsp-cppflags'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            s = self.get(l)[0]
            if s[-1] == '=':
                s += ' $(AM_CPPFLAGS)'
            self.replace(l, s)

    def update_with_rtems_tops(self):
        change = 'with-rtems-tops'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            s = self.get(l)[0]
            if s.find('CONFIG_SHELL') >= 0:
                self.replace(l,     '           CONFIG_SHELL=\$(SHELL) RTEMS_BSP=$bsp \$(rtems_bsp_configure) \\\\')
                self.insert(l + 1, ['           --with-rtems-build-top=\$\${PWD} --with-rtems-source-top=${rtems_source_top} ) \\\\'])
            else:
                self.insert(l + 1, ['rtems_source_top=$(cd ${srcdir}/.. && pwd)'])

    def update_compile_bsp(self):
        change = 'compile-bsp'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            s = self.get(l)[0]
            if s.startswith('AM_CPPFLAGS'):
                replace = False
                if '@RTEMS_CPPFLAGS@' not in s:
                    s += ' @RTEMS_CPPFLAGS@'
                    replace = True
                update = ['./c/src/automake/compile.am',
                          './testsuites/automake/compile.am']
                if self.source in update:
                    s += ' @RTEMS_BSP_CPPFLAGS@'
                    replace = True
                if replace:
                    self.replace(l, s)
            else:
                self.replace(l, s.replace(' $(GCCSPECS)', ''))

    def update_arflags(self):
        change = 'arflags'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            self.remove(l)

    def update_compile_ar(self):
        change = 'compile-ar'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            self.insert(l + 1, ['', 'ARFLAGS = crD'])

    def update_if_compile(self):
        change = 'if-compile'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            for s in self.get(1, l - 1):
                if s.startswith('if '):
                    count = 1
                    if self.source_empty(l + 1):
                        count += 1
                    self.remove(l, count)
                    self.insert(1, ['include $(top_srcdir)/automake/compile.am', ''])
                    break

    def update_all_local(self):
        change = 'all-local'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            s = self.get(l)[0]
            self.replace(l, s.replace('preinstall ', ''))

    def update_mkdir_project_lib(self):
        change = 'mkdir-project-lib'
        if self.source == './c/src/Makefile.am':
            for c in range(0, len(self.changes[change])):
                l = self.changes[change][c]
                if l == 0:
                    continue
                self.insert(l, ['$(PROJECT_LIB)/$(dirstamp):',
                                '	@echo "Making project library directory: $(PROJECT_LIB)"',
                                '	@$(MKDIR_P) $(PROJECT_LIB)',
                                '	@: > $(PROJECT_LIB)/$(dirstamp)',
                                '',
                                'all-local: $(PROJECT_LIB)/$(dirstamp)',
                                ''])

    def update_project_libs_a(self):
        change = 'project-libs-a'
        plus = ''
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            s = self.get(l)[0]
            lib = s.split('=')[1].strip()
            lib_name = os.path.basename(lib).strip()
            r = ['',
                 '$(PROJECT_LIB)/@LN@: @L@',
                 '	$(INSTALL_DATA) $< $(PROJECT_LIB)/@LN@',
                 'TMPINSTALL_FILES @PLUS@= $(PROJECT_LIB)/@LN@',
                 '']
            r = [i.replace('@L@', lib) for i in r]
            r = [i.replace('@LN@', lib_name) for i in r]
            r = [i.replace('@PLUS@', plus) for i in r]
            plus = '+'
            self.insert(l + 1, r)

    def update_project_libs_linkcmds(self):
        change = 'project-libs-linkcmds'
        plus = ''
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            s = self.get(l)[0]
            linkcmds = s.split('=')[1].strip()
            path = os.path.dirname(self.source)
            if not os.path.exists(os.path.join(path, linkcmds)):
                self.remove(l)

    def update_move_compile_am(self):
        change = 'move-compile-am'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0 or l != 1:
                continue
            s = self.get(l + 2, 2)
            self.remove(l + 2)
            self.insert(1, s)

    def update_bsp_linkcmds(self):
        change = 'bsp-linkcmds'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            self.remove(l)

    def update_testsuite_link(self):
        change = 'testsuite-link'
        if self.source == './testsuites/automake/compile.am':
            for c in range(0, len(self.changes[change])):
                l = self.changes[change][c]
                if l == 0:
                    continue
                s = self.get(l + 1)[0]
                self.remove(l)
                self.insert(l, ['AM_LDFLAGS  = -B$(RTEMS_BSP_LIBBSP_PATH) $(GCCSPECS) -qnolinkcmds \\',
                                '              -L$(PROJECT_LIB) \\',
                                '              -L$(RTEMS_BSP_ARCH_LINKCMDS_PATH) \\',
                                '              -Wl,-T$(RTEMS_BSP_LINKCMDS_PATH)/$(RTEMS_BSP_LINKCMDS_SCRIPT) \\',
                                '              $(TEST_LD_FLAGS)'])

    def update_preinstall_targets(self):
        change = 'preinstall-targets'
        for c in range(0, len(self.changes[change])):
            l = self.changes[change][c]
            if l == 0:
                continue
            s = self.get(l)[0]
            self.replace(l, s.replace(' $(preintstall-targets)', ''))

    def update_clean_until_empty(self):
        change = 'clean-until-empty'
        excludes = ['compile.am']
        name = os.path.basename(self.source)
        if name not in excludes:
            trace = self.source == 'xx'
            c = 0
            while c < len(self.changes[change]):
                if trace:
                    print()
                    print(']]]!', c, self.changes[change])
                l = self.changes[change][c]
                c += 1
                if trace:
                    print(']]]> l=%d src=%d' % (l, self.source_len()))
                if l == 0:
                    continue
                end = l
                if l > 1:
                    empty_before = self.source_empty(l - 1)
                else:
                    empty_before = True
                while end < self.source_len():
                    if trace:
                        print(']]]  l=%d end=%d "%s"' % (l, end, self.get(end)[0]))
                    s = self.get(l)[0]
                    if s.endswith('preinstall.am') or self.source_empty(end):
                        if not empty_before and end > l:
                            end -= 1
                        break
                    end += 1
                count = end - l + 1
                if trace:
                    print(']]]< pos=%d end=%d count=%d src=%d' % (l, end, count, self.source_len()))
                    print(']]]<  1:', self.changes[change])
                self.remove(l, count)
                if trace:
                    print(']]]<  2:', c, self.changes[change])
            if name == 'local.am':
                self.insert(self.source_len() + 1, ['', 'all-local: $(TMPINSTALL_FILES)'])

    def update(self):
        self.update_rtems_top()
        self.update_rtems_include()
        self.update_multilib()
        self.update_include()
        self.update_cpuopts()
        self.update_gccspecs()
        self.update_libbsp_cppflags()
        self.update_with_rtems_tops()
        self.update_compile_bsp()
        self.update_arflags()
        self.update_compile_ar()
        self.update_if_compile()
        self.update_all_local()
        self.update_mkdir_project_lib()
        self.update_project_libs_a()
        self.update_project_libs_linkcmds()
        self.update_move_compile_am()
        #self.update_bsp_linkcmds()
        self.update_testsuite_link()
        self.update_preinstall_targets()
        self.update_clean_until_empty()

def run():
    import argparse
    parser = argparse.ArgumentParser(description = 'Patch a build.')
    parser.add_argument('--verbose', dest = 'verbose',
                        help='Verbose output')
    parser.add_argument('--dry-run', dest = 'write', action = 'store_false',
                        default = True, help='Dry run, do not write the changes out')
    args = parser.parse_args()

    j = jobs('.',
             ['*.am', '*.m4', 'configure.ac'],
             excludes = ['rtems-includes.m4', 'preinstall.am'],
             verbose = args.verbose)
    j.run(converter_5, write = args.write)

if __name__ == '__main__':
    run()
