# Text user interface for reportbug
#   Written by Chris Lawrence <lawrencc@debian.org>
#   (C) 2001 Chris Lawrence
#
# This program is freely distributable per the following license:
#
##  Permission to use, copy, modify, and distribute this software and its
##  documentation for any purpose and without fee is hereby granted,
##  provided that the above copyright notice appears in all copies and that
##  both that copyright notice and this permission notice appear in
##  supporting documentation.
##
##  I DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
##  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL I
##  BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
##  DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
##  WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
##  ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
##  SOFTWARE.

import commands, string, sys, os, re, math
from debianbts import launch_browser

ISATTY = not (commands.getoutput('tty') == 'not a tty')

try:
    r, c = string.split(commands.getoutput('stty size'))
    rows, columns = int(r) or 24, int(c) or 79
except:
    rows, columns = 24, 79

def ewrite(message, *args):
    if not ISATTY:
        return

    sys.stderr.write(message % args)

log_message = ewrite

# Readline support, if available
try:
    import readline
    readline.parse_and_bind("tab: complete")

    try:
        # minimize the word delimeter list if possible
        readline.set_completer_delims(' ')
    except:
        pass
except:
    readline = None

class our_completer:
    def __init__(self, completions=None):
        self.completions = completions

    def complete(self, text, i):
        if not self.completions: return None
        
        matching = []
        for possible in self.completions:
            if text == possible[:len(text)]:
                matching.append(possible)
        if i < len(matching):
            return matching[i]
        else:
            return None

def our_raw_input(prompt = None, completions=None):
    sys.stderr.flush()
    if readline:
        readline.set_completer(our_completer(completions).complete)
        
    try:
        if prompt:
            ret = raw_input(prompt)
        else:
            ret = raw_input()
    except EOFError:
        ewrite('\nUser interrupt (^D).\n')
        raise SystemExit

    if readline: readline.set_completer(None)
    return string.strip(ret)

def select_options(msg, ok, help, allow_numbers=None):
    err_message = ''
    for option in ok:
        if option in string.uppercase:
            default=option
            break
    
    if not help: help = {}

    if '?' not in ok: ok = ok+'?'

    ch = our_raw_input(msg+' ['+string.join(ok, '|')+']? ')
    # Allow entry of a bug number here
    if allow_numbers:
        while ch and ch[0] == '#': ch = ch[1:]
        if type(allow_numbers) == type(1):
            try:
                return str(int(ch))
            except ValueError:
                pass
        else:
            try:
                number = int(ch)
                if number in allow_numbers:
                    return str(number)
                else:
                    nums = list(allow_numbers)
                    nums.sort()
                    err_message = 'Only the following entries are allowed: '+\
                                  string.join(map(str, nums), ', ')
            except ValueError:
                pass
                
    if not ch: ch = default
    ch = ch[0]
    if ch=='?':
        help['?'] = 'Display this help.'
        for ch in ok:
            if ch in string.uppercase:
                desc = '(default) '
            else:
                desc = ''

            ch = string.lower(ch)
            desc = desc + help.get(ch, 'No help for this option.')
            ewrite('%s - %s\n', ch, desc)
        return select_options(msg, ok, help, allow_numbers)
    elif (string.lower(ch) in ok) or (string.upper(ch) in ok):
        return string.lower(ch)
    elif err_message:
        ewrite(indent_wrap_text(err_message))
    else:
        ewrite('Invalid selection.\n')
        
    return select_options(msg, ok, help, allow_numbers)

def indent_wrap_text(text, indent=0, linelen=None, firstindent=None,
                     firstlinelen=0):
    formatted = ''
    if firstindent is None: firstindent=indent
    pars = string.split(text, '\n\n')

    for par in pars:
        col = firstindent
        maxcol = firstlinelen or linelen or columns
        atbreak = 0
        for word in string.split(par):
            if atbreak:
                if col + len(word) >= maxcol:
                    formatted = formatted + '\n' + ' '*indent
                    col = indent
                    maxcol = linelen or columns
                else:
                    formatted = formatted + ' '
                    col = col + 1
            formatted = formatted + word
            col = col + len(word)
            atbreak = 1
        formatted = formatted + '\n'

    return formatted

def long_message(text, *args):
    ewrite(indent_wrap_text(text % args))


def get_string(prompt, options=None, title=None, force_prompt=0):
    if (len(prompt) < 2*columns/3) and not force_prompt:
        return our_raw_input(prompt, options)
    else:
        ewrite(indent_wrap_text(prompt))
        return our_raw_input('> ', options)

