#!/usr/bin/python3
#
# lincredits -- Beautify the Linux CREDITS file in various formats
#   Written by Chris Lawrence <lawrencc@debian.org>
#   (C) 1999-2023 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 appear 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.
#
# TODO:
# - Cleanup code
# - Support more tags
# - Put on Github
# - Better formatting when only one entry of a type is present

import sys
import getopt
import re
import html
import argparse
import email.utils

CREDITMAP = {
    'N' : 'name', # file regex for MAINTAINERS
    'E' : 'email',
    'M' : 'maintainer',
    'F' : 'files',
    'P' : 'pgp',
    'W' : 'url',
    'D' : 'desc',
    'S' : 'mail',
    'R' : 'reviewer',
    'T' : 'devtree',
    'X' : 'exclude',
    'K' : 'keyword', # regex
    'L' : 'email-list',
    'Q' : 'patchwork',
    'B' : 'bugs-url',
    'C' : 'chat-url',
    }

MAINTMAP = CREDITMAP.copy()
MAINTMAP['S'] = 'status'
MAINTMAP['N'] = 'file-regex'

def parseblock(lines, maintainers=False):
    creditinfo = {}
    creditline = re.compile(r':\s+')

    mapping = MAINTMAP if maintainers else CREDITMAP

    for line in lines:
        dat = creditline.split(line, 1)
        # Skip improperly formatted headers
        if len(dat) < 2:
            continue
        (dtype, data) = dat
        var = mapping.get(dtype)
        if var:
            creditinfo.setdefault(var, []).append(data)
        else:
            print('Warning: Unknown header:', dtype, file=sys.stderr)

    return creditinfo

def parse(infile):
    nameline = re.compile('N:\s+')
    headerline = re.compile('[A-Z]:\s+')

    linebuff = []
    people = {}
    past_header = False
    maintainers = infile.name and ('MAINTAINERS' in infile.name)

    for line in infile:
        line = line.rstrip()

        if not past_header:
            if '----------' in line:
                past_header = True
            elif 'List of maintainers' in line:
                # Heuristic to detect maintainers file
                maintainers = True
            continue

        # Skip comments
        if line.startswith('#'):
            continue

        if maintainers and line and not headerline.match(line):
            blockname = line
            linebuff = []
            continue
        elif nameline.match(line) and not maintainers:
            blockname = nameline.split(line, 1)[1]
            linebuff = []
            continue

        if linebuff and not line:
            info = parseblock(linebuff, maintainers)
            people[blockname] = info
        else:
            linebuff.append(line)

    if linebuff: # Flush any remaining data
        info = parseblock(linebuff)
        people[blockname] = info

    return people, maintainers

def format_text(info, filename, outfile, maintainers):
    filetype = 'Maintainers' if maintainers else 'Credits'

    print(f'{filetype} from {filename}', file=outfile)

    for (block, binfo) in info.items():
        print(block, file=outfile, end=' ')
        if maintainers:
            print(file=outfile)

        if 'email' in binfo:
            outfile.write('<'+binfo['email'][0]+'>:\n')
            bits = binfo['email'][1:]
            if bits:
                outfile.write('  Alternate addresses:\n')
                for bit in bits:
                    outfile.write('    <'+bit+'>\n')
        elif not maintainers:
            print(':', file=outfile)

        if 'maintainer' in binfo:
            print('  Maintained by:', file=outfile)
            for maint in binfo['maintainer']:
                print('   ', maint, file=outfile)

        if 'reviewer' in binfo:
            print('  Patches reviewed by:', file=outfile)
            for maint in binfo['reviewer']:
                print('   ', maint, file=outfile)

        if 'email-list' in binfo:
            print('  Mailing list:', file=outfile)
            for maillist in binfo['email-list']:
                print('   ', maillist, file=outfile)

        if 'status' in binfo:
            print('  Status:', file=outfile)
            for status in binfo['status']:
                print('   ', status, file=outfile)

        if 'url' in binfo:
            outfile.write('  Website:\n')
            bits = binfo['url']
            for bit in bits:
                outfile.write('    '+bit+'\n')
        if 'pgp' in binfo:
            outfile.write('  PGP key fingerprint:\n')
            bits = binfo['pgp']
            for bit in bits:
                outfile.write('    '+bit+'\n')
        if 'desc' in binfo:
            bits = binfo['desc']
            outfile.write('  Contributions:\n')
            for bit in bits:
                outfile.write('    '+bit+'\n')
        if 'mail' in binfo:
            bits = binfo['mail']
            outfile.write('  Physical address:\n')
            for bit in bits:
                outfile.write('    '+bit+'\n')
        outfile.write('\n')
    return

