#!/usr/bin/python3
# vim: tabstop=4 expandtab

# new commands start with keyword on position 1
# continuations start with whitespace
# comment lines start with # on first position
# keywords: msg, run, edit, msg, cd, nur

import argparse, os, re, readline, sys, tempfile

def parse(txt, verbose=0):
    cmds = []
    txt = txt.split('\n')
    M = re.compile('^(?:|(#|msg|cd|run|nur|edit|ask|\s)(.*))$')
    for i in range(len(txt)):
        m = M.match(txt[i])
        verbose > 1 and print('parse: %s' % str(m.groups()))
        if not m:
            print('syntax error in line %i: %s' % (i, txt[i]))
            return None
        if m.group(1) in (' ', '\t'):
            #continuation line
            if not len(cmds):
                cmds = ['', []]
            cmds[-1][1].append(m.group(0))
        elif m.group(1) in (None, '#'):
            cmds.append(['', [m.group(0)]])
        elif m.group(1) in ('msg', 'run', 'edit', 'ask', 'cd', 'nur'):
            cmds.append([m.group(1), [m.group(2)]])
        else:
            raise Exception('not implemented in line %i: %s' % (i, txt[i]))
    return cmds

def write(fn, cmds, verbose=0):
    f = open(fn, 'w')
    f.write('\n'.join(map(lambda cmd: cmd[0] + '\n'.join(cmd[1]), cmds)))
    f.close()
    if verbose > 1:
        print('wrote ' + fn)

def read(fn, verbose=0):
    f = open(fn, 'r')
    data = f.read()
    f.close()
    if verbose > 1:
        print('read ' + fn)
    return data

striplist = lambda l: list(map(lambda s: s.strip(), l))

def walker(script, verbose=0, ask=False, yes=False, pc=0, **args):
    editor = yes and 'true' or os.getenv('EDITOR', 'vim')
    echo = lambda: '[%i] %s %s' % (pc, cmds[pc][0], striplist(cmds[pc][1]))

    if script == '-':
        script = ''
        cmds = parse(sys.stdin.read(), verbose)
    else:
        cmds = parse(read(script,verbose), verbose)
    modified = False
    if not cmds:
        return 1

    ret = 0
    action = ''
    pc -= 1
    while pc < len(cmds):
        if verbose > 1:
            print('pc, ret, action = %i, %i, %s ' % (pc, ret, action))
        if action == '':
            if ret:
                if yes:
                    print('%s failed, exit' % echo())
                    break
                action = input('%s failed [*]? ' % echo())
            else:
                action = ask and 'a' or 'r'
        elif action == 'a':
            action = input('%s [r]? ' % echo()) or 'r'
            ret = 0
        elif action in ('h', '?'):
            print('h?: help, r: (re)run, e: edit, a: ask, s: show, n+: next, p-: prev, <int>:jump, w: write')
            action = ''
        elif action == 'q':
            break
        elif action == 'r':
            cmd = cmds[pc][0]
            lines = striplist(cmds[pc][1])
            if not cmd in ('', 'msg') and verbose > 0:
                print('%s ...' % echo())
            ret = 0
            if cmd == '':
                pass
            elif cmd == 'msg':
                print('>>> ' + '>>> '.join(lines))
            elif cmd == 'ask':
                print('>>> ' + '>>> '.join(lines))
                answer = yes and 'y' or input('[y*] ? ')
                ret = answer != 'y' and 1 or 0
            elif cmd == 'edit':
                ret = os.system('%s %s' % (editor, ' '.join(lines)))
            elif cmd == 'cd':
                try: 
                    os.chdir(' '.join(lines))
                except OSError:
                    ret = 1
            elif cmd in ('run', 'nur'):
                ret = os.system(' '.join(lines))
                if cmd == 'nur':
                    ret = ret == 0
            else:
                raise ValueError('not implemented: %s' % cmd)
            if not cmd in ('', 'msg') and verbose > 0:
                print('%s returned %i' % (echo(), ret))
            if not ret:
                pc += 1
            action = ''
        elif action in ('c', 's', 'S'):
            d = (0, 3, len(cmds))[('c', 's', 'S').index(action)]
            pc_ = pc
            for pc in range(max(0, pc - d), min(len(cmds), pc + d + 1)):
                print('%s%s' % (pc == pc_ and '>' or ' ', echo()))
            pc = pc_
            action ='a'
        elif action == 'e':
            f, fn = tempfile.mkstemp()
            os.close(f)
            write(fn, cmds)
            while True:
                os.system('%s %s' % (editor, fn))
                tmp = parse(read(fn))
                if not tmp:
                    if input("Retry edit [n*]?") == 'n':
                        break
                else:
                    if cmds != tmp:
                        modified = True
                        cmds = tmp
                    break
            os.unlink(fn)
            ret = 0
            action = 'a'
        elif action == 'w':
            script = input('filename [%s]: ' % script) or script
            try:
                write(script, cmds, verbose)
                modified = False
            except OSError:
                print('write failed')
        elif action.isdigit():
            pc = int(action)
            action = ''
            ret = 0
        elif action in ('n', '+'):
            ret, pc, action = 0, pc + 1, ''
        elif action in ('p', '-'):
            ret, pc, action = 0, pc - 1, ''
        elif action[0].isdigit() or action[0] in ('+', '-'):
            try:
                ret, pc, action = 0, pc + int(action), ''
            except:
                print('not a number')
                action = ''
        else:
            print('action not found: %s' % action)
            action = ''
    
    if script:
        f, fn = tempfile.mkstemp()
        os.close(f)
        write(fn, cmds, verbose)
        while read(script, verbose) != read(fn, verbose):
            action = input('script modified, write [ydn]? ')
            if action == 'n':
                break
            elif action == 'd':
                os.system('diff -ru "%s" "%s"' % (script, fn))
            if action == 'y':
                try:
                    write(script, cmds, verbose)
                except OSError:
                    print('write failed')
        os.unlink(fn)
    return ret and 1 or 0


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='checklist style script runner')
    parser.add_argument('-v', '--verbose', action='count', default=0,
        help='increase verbosity')
    parser.add_argument('-q', '--quiet', action='count', default=0,
        help='decrease verbosity')
    parser.add_argument('-a', '--ask', action='store_true',
        help='ask for every command')
    parser.add_argument('-y', '--yes', action='store_true',
        help='assume yes for all questions')
    
    parser.add_argument('script',
        help='script file')
    parser.add_argument('pc', nargs='?', type=int, default=0,
        help='program counter')

    args = parser.parse_args()
    args.verbose -= args.quiet
    del(args.quiet)

    sys.exit(walker(**vars(args)))

