#!/usr/bin/python3
'''
Rover is a text-based light-weight frontend for update-alternatives.
Copyright (C) 2018 Mo Zhou <lumin@debian.org>
License: GPL-3.0+
'''
from typing import *
import argparse
import re, sys
import subprocess
import termbox
#import q  # XXX: only used for debugging


__VERSION__ = '0.2'
__AUTHOR__ = 'Mo Zhou <lumin@debian.org>'


def Version():
    '''
    Print version information to screen.
    '''
    print(f'Rover {__VERSION__} by {__AUTHOR__}')
    exit(0)


def systemShell(command: List[str]) -> str:
    '''
    Execute the given command in system shell. Unlike os.system(),
    the program output to stdout and stderr will be returned.
    '''
    subp = subprocess.Popen(command,
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    result = subp.communicate()[0].decode().strip()
    retcode = subp.returncode
    return result, retcode


class Rover(object):
    '''
    Text-based light-weight frontend to update-alternatives.
    '''
    color_alt_act = (termbox.WHITE, termbox.BLUE)
    color_sel_act = (termbox.WHITE, termbox.CYAN)
    color_normal = (termbox.WHITE, termbox.BLACK)
    color_status_normal = (termbox.WHITE | termbox.BOLD, termbox.BLACK)
    color_status_error = (termbox.WHITE | termbox.BOLD, termbox.RED)
    color_status_ok  = (termbox.WHITE | termbox.BOLD, termbox.GREEN)
    color_status_greet = (termbox.MAGENTA | termbox.BOLD, termbox.BLACK)
    color_status_alert = (termbox.YELLOW | termbox.BOLD, termbox.BLACK)
    status = f'Rover {__VERSION__} by {__AUTHOR__}'
    regex = ''
    alt_idx = 0  # the highlighted alternative in left pane
    sel_idx = 0  # the highlighted selection in right pane
    parsed_query = {'display': '', 'name': '', 'alternatives': ''}
    lw = [0, 0]
    rw = [0, 0]

    def __init__(self, tb):
        '''
        set default values, and read the alternatives list
        tb: instantiated Termbox
        '''
        # Default Values
        self.color_status = self.color_status_greet
        self.tb = tb

        # read alternative list and initialize data for both panes
        self.all_choices = []
        self.choices = list(sorted(self.all_choices))
        self.respawn_choices()

    def hint(self):
        '''
        display keybinding hint in status bar
        '''
        h1 = 'HINT: [↓] z,n  [↑] w,p'.ljust(int(24*self.tb.width()/80))
        h1 += '| '
        h2 = '[↓] j,↓  [↑] k,↑  [*] SPACE,ENTER'
        h2 = h2.ljust(self.tb.width()-len(h1))
        self.status = h1 + h2
        self.color_status = self.color_status_alert

    def respawn_choices(self):
        '''
        Re-read "update-alternatives --get-selections"
        '''
        old_alt_idx, old_alt_num = self.alt_idx, len(self.choices)
        old_lw = self.lw
        get_selections, retcode = systemShell(
                ['update-alternatives', '--get-selections'])
        if retcode:
            self.status = f'Failed to get selections. ({retcode})'
            self.color_status = self.color_status_error
        self.all_choices = get_selections.split('\n')
        self.choices = list(sorted(self.all_choices))
        self.filter()
        # lw :the window (range) of items to be displayed on left side
        self.lw = [0, min(tb.height()-1, len(self.choices))]
        self.parse_alternative()
        # restore 
        if len(self.choices) == old_alt_num:
            self.alt_idx, self.lw = old_alt_idx, old_lw

    def filter(self):
        '''
        filter contents in the left side pane. You can use either
        substring match or regex to match alternative names.
        '''
        matched = [x for x in self.all_choices
                if (self.regex.lower() in x.lower())
                or re.match(self.regex, x)]
        self.choices = list(sorted(set(matched)))
        self.parse_alternative()

    def parse_alternative(self):
        '''
        parse the output of update-alternatives --query NAME
        '''
        try:
            name, mode, select = self.choices[self.alt_idx].split()
        except IndexError as e:
            name, mode, select = '', '', ''
        lines, retcode = systemShell(['update-alternatives', '--query', name])
        lines = lines.split('\n')
        alternatives, priorities = [], []
        value = [x for x in lines if x.startswith('Value')]
        value = ' ' if not value else value[0]
        value = re.sub('Value:\s*(\w*)\s*', '\\1', value)
        status = [x for x in lines if x.startswith('Status')]
        status = ' ' if not status else status[0]
        status = re.sub('Status:\s*(\w*)\s*', '\\1', status)
        for line in lines:
            if line.startswith('Alternative:'):
                line = re.sub('Alternative:\s*(\w*)\s*', '\\1', line)
                alternatives.append(line)
            elif line.startswith('Priority:'):
                line = re.sub('Priority:\s*(\d*)\s*', '\\1', line)
                priorities.append(int(line.strip()))
        candidates = list(zip(alternatives, priorities))
        display = []
        if 'auto' == status:
            display.append('[*] auto')
        else:
            display.append('[ ] auto')
        for candidate in candidates:
            if candidate[0] == value and 'auto' != status:
                display.append('[*]' + f' {candidate[1]} {candidate[0]}')
            else:
                display.append('[ ]' + f' {candidate[1]} {candidate[0]}')
        self.parsed_query = {'display': display, 'name': name,
                'alternatives': ['auto'] + alternatives}
        # rw :the window (range) of items to be displayed on right side
        self.rw = [0, min(tb.height()-1, len(display))]

    def draw(self):
        '''
        Draw all things
        '''
        # Draw Sidebar
        choices = list(enumerate(self.choices))[self.lw[0]:self.lw[1]]
        sb_x, sb_w = 0, int(24*self.tb.width()/80)
        for i, (j, choice) in enumerate(choices):
            color = self.color_alt_act if (j == self.alt_idx) else self.color_normal
            name, mode, value = choice.split()
            for k in range(sb_w):
                ch = name[k] if k < len(name) else ' '
                self.tb.change_cell(sb_x + k, i, ord(ch), *color)
        # Draw selection area
        display = list(enumerate(self.parsed_query['display']))[self.rw[0]:self.rw[1]]
        sa_x, sa_w = sb_x+sb_w+1, self.tb.width()-sb_x-1
        for i, (j, line) in enumerate(display):
            color = self.color_sel_act if (j == self.sel_idx) else self.color_normal
            for k in range(sa_w):
                ch = line[k] if k < len(line) else ' '
                self.tb.change_cell(sa_x + k, i, ord(ch), *color)
        # Draw Statusbar
        st_x, st_w = 0, self.tb.width()
        for k in range(st_w):
            ch = self.status[k] if k < len(self.status) else ' '
            self.tb.change_cell(st_x + k, self.tb.height()-1, ord(ch), *self.color_status)
        # reset special color setting
        self.color_status = self.color_status_normal

    def move_up(self):
        '''
        move up the cursor on left side pane
        '''
        self.alt_idx = max(0, self.alt_idx - 1)
        if self.alt_idx < self.lw[0]:
            self.lw = list(map(lambda x: x-1, self.lw))
        self.status = ' | '.join(self.choices[self.alt_idx].split())
        self.sel_idx = 0
        self.parse_alternative()

    def move_down(self):
        '''
        move down the cursor on left side pane
        '''
        self.alt_idx = min(len(self.choices)-1, self.alt_idx + 1)
        if self.alt_idx >= self.lw[1]:
            self.lw = list(map(lambda x: x+1, self.lw))
        self.status = ' | '.join(self.choices[self.alt_idx].split())
        self.sel_idx = 0
        self.parse_alternative()

    def sel_move_up(self):
        '''
        move up the cursor on right side pane
        '''
        self.sel_idx = max(0, self.sel_idx - 1)
        if self.sel_idx < self.rw[0]:
            self.rw = list(map(lambda x: x-1, self.rw))
        highlight = self.parsed_query['alternatives'][self.sel_idx]
        self.status = f'*? {highlight}'
        self.color_status = self.color_status_alert

    def sel_move_down(self):
        '''
        move down the cursor on right side pane
        '''
        self.sel_idx = min(len(self.parsed_query['display'])-1, self.sel_idx + 1)
        if self.sel_idx >= self.rw[1]:
            self.rw = list(map(lambda x: x+1, self.rw))
        highlight = self.parsed_query['alternatives'][self.sel_idx]
        self.status = f'*? {highlight}'
        self.color_status = self.color_status_alert

    def config(self):
        '''
        change alternatives setting
        '''
        name = self.parsed_query['name']
        selection = self.parsed_query['alternatives'][self.sel_idx]
        msg, code = None, None
        if selection == 'auto':
            msg, code = systemShell(['update-alternatives', '--auto', name])
        else:
            msg, code = systemShell(['update-alternatives', '--set', name, selection])
        if code:
            self.status = f'Permission Denied. Are you root?'
            self.color_status = self.color_status_error
        else:
            self.status = f'{name} -> {selection}'
            self.color_status = self.color_status_ok
            self.respawn_choices()


if __name__ == '__main__':
    ag = argparse.ArgumentParser()
    ag.add_argument('-e', '--expression', type=str, default=None,
            help='Only display matched alternative names')
    ag.add_argument('-v', '--version', action='store_true',
            help='Print version information')
    ag = ag.parse_args()

    if ag.version:
        Version()

    with termbox.Termbox() as tb:
        tb.clear()
        rv = Rover(tb)

        if ag.expression:
            rv.regex = ag.expression
            rv.filter()

        rv.draw()
        tb.present()

        state_running = True
        state_input = False
        while state_running:
            ev = tb.poll_event()
            while ev:
                (typ, ch, key, mod, w, h, x, y) = ev
                # quit
                if (typ, key) == (termbox.EVENT_KEY, termbox.KEY_ESC):
                    state_running = False
                # quit
                elif ch == 'q' and not state_input:
                    state_running = False
                # right: down
                elif ch == 'j' and not state_input:
                    rv.sel_move_down()
                # right: down
                elif (typ, key) == (termbox.EVENT_KEY, termbox.KEY_ARROW_DOWN):
                    rv.sel_move_down()
                # right: up
                elif ch == 'k' and not state_input:
                    rv.sel_move_up()
                # right: up
                elif (typ, key) == (termbox.EVENT_KEY, termbox.KEY_ARROW_UP):
                    rv.sel_move_up()
                # left: down
                elif (typ, key) == (termbox.EVENT_KEY, termbox.KEY_CTRL_N):
                    rv.move_down()
                # left: down
                elif ch in ('n', 'z') and not state_input:
                    rv.move_down()
                # left: up
                elif ch in ('p', 'w') and not state_input:
                    rv.move_up()
                # left: up
                elif (typ, key) == (termbox.EVENT_KEY, termbox.KEY_CTRL_P):
                    rv.move_up()
                # input regex
                elif ch in ('l', '/') and not state_input:
                    state_input = True
                    rv.status = '?> '
                    rv.regex = ''
                    rv.color_status = rv.color_status_greet
                # 2 cased for Enter key
                elif (typ, key) == (termbox.EVENT_KEY, termbox.KEY_ENTER):
                    if state_input:
                        # end regex input
                        state_input = False
                        rv.filter()
                        rv.alt_idx = 0
                        rv.lw = [0, tb.height()-1]
                    else:
                        # trigger udpate-alternatives update
                        rv.config()
                # trigger alternatives update
                elif (typ, key) == (termbox.EVENT_KEY, termbox.KEY_SPACE):
                    if not state_input:
                        rv.config()
                # input regex
                elif state_input and ch:
                    rv.status += ch
                    rv.regex += ch
                    rv.color_status = rv.color_status_greet
                # remove character while typing
                elif state_input and termbox.KEY_BACKSPACE:
                    rv.regex = rv.regex[:-1]
                    rv.status = '?> ' + rv.regex
                    rv.color_status = rv.color_status_greet
                # status: display keybinding hint
                elif not state_input and ch == 'h':
                    rv.hint()
                # don't know what to do. Hint the user about usage.
                else:
                    rv.hint()
                ev = tb.peek_event()
            # refresh screen
            tb.clear()
            rv.draw()
            tb.present()