def make_email_link(addr, brackets=True):
    escaddr = html.escape(addr)
    formatted_link = f'<A HREF="mailto:{escaddr}">{escaddr}</A>'
    if brackets:
        return f'&lt;{formatted_link}&gt;'
    else:
        return formatted_link

def make_url_link(addr):
    escaddr = html.escape(addr)
    return f'<A HREF="{escaddr}">{escaddr}</A>'

def format_html(info, filename, outfile, maintainers):
    filetype = 'Maintainers' if maintainers else 'Credits'

    outfile.write('<!DOCTYPE html>\n')
    outfile.write('<HTML lang="en"><HEAD>\n')
    print(f'<TITLE>{filetype} from {html.escape(filename)}</TITLE>',
          file=outfile)
    outfile.write('</HEAD>\n\n<BODY><DL>\n')

    for block, binfo in info.items():
        if block:
            print(f'<DT>{html.escape(block)}', file=outfile, end='')
        else:
            outfile.write('<DT>')

        if 'email' in binfo:
            primary_email = binfo['email'][0]
            (realname, email_addr) = email.utils.parseaddr(primary_email)
            if realname and email_addr:
                print(f' ({realname}) {make_email_link(email_addr)}:</DT>',
                      file=outfile)
            elif email_addr:
                print(f' {make_email_link(email_addr)}:</DT>',
                      file=outfile)
            else:
                print(f' {html.escape(primary_email)}:</DT>', file=outfile)

            print('<DD><DL>', file=outfile)
            if (bits := binfo['email'][1:]):
                outfile.write('<DT>Alternate addresses:\n<DD><UL>\n')
                for bit in bits:
                    (realname, email_addr) = email.utils.parseaddr(bit)
                    if realname and email_addr:
                        print(f'<LI>{realname} {make_email_link(email_addr)}</LI>', file=outfile)
                    elif email_addr:
                        print(f'<LI>{make_email_link(email_addr, False)}</LI>',
                              file=outfile)
                    else:
                        print(f'<LI>{html.escape(bit)}</LI>', file=outfile)
                outfile.write('</UL>\n')
        else:
            outfile.write(':</DT>\n<DD><DL>\n')

        if 'maintainer' in binfo:
            print('<DT>Maintained by:</DT>\n<DD><UL>', file=outfile)
            for maint in binfo['maintainer']:
                (realname, email_addr) = email.utils.parseaddr(maint)
                if realname and email_addr:
                    print(f'<LI>{realname} {make_email_link(email_addr)}</LI>',
                          file=outfile)
                elif email_addr:
                    print(f'<LI>{make_email_link(email_addr, False)}</LI>',
                          file=outfile)
                else:
                    print(f'<LI>{html.escape(maint)}</LI>', file=outfile)
            print('</UL></DD>', file=outfile)

        if 'reviewer' in binfo:
            print('<DT>Patches reviewed by:</DT>\n<DD><UL>', file=outfile)
            for maint in binfo['reviewer']:
                (realname, email_addr) = email.utils.parseaddr(maint)
                if realname and email_addr:
                    print(f'<LI>{realname} {make_email_link(email_addr)}</LI>',
                          file=outfile)
                elif email_addr:
                    print(f'<LI>{make_email_link(email_addr, False)}</LI>',
                          file=outfile)
                else:
                    print(f'<LI>{html.escape(maint)}</LI>', file=outfile)
            print('</UL></DD>', file=outfile)

        if 'email-list' in binfo:
            print('<DT>Mailing list:</DT>\n<DD><UL>', file=outfile)
            for maint in binfo['email-list']:
                (realname, email_addr) = email.utils.parseaddr(maint)
                if realname and email_addr:
                    print(f'<LI>{realname} {make_email_link(email_addr)}</LI>',
                          file=outfile)
                elif email_addr:
                    print(f'<LI>{make_email_link(email_addr, False)}</LI>',
                          file=outfile)
                else:
                    print(f'<LI>{html.escape(maint)}</LI>', file=outfile)
            print('</UL></DD>', file=outfile)

        if 'status' in binfo:
            print(f'<DT>Status:</DT>\n<DD><UL>', file=outfile)
            bits = binfo['status']
            for bit in bits:
                print(f'<LI>{html.escape(status)}</LI>', file=outfile)
            print('</UL></DD>', file=outfile)

        if 'url' in binfo:
            outfile.write('<DT>Website:</DT>\n<DD><UL>\n')
            bits = binfo['url']
            for bit in bits:
                outfile.write('<LI>'+make_url_link(bit)+'\n')
            outfile.write('</UL></DD>\n')
        if 'pgp' in binfo:
            outfile.write('<DT>PGP key fingerprint:</DT>\n<DD><UL>\n')
            bits = binfo['pgp']
            for bit in bits:
                outfile.write('<LI><TT>'+html.escape(bit)+'</TT>\n')
            outfile.write('</UL></DD>\n')
        if 'desc' in binfo:
            bits = binfo['desc']
            outfile.write('<DT>Contributions:</DT>\n<DD><UL>\n')
            for bit in bits:
                outfile.write('<LI>'+html.escape(bit)+'\n')
            outfile.write('</UL>\n')
        if 'mail' in binfo:
            bits = binfo['mail']
            outfile.write('<DT>Physical address:</DT>\n<DD>')
            print('<BR>\n'.join(html.escape(bit) for bit in bits),
                  file=outfile, end='</DD>\n')
        outfile.write('</DL>\n')

    outfile.write('</DL></BODY>\n</HTML>\n')
    return

