#!/usr/bin/env python3
import sys, re, os, logging

# less and others have a number of things like this 
#
#       -hn or --max-back-scroll=n
#              Specifies  a  maximum number of lines to scroll backward.  If it
#              is necessary to scroll backward more than n lines, the screen is
#              repainted in a forward direction instead.  (If the terminal does
#              not have the ability to scroll backward, -h0 is implied.)
#
# Because of this the only *real* reliable system is to look at the formatting, 
# which normally gets stripped so we have to be a bit more clever and be a full
# wrapper to man as opposed to just parsing <stdout> 
#

logging.basicConfig(level=(os.environ.get('LOGLEVEL') or 'critical').upper())

# This allows us to do mansnip <num> <page> or just <page> 
cutoff = 3 if sys.argv[1].isnumeric() else 2
os.environ['MAN_KEEP_FORMATTING'] = '1'
cmd = '/usr/bin/man {}'.format(' '.join(sys.argv[1:cutoff]))
logging.info(cmd)
man_input = os.popen(cmd).read()

pack = sys.argv[cutoff:]

opts = '|'.join(pack)
#
# lsof uses +|- syntax, that's what the very fun (\+\||) is for
# Then comes the term expansion and a few trailing characters.
# ffmpeg uses [ at times (see hwaccel), many things use ',' to show a second option.
# the second part is for the long options, sometimes (git config) specified by commas
#
my_re = '^\s*((\+\||)({})([\s_\[].*|, .*|)|-.*\s({}))$'.format(opts, opts)
logging.info(my_re)

is_def = False
line_def = False
stack_start = False
term_indent = False

buf_start = False
buf = []

indent_window = []
stack_guess = []
stack_indent = []
last_stack_guess = []

# Words we can just leave out of the breadcrumb.
filler_terms = ['DESCRIPTION','OPTIONS']

clean_ansi = lambda w: re.sub('(.\x08)', '', w)

#
# There's lots of let's say "creative" ways to format man pages, so
# we have a plain-text version in case our sophisticated searching
# method fails.
#
man_input_plain = clean_ansi(man_input).split('\n')
man_input = man_input.split('\n')

#
# We try to output something nice and readable. If the input is a
# lot of lines, say zshall (all of zsh), then we set aside a bit of
# space for the indentation.
#
rs = 5 if len(man_input) < 1e4 else 7

for line_num, line in enumerate(man_input):
    line = line.strip('\n')

    #
    # Establish the "indent", this is crucial to how essentially 
    # everything works.
    #
    indent = re.match('^(\s*)', line).end()
    indent_window = indent_window[-2:] + [indent]

    if len(line):
        logging.debug(line)

        # Our nice stack guessing system essentially keeps a stack
        # of the indents and then based on the current indent either
        # pushes or pops on to the stack.

        #
        # This one is kinda tricky. If we are looking for -z in say, zshall
        #
        #         zcompile [ -U ] [ -z | -k ] [ -R | -M ] file [ name ... ]
        #         zcompile -ca [ -m ] [ -R | -M ] file [ name ... ]
        #         zcompile -t file [ name ... ]
        #               -z  
        #
        # Is what we want. So what we want is a longer version of what we currently have
        #
        # But it gets trickier since we suppress empty lines in this algorithm.
        # 
        #         r      Same as fc -e -.
        #
        #         read [ -rszpqAclneE ] [ -t [ num ] ] [ -k [ num ] ] [ -d delim ]
        #              [ -u n ] [ name[?prompt] ] [ name ...  ]
        #
        # Also from zsh, will be interpreted as a group, so we have to just this once
        # use our real indent tracker to catch it.
        #
        if len(stack_indent) and indent == stack_indent[-1] and indent_window[-2] != 0:
            #
            # Historically string growing like this was always dog, I don't know
            # if it's still super slow. Mostly of the time we won't actually be using
            # it. Since we aren't doing a stream, we could just reconstruct it by
            # storing "pointers" (indexes here) ... sometime in the future.
            #
            stack_guess[-1] += '\n' + man_input[line_num]

        else:
            while len(stack_indent) and indent <= stack_indent[-1]:
                stack_indent.pop()
                stack_guess.pop()

        if not len(stack_indent) or indent > stack_indent[-1]:
            stack_indent.append(indent)
            stack_guess.append(man_input[line_num].strip())

    if not len(buf):

        # See if our ansi escaped or plain text version has what we are looking for.
        # yes that makes it a bit slower, but not as slow as doing it manually.
        res = re.match(my_re, line) or re.match(my_re, man_input_plain[line_num])

        if res:
            #
            # This is sheer frantic handwaving for things like this (From bash)
            #
            #  declare [-aAfFgilnrtux] [-p] [name[=value] ...]
            #  typeset [-aAfFgilnrtux] [-p] [name[=value] ...]
            #
            # Surely, if I search for "declare" this is what I want, but it
            # it breaks our classic rules so instead we try a number of 
            # imperfet guesses.
            #
            # The first one is back-searching the indent margins. Generally
            # there's a space before we see this and then some end of a 
            # previous block that was indented further. soooo yeah we
            # look for that.
            #
            if indent_window[0] > indent and indent_window[1] == 0:
                is_def = True

            #
            # From man 7 man we get things like this:
            #
            #  .I  Italics
            #
            #  .IB Italics alternating with bold
            #
            #  .IR Italics alternating with Roman
            #
            elif indent_window[0] == indent and indent_window[1] == 0:
                line_def = True

            buf.append(line)
            buf_start = line_num
            stack_start = stack_guess[:]
            term_indent = indent

    elif term_indent:
        if (indent == 0 and len(line)) or (
              indent > 1 and (indent < term_indent or (not is_def and indent == term_indent)
           )):
            spacer = '\n' + ' ' * rs
            if (len(buf) == 2 and indent == term_indent and line_def) or len(buf) > 2:
                #
                # This is a lot of fancy formatting. We want the
                # vertical whitespace between the heading and snippet to be consistent.
                # If we simply removed the empty lines then it would dump the interstitial
                # (as in, in the middle of our content) empty lines. so we want to get rid
                # of leading and trailing new lines.
                #
                # So we join the array, strip out the trailing new lines, then split it up
                # again. eh, it's fine.
                #
                buf = '\n'.join(buf).strip('\n').split('\n')

                #
                # If we show the same breadcrumb every time it gets kinda laborious and 
                # repetitive so we cleverly hide the redundancy. 
                #
                stack_small = stack_start[:]
                for i in range(0, min(len(last_stack_guess),len(stack_start))):
                    if last_stack_guess[i] == stack_start[i]:
                        stack_small = stack_small[1:]

                last_stack_guess = stack_start[:]

                if len(stack_small) > 1:
                    if clean_ansi(stack_small[0]) in filler_terms:
                        stack_small = stack_small[1:]
                #
                # Now we have to figure out how to print it out to make it look right.
                # Sometimes it can be inline. The %/fmt double formatting trick comes 
                # in handle here.
                #
                if len(stack_small) > 1:
                    stack_print = stack_small[0]
                    if len(stack_small) > 2:
                        stack_print += spacer + spacer.join(stack_small[1:-1])
                    stack_print += "\n"
                else:
                    stack_print = ''
                    buf[0] = re.sub('^\s{%d}' % rs, '', buf[0])

                print(("{:<%d}{}{}\n" % rs).format(buf_start, stack_print, '\n'.join(buf)))

            buf = []
            term_indent = False
            is_def = False
        else:
            # Once our formatting goes in we have to reset it 
            if is_def and indent > term_indent:
                is_def = False
                
            buf.append(line)