def menu(par, options, prompt, default=None, title=None, any_ok=0, order=None,
         extras=''):
    if title:
        ewrite(title+'\n\n')

    ewrite(indent_wrap_text(par, linelen=columns)+'\n')

    if type(options) == type({}):
        options = options.copy()
        # Convert to a list
        if order:
            list = []
            for key in order:
                if options.has_key(key):
                    list.append( (key, options[key]) )
                    del options[key]

            # Append anything out of order
            options = options.items()
            options.sort()
            for option in options:
                list.append( option )
            options = list
        else:
            options = options.items()
            options.sort()
            
    allowed = map(lambda x: x[0], options)
    if extras:
        allowed = allowed + extras
    
    maxlen_name = min(max(map(len, allowed)), columns/3)
    digits = int(math.ceil(math.log10(len(options)+1)))

    i = 1
    for name, desc in options:
        text = ('%*d %-*.*s  ' % (digits, i, maxlen_name, maxlen_name, name))+\
               indent_wrap_text(desc, indent=maxlen_name+digits+3,
                                linelen=columns-1)
        ewrite(text)
        if len(options) < 5:
            ewrite('\n')
        i = i+1
    if len(options) >= 5:
        ewrite('\n')

    if default:
        prompt = prompt + '[%s] ' % default

    while 1:
        response = our_raw_input(prompt, allowed)
        if not response: response = default
        
        if response in allowed or response == default:
            return response

        try:
            num = int(response)
            if 1 <= num <= len(options):
                return options[num-1][0]
        except ValueError:
            pass

        if any_ok:
            return response

        ewrite('Invalid entry.\n')
    return


# Things that are very UI dependent go here
def show_report(number, system, use_ldap, mirrors,
                http_proxy, screen=None, queryonly=0, title='',
                archived='no'):
    import debianbts

    sysinfo = debianbts.SYSTEMS[system]
    ewrite('Retrieving report #%d from %s bug tracking system...\n',
           number, sysinfo['name'])

    try:
        info = debianbts.get_report(number, system, ldap_ok=use_ldap,
                                    mirrors=mirrors, http_proxy=http_proxy,
                                    archived=archived)
    except:
        ewrite('No report available: #%s\n', number)
        return

    if info:
        (title, body) = info
##         pos = string.find(body, '-- System Info')
##         if pos >= 0:
##             body = body[:pos]

        text = title+'\n\n'+body
        skip_pager = 0
        
        while 1:
            if not skip_pager:
                fd = os.popen('sensible-pager', 'w')
                fd.write(text)
                fd.close()
            skip_pager = 0
            
            if queryonly:
                break
            else:
                x = select_options("Do you have extra information about this "
                                   "bug", 'yNrbq',
                                   {'y' : 'Provide extra information.',
                                    'n' : 'Return to browsing bugs.',
                                    'r' : 'Redisplay this report.',
                                    'b' : 'Launch web browser to read '
                                    'full log.',
                                    'q' : "I'm bored; quit please."})
                if x == 'y':
                    return number
                elif x == 'q':
                    return -1
                elif x == 'b':
                    launch_browser(number, system, mirrors)
                    skip_pager = 1
                elif x != 'r':
                    break
    else:
        ewrite('No report available: #%s\n', number)
            
    return