def texify(text, href=False):
    text = text.replace('\\', '\char 92')
    for ch in '#$%&_{}':
        text = text.replace(ch, '\\'+ch)
    if href:
        return text

    text = text.replace('"', '\\textquotedbl{}')
    text = text.replace('<', '\\textless{}')
    text = text.replace('>', '\\textgreater{}')
    text = text.replace('|', '\\textbar{}')
    text = text.replace('~', '\~{}')
    return text

def make_latex_email_link(addr, brackets=True):
    href_addr = texify(addr, True)
    taddr = texify(addr)
    formatted_link = r'\href{mailto:%s}{%s}' % (href_addr, taddr)
    if brackets:
        return '\\textless{}%s\\textgreater{}' % formatted_link
    else:
        return formatted_link

def format_latex(info, filename, outfile, maintainers):
    filetype = 'Maintainers' if maintainers else 'Credits'

    startlist = '\n\\begin{list}{}{}'

    print('\\documentclass{article}\n', file=outfile)
    print('\\usepackage[T1]{fontenc}', file=outfile)
    print('\\usepackage[utf8]{inputenc}', file=outfile)
    print('\\usepackage{hyperref}', file=outfile)
    outfile.write('\\title{%s from %s}\n' % (filetype, texify(filename)))
    outfile.write('\\author{Generated by \\texttt{lincredits}}\n')
    outfile.write('\\setlength{\parindent}{0in}\n')
    outfile.write('\\begin{document}\n\\maketitle\n')

    for block, binfo in info.items():
        # if block == 'David Gentzel':
        #     print(repr(binfo), file=sys.stderr)

        thisinfo = binfo
        outfile.write('\\begin{minipage}{\\textwidth}\n')
        outfile.write(texify(block))
        keys = list(thisinfo.keys())
        madelist = False
        if 'email' in thisinfo:
            primary_email = thisinfo['email'][0]
            (realname, email_addr) = email.utils.parseaddr(primary_email)
            if realname and email_addr:
                print(f' ({texify(realname)}) '
                      f'{make_latex_email_link(email_addr)}:',
                      file=outfile)
            elif email_addr:
                print(f' {make_latex_email_link(email_addr)}:',
                      file=outfile)
            else:
                print(f' {texify(primary_email)}:', file=outfile)

            if (bits := thisinfo['email'][1:]):
                print(startlist, file=outfile)
                outfile.write('\\item Alternate addresses:\n')
                outfile.write('\\begin{itemize}\n')
                for bit in bits:
                    (realname, email_addr) = email.utils.parseaddr(bit)
                    if realname and email_addr:
                        print(f'\\item {texify(realname)} '
                              f'{make_latex_email_link(email_addr)}',
                              file=outfile)
                    elif email_addr:
                        print('\\item '
                              f'{make_latex_email_link(email_addr, False)}',
                              file=outfile)
                    else:
                        print(f'\\item {texify(bit)}', file=outfile)
                outfile.write('\\end{itemize}\n')
                madelist=True

        if 'maintainer' in thisinfo:
            if not madelist:
                print(startlist, file=outfile)
                madelist=True

            print('\\item Maintained by:', file=outfile)
            print('\\begin{itemize}', file=outfile)
            for maint in thisinfo['maintainer']:
                (realname, email_addr) = email.utils.parseaddr(maint)
                if realname and email_addr:
                    print(f'\\item {realname} '
                          f'{make_latex_email_link(email_addr)}',
                          file=outfile)
                elif email_addr:
                    print(f'\\item {make_latex_email_link(email_addr, False)}',
                          file=outfile)
                else:
                    print(f'\\item {texify(maint)}', file=outfile)
            print('\\end{itemize}', file=outfile)

        if 'reviewer' in thisinfo:
            if not madelist:
                print(startlist, file=outfile)
                madelist=True

            print('\\item Patches reviewed by:', file=outfile)
            print('\\begin{itemize}', file=outfile)
            for maint in thisinfo['reviewer']:
                (realname, email_addr) = email.utils.parseaddr(maint)
                if realname and email_addr:
                    print(f'\\item {realname} '
                          f'{make_latex_email_link(email_addr)}',
                          file=outfile)
                elif email_addr:
                    print(f'\\item {make_latex_email_link(email_addr, False)}',
                          file=outfile)
                else:
                    print(f'\\item {texify(maint)}', file=outfile)
            print('\\end{itemize}', file=outfile)

        if 'email-list' in thisinfo:
            if not madelist:
                print(startlist, file=outfile)
                madelist=True

            print('\\item Mailing list:', file=outfile)
            print('\\begin{itemize}', file=outfile)
            for maint in thisinfo['email-list']:
                (realname, email_addr) = email.utils.parseaddr(maint)
                if realname and email_addr:
                    print(f'\\item {realname} '
                          f'{make_latex_email_link(email_addr)}',
                          file=outfile)
                elif email_addr:
                    print(f'\\item {make_latex_email_link(email_addr, False)}',
                          file=outfile)
                else:
                    print(f'\\item {texify(maint)}', file=outfile)
            print('\\end{itemize}', file=outfile)

        if 'status' in thisinfo:
            if not madelist:
                print(startlist, file=outfile)
                madelist=True

            bits = thisinfo['status']
            outfile.write('\\item Status:\n')
            outfile.write('\\begin{itemize}\n')
            for bit in bits:
                outfile.write('\\item '+texify(bit)+'\n')
            outfile.write('\\end{itemize}\n')

        if 'url' in thisinfo:
            if not madelist:
                print(startlist, file=outfile)
                madelist=True

            outfile.write('\\item Website:\n')
            outfile.write('\\begin{itemize}\n')
            bits = thisinfo['url']
            for bit in bits:
                bit = texify(bit, href=True)
                print('\\item \\url{%s}' % (bit), file=outfile)
            outfile.write('\\end{itemize}\n')

        if 'pgp' in thisinfo:
            if not madelist:
                print(startlist, file=outfile)
                madelist=True

            outfile.write('\\item PGP key fingerprint:\n')
            outfile.write('\\begin{itemize}\n')
            bits = thisinfo['pgp']
            for bit in bits:
                outfile.write('\\item \\texttt{'+texify(bit)+'}\n')
            outfile.write('\\end{itemize}\n')

        if 'desc' in thisinfo:
            if not madelist:
                print(startlist, file=outfile)
                madelist=True

            bits = thisinfo['desc']
            outfile.write('\\item Contributions:\n')
            outfile.write('\\begin{itemize}\n')
            for bit in bits:
                outfile.write('\\item '+texify(bit)+'\n')
            outfile.write('\\end{itemize}\n')
        if 'mail' in thisinfo:
            if not madelist:
                print(startlist, file=outfile)
                madelist=True

            bits = thisinfo['mail']
            outfile.write('\\item Physical address:\n')
            outfile.write('\\begin{list}{}{}\n')
            for bit in bits:
                outfile.write('\\item '+texify(bit)+'\n')
            outfile.write('\\end{list}\n')

        if madelist:
            outfile.write('\\end{list}\n')
        outfile.write('\\end{minipage}\n\n')
        outfile.write('\\vspace{\\baselineskip}\n')

    outfile.write('\\end{document}')
    return

