"""
Present the user with a text menu of options and use a single keystroke (via raw
mode in the terminal) to select a choice.

A similar module is provided by Sean Reifschneider at
ftp://ftp.tummy.com/pub/tummy/rawstdin/
"""
__author__ = 'Martin Blais <blais@furius.ca>'

import sys, string, termios, tty
from itertools import starmap

__all__ = ('query_choices',)



def query_choices(choices,
                  prefix=None,
                  loop=True,
                  allow_interrupt=True,
                  console_file=None):
    """Given a list of choice strings, present the user with them and return the
    selected one. The user is queried with a single character.

    Each item of the choice list can be either a single string or a pair of
    (string, character), to force which character to be used.

    If 'loop' is True, do not return until the user provides a valid choice.
    Otherwise in the case of an invalid choice, return None.  If the loop is
    interrupted, this function returns KeyboardInterrupt for a choice.
    """

    if isinstance(choices, list) and all(isinstance(x, str) for x in choices):
        ochoices = choice_chars(choices)
        rchoices = dict((c, name) for (name, c) in ochoices)
    elif isinstance(choices, list) and all(isinstance(x, tuple) for x in choices):
        ochoices = choice_chars(choices)
        rchoices = dict((c, name) for (name, c) in ochoices)
    elif isinstance(choices, dict):
        assert all(isinstance(x, str) and len(x) == 1
                   for x in choices.iterkeys())
        assert all(isinstance(x, str)
                   for x in choices.itervalues())
        rchoices = choices
        ochoices = choices.items()
    else:
        raise KeyError("Invalid argument type: {}".format(choices))

    write = console_file.write or sys.stdout.write
    try:
        while 1:
            if prefix:
                write(prefix)
                write(' ')
            write('[ %s ]? ' % ' | '.join(starmap(highlight_char, ochoices)))
            c = read_one()
            try:
                choice = rchoices[c.lower()]
                write('%s\n' % choice)
                break
            except KeyError:
                write('**Invalid choice**\n')
                if not loop:
                    choice = None
                    break
    except KeyboardInterrupt:
        write('**Interrupted**\n')
        if allow_interrupt:
            raise KeyboardInterrupt
        else:
            choice = KeyboardInterrupt
    return choice


def read_one():
    "Reads a single character from the terminal."

    # Set terminal in raw mode for faster input.
    orig_term_attribs = termios.tcgetattr(sys.stdin.fileno())
    tty.setraw(sys.stdin.fileno())
    try:
        c = sys.stdin.read(1)
        if c == chr(3): # INTR
            raise KeyboardInterrupt
    finally:
        termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH,
                          orig_term_attribs)
    return c


def highlight_char(s, c):
    """Lower the string, and set the first occurrence of given character as
    uppercase.  If the given character is not present in the string, append a
    separator with the character in it.

    >>> highlight_char('Remove', 'r')
    'Remove'

    >>> highlight_char('Remove', 'e')
    'rEmove'

    >>> highlight_char('Remove', 't')
    'remove(t)'
    """
    s = s.lower()
    c = c.lower()
    try:
        idx = s.index(c)
        out = s[:idx] + c.upper() + s[idx+1:]
    except ValueError:
        out = '%s:%s' % (c, s)
    return out


def choice_chars(choices):
    """Compute a list of characters from a list of choices. Return a list of
    (choice, char) pairs, in the same order as provided.

    >>> choice_chars('Resolve Redo'.split())
    [('Resolve', 'r'), ('Redo', 'e')]

    >>> choice_chars('Resolve Skip View Quit Redo'.split())
    [('Resolve', 'r'), ('Skip', 's'), ('View', 'v'), ('Quit', 'q'), ('Redo', 'e')]

    >>> choice_chars(['Resolve', 'Skip', 'View', 'Quit', ('Redo', 'd')])
    [('Resolve', 'r'), ('Skip', 's'), ('View', 'v'), ('Quit', 'q'), ('Redo', 'd')]

    """
    # Integrate all the hard choices provided by the user.
    hard_map = dict(x for x in choices if isinstance(x, tuple))
    charset = set(hard_map.itervalues())
    for c in charset:
        if not isinstance(c, str) or len(c) != 1:
            raise ValueError('Invalid choice character: %s' % c)

    # Check for collisions in the hard-coded choices.
    if len(charset) != len(hard_map):
        raise ValueError('Collision in choice characters: %s' % ''.join(charset))

    out_choices = []
    for choice in choices:
        if isinstance(choice, tuple):
            # Get the hard-coded character.
            choice = choice[0]
            c = hard_map[choice]
        else:
            assert isinstance(choice, str)

            # Select an appropriate character for the choice.
            for c in choice.lower():
                if c in charset:
                    continue
                charset.add(c)
                break
            else:
                # Select any other available character.
                for c in string.lowercase:
                    if c in charset:
                        continue
                    charset.add(c)
                else:
                    raise ValueError(
                        "Cannot find an appropriate character for %s outside of %s" %
                        (choice, ''.join(charset)))

        out_choices.append( (choice, c) )

    return out_choices


## FIXME: could add a default eventually on pressing Enter.


def test():
    print '\n\n1'
    query_choices('Resolve|Skip|View|Quit|Redo'.split('|'), 0)
    print '\n\n2'
    query_choices( ('Resolve', 'Skip', 'View', 'Quit', ('Redo', 'd')))

if __name__ == '__main__':
    test()