def handle_bts_query(package, bts, use_ldap, mirrors=None, http_proxy="",
                     queryonly=0, title="", screen=None, archived='no',
                     source=0):
    import debianbts
    
    root = debianbts.SYSTEMS[bts].get('btsroot')
    if not root:
        ewrite('%s bug tracking system has no web URL; bypassing query\n',
               debianbts.SYSTEMS[bts]['name'])
        return

    ewrite('Querying %s bug tracking system for reports on %s\n',
           debianbts.SYSTEMS[bts]['name'], package)
    ewrite('(Use ? for help at prompts.)\n')
    bugs = []

    try:
        (count, title, hierarchy)=debianbts.get_reports(
            package, bts, ldap_ok=use_ldap, mirrors=mirrors,
            source=source, http_proxy=http_proxy, archived=archived)
        if debianbts.SYSTEMS[bts].has_key('namefmt'):
            package2 = debianbts.SYSTEMS[bts]['namefmt'] % package
            (count2, title2, hierarchy2) = \
                     debianbts.get_reports(package2, bts, ldap_ok=use_ldap,
                                           mirrors=mirrors, source=source,
                                           http_proxy=http_proxy)
            count = count+count2
            for entry in hierarchy2:
                hierarchy.append( (package2+' '+entry[0], entry[1]) )

        exp = re.compile(r'\#(\d+):')
        for entry in hierarchy or []:
            for bug in entry[1]:
                match = exp.match(bug)
                if match:
                    bugs.append(int(match.group(1)))

        if not count:
            if hierarchy == None:
                ewrite('Package does not appear to exist.\n')
            else:
                ewrite('No outstanding bugs.\n')
        else:
            plural = 's'
            if count == 1: plural = ''

            ewrite('%d bug report%s found:\n', count, plural)
            endcount = catcount = 0
            scount = startcount = 1
            category = hierarchy[0]
            lastpage = []
            digits = int(math.ceil(math.log10(len(bugs)+1)))
            linefmt = '  %'+str(digits)+'d) %s\n'
            while category:
                scount = scount + 1
                catname, reports = category[0:2]
                while catname[-1] == ':': catname = catname[:-1]
                total = len(reports)

                while len(reports):
                    these = reports[:rows-2]
                    reports = reports[rows-2:]
                    remain = len(reports)

                    tplural = rplural = 's'
                    if total == 1: tplural=''
                    if remain != 1: rplural=''
                    
                    if remain:
                        lastpage.append(' %s: %d report%s (%d remain%s)\n' %
                                        (catname, total, tplural,
                                         remain, rplural))
                    else:
                        lastpage.append(' %s: %d report%s\n' %
                                        (catname, total, tplural))

                    oldscount, oldecount = scount, endcount
                    for report in these:
                        scount = scount + 1
                        endcount = endcount + 1
                        lastpage.append(linefmt % (endcount,
                                                   report[:columns-digits-5]))

                    if category == hierarchy[-1] or \
                       (scount >= (rows - len(hierarchy[catcount+1][1]) - 1)):
                        if endcount == count:
                            skipmsg = ''
                        else:
                            skipmsg = ' (s to skip rest)'

                        options = 'yNmrq'
                        if queryonly: options = 'Nmrq'
                        if skipmsg: options = options + 's'

                        rstr = "(%d-%d/%d) " % (startcount, endcount, count)
                        pstr = rstr + "Is the bug you found listed above"
                        if queryonly:
                            pstr = rstr + "What would you like to do next"
                        allowed = bugs+range(1, count+1)
                        while 1:
                            sys.stderr.writelines(lastpage)
                            x = select_options(pstr, options, {
                                'y' : 'Problem already reported; optionally '
                                'add extra information.',
                                'n' : 'Problem not listed above; possibly '
                                'check more (go to next page).',
                                'm' : 'Get more information about a bug (you '
                                'can also enter a number\n'
                                '     without selecting "m" first).',
                                'r' : 'Redisplay the last bugs shown.',
                                'q' : "I'm bored; quit please.",
                                's' : 'Skip remaining problems; file a new '
                                'report immediately.'},
                                               allow_numbers=allowed)
                            if x == 'n':
                                lastpage = []
                                break
                            elif x == 'r':
                                continue
                            elif x == 'q':
                                return -1
                            elif x == 's':
                                debianbts.close_ldap_conn()
                                return
                            elif x == 'y':
                                if queryonly:
                                    return

                                number = our_raw_input(
                                    'Enter the number of the bug report '
                                    'you want to give more info on,\n'
                                    'or press ENTER to exit: #')
                                while number and number[0] == '#':
                                    number=number[1:]
                                if number:
                                    try:
                                        number = int(number)
                                        if number not in bugs and 1 <= number <= len(bugs):
                                            number = bugs[number-1]
                                        return number
                                    except ValueError:
                                        ewrite('Invalid report number: %s\n',
                                               number)
                                else:
                                    return -1
                            else:
                                if x == 'm' or x == 'i':
                                    number = our_raw_input(
                                        'Please enter the number of the bug '
                                        'you would like more info on: #')
                                else:
                                    number = x

                                while number and number[0] == '#':
                                    number=number[1:]

                                if number:
                                    try:
                                        number = int(number)
                                        if number not in bugs and 1 <= number <= len(bugs):
                                            number = bugs[number-1]
                                        res = show_report(number, bts,
                                                          use_ldap, mirrors,
                                                          http_proxy,
                                                          queryonly=queryonly,
                                                          screen=screen,
                                                          title=title)
                                        if res:
                                            return res
                                    except ValueError:
                                        ewrite('Invalid report number: %s\n',
                                               number)
                                         
                        startcount = endcount+1
                        scount = 0

                    # these now empty
                        
                if category == hierarchy[-1]: break

                catcount = catcount+1
                category = hierarchy[catcount]
                if scount:
                    lastpage.append('\n')
                    scount = scount + 1

        debianbts.close_ldap_conn()

    except IOError:
        res = select_options('Unable to connect to BTS; continue', 'yN',
                             {'y': 'Keep going.',
                              'n': 'Abort.'})
        if res == 'n':
            return -1