USAGE = ('lincredits: Parse the Linux CREDITS file\n\n'
         'Usage: lincredits [options] <filename>\n'
         'Supported options:\n'
         '  --text:  Output as formatted text (default)\n'
         '  --html:  Output as HTML formatted text\n'
         '  --latex: Output as LaTeX source\n'
         '  --output <filename>: Output to a file instead of standard output\n'
         '  --help:  Show this help text\n'
         )

def main():
    parser = argparse.ArgumentParser(description='Process Linux CREDITS and MAINTAINERS files')
    parser.add_argument('filename', metavar='FILE',
                        help="CREDITS or MAINTAINERS file to parse")
    parser.add_argument('--html', dest='formatter', action='store_const',
                        const=format_html, default=format_text,
                        help='Output as HTML-formatted text')
    parser.add_argument('--latex', '--LaTeX',
                        dest='formatter', action='store_const',
                        const=format_latex,
                        help='Output as LaTeX source')
    parser.add_argument('--text', dest='formatter', action='store_const',
                        const=format_text,
                        help='Output as plain text (default)')
    parser.add_argument('-o', '-output', dest='outfile', type=str,
                        metavar='OUTFILE',
                        help='Output to OUTFILE instead of standard output')

    args = parser.parse_args()

    if args.outfile:
        outfile = open(args.outfile, 'w')
    else:
        outfile = sys.stdout

    infile = open(args.filename, 'r')
    info, maintainers = parse(infile)
    infile.close()

    args.formatter(info, args.filename, outfile, maintainers)
    if outfile != sys.stdout:
        outfile.close()
    return

if __name__ == '__main__':
    main()
