#!/usr/bin/env python
"""make_manpage.py - to make a manpage in python scripts"""
from datetime import date
import time
import types
import sys
import os
import getopt

manpage = {}

manpage["NAME"] = __doc__

manpage["DESCRIPTION"] = """\
As an executable: To convert a manpage structure in a python program to a man page.

As a module, supplies

    synopsis_from_options
    getopt_options_from_manpage
    usage

You can override the man page produced by supplying your own make_manpage call which looks like:

    make_manpage

See the FUNCTION CALLS section on these function calls."""

manpage["OPTIONS"] = [
    {"option": "-h, -?, --help",
     "description": "Help, the same text as the Synopsis section in the man page",
     "required": False},
    {"option": "-p, --path MAN_PATH",
     "description": "Set the man path to save the man page to (defaults to current directory)",
     "required": False},
    {"option": "-f, --file FILENAME",
     "description": "Set the name of the man page (defaults to APPNAME.SECTION)",
     "required": False},
    {"option": "-s, --section SECTION",
     "description": "Set the section for man page (defaults to 1)",
     "required": False},
    {"option": "-v, --version VERSION",
     "description": "Set the version of the program (defaults to 1.0)",
     "required": False},
    {"option": "-d, --date DATE",
     "description": "Set the date of release (defaults to the file's ctime)",
     "required": False},
    {"option": "-o, --stdout",
     "description": "Only print to STDOUT",
     "required": False},
    {"option": "APPNAME",
     "description": "The name of the application, including the full path",
     "required": True}
]

manpage["AUTHOR"] = "Mark Hackett"


# We want USAGE to come furst out of the sections that aren't considered defined in a man page
manpage["$1$.USAGE"] = [
    ["Run the manpage program on a program called moo.py which is in /usr/local/bin and store to current directory as moo.1",
     "make_manpage.py -f moo.1 /usr/local/bin/moo.py"],
    ["To create a man page for this program and view it as ASCII through troff",
     "make_manpage.py --stdout make_manpage.py | groff -t -man -Tascii"],
    ["To create a man page of another python program which has the manpage structure defined in it, "+
     "saving it as Release 1.3 and date it 12 Dec 2010, saving the resulting man page to "+
     "/opt/app/man/cat1/someprogram.py.1",
     "make_manpage --date \"12 Dec 2012\" --version 1.3 --path /opt/app/man someprogram.py"],
]

required_sections = [
    "NAME: Name section of the man page as a single string",
    "DESCRIPTION: Description section of the man page as a single string",
    "OPTIONS: An array of dictionary entries defining the options or parameters",
    "AUTHOR: The author(s) of the program as a single string",
]

optional_sections = [
    "SYNOPSIS: Anything to add to the default synopsis created by the name of the program and the option list as a single string ",
    "SEE ALSO: The See Also section of the man page as a single string ",
    "BUGS: The bugs section of the man page as a single string ",
]

# This section will come next
manpage["$2$.MAN PAGE STRUCTURE FORMAT"] = """\
The manpage structure format is a dictionary called "manpage" with the following required elements:\n"""

for option in required_sections:
    manpage["$2$.MAN PAGE STRUCTURE FORMAT"] += "\n\t%s\n" % option

manpage["$2$.MAN PAGE STRUCTURE FORMAT"] += """\nAnd with the following optional dictionary elements:\n"""

for option in optional_sections:
    manpage["$2$.MAN PAGE STRUCTURE FORMAT"] += "\n\t%s\n" % option

manpage["$2$.MAN PAGE STRUCTURE FORMAT"] += """\nAny other heading not so named will behave depending on the format of the dictionary lookup.
The name of the section will be the name of the dictionary item (e.g. "EXAMPLES").
If that item is a list, then a table will be created from the first and second items
in each list entry, iterating through all list items.

I.e.: "EXAMPLES": [["the number 1"],[1]] will be printed out as

EXAMPLES
    The number 1
        1

If that item is a string, then that string will be printed as-is.

I.e.  "EXAMPLES": "This is an example" will be printed out as:

EXAMPLES
    This is an example

Sections that are not recognised are sorted alphabetically, but characters that begin like
"$*$." (take note of the dot there) are not printed to the section heading and you can use
these to force a specific order to these, which will be placed before where the "SEE ALSO"
section of the man page would appear.

I.e. $1$.HORSE will appear before $2$.APPLECART, printing out to a man page like this:

HORSE
    Section about the Horse

APPLECART
    Section about the Applecart"""

manpage["$2.1$.OPTIONS STRUCTURE FORMAT"] = """\
The OPTIONS structure is set so as to automatically produce the synopsis of the call structure, the description of the
options themselves, and the items needed to be passed into the getopts call to process the option argument list. To
this end, they require several components to be supplied by name as a dictionary entry for each option that the program
will recongise:
    'option': Option synonym list separated by ", " with the single word denoting the value afterward, separated by a space
    'desription': Description of the option above
    'required': True|False (evaluated as "is the option required?")

i.e.
    {"option": "-h, -?, --help FUNCTION", "description": "Help on function FUNCTION", "required": False}

becomes

    [-h|-?|--help FUNCTION]

in the SYNOPSIS,

    [-h|-?|--help FUNCTION]
        Help on function FUNCTION

in the OPTIONS section and

    "h:?:",["help="]

is returned from make_getopt_options_from_manpage"""

def synopsis_from_options(appname, options):
    """synopsis_from_options (appname, options)
    appname: string for the name of the application to be processed
    options: the OPTIONS section of the man page.
    returns synopsis: the string to display as the synopsis section content

To create the short help on how to run the program"""

    synopsis = appname
    synopsis += " "
    if "OPTIONS" in options:
        options = options["OPTIONS"]
    for item in options:
        optionoptions = item["option"].split(" ")
        if "-" not in optionoptions[-1]:
            arg = optionoptions.pop()
        else:
            arg = ""
        if not item["required"]:
            synopsis += "["
        for thisopt in optionoptions:
            thisopt = thisopt.strip(",")
            if synopsis[-1] != "[":
                synopsis += "|"
            synopsis += thisopt
        if optionoptions and arg:
            synopsis += " "
        synopsis += arg
        if not item["required"]:
            synopsis += "]"
        synopsis += " "
    return synopsis

def _generic_section_write(appname, f, section, content):
    """_generic_section_write(appname, f, section, content)"""
    if section not in content:
        return
    if section[0] == "$":
        print >> f, ".SH "+section.rpartition("$.")[2]
    else:
        print >> f, ".SH "+section

    if type(content[section]) is types.ListType:
        for item in content[section]:
            if type(item) is types.StringType:
                print >> f, ".P\n"+item
            elif len(item) == 1:
                print >> f, ".P\n"+item[0]
            else:
                print >> f, ".TP"
                print >> f, item.pop(0)
                print >> f, item.pop(0)
                for line in item:
                    print >> f, ".RS 0\n"+line
    else:
        print >> f, content[section]

def _synopsis_section_write(appname, f, section, content):
    """_synopsis_section_write(appname, f, section, content)"""
    print >> f, ".SH SYNOPSIS"
    print >> f, synopsis_from_options(appname, content["OPTIONS"])
    _generic_section_write(appname, f, "SYNOPSIS", content)

def _options_section_write(appname, f, section, content):
    """_options_section_write(appname, f, section, content)"""
    print >> f, ".SH OPTIONS"
    for item in content[section]:
        print >> f, '.TP'
        print >> f, item["option"]
        print >> f, item["description"]

def _missing_required_sections(headings):
    """missing_required_sections(headings)"""
    missing = False
    
    for section in required_sections:
        section = section.split(":")[0]
        if section not in headings:
            print >> sys.stderr, "Missing required section "+section
            missing = True
    return missing

def make_manpage(appname, headings, filename = None, version = 1.0, page = 1, dateis = None):
    """make_manpage(appname, headings, filename = None, version = 1.0, page = 1, dateis = None)
    appname: string for the name of the application to be processed
    headings: the manpage structure
    filename: the name of the file to save the man page to (None = STDOUT)
    version: the version number of the code released
    page: the man page section to save this in
    dateis: the date to put on the man page (None=Today)

Write your own function if you need to replace this functionality"""

    first_headings = ["NAME", "SYNOPSIS", "DESCRIPTION", "OPTIONS", "EXAMPLES"]
    last_headings = ["SEE ALSO", "BUGS", "AUTHOR"]
    nongeneric_sections = {"SYNOPSIS": _synopsis_section_write, "OPTIONS": _options_section_write}

    if _missing_required_sections(headings):
        sys.exit(1)

    if dateis == None:
        dateis = date.today().strftime("%d %B %Y")
    if filename == None:
        f = sys.stdout
    else:
        try:
            f = open(filename, "w")
        except:
            sys.stderr.write("Cannot write out the man page to "+filename+"\n")
            sys.exit(2)

    print >> f, '.TH man '+str(page)+' "'+dateis+'" "'+str(version)+'" "'+appname+'"'

    for heading in first_headings:
        if heading in nongeneric_sections:
            nongeneric_sections[heading](appname, f, heading, headings)
        else:
            _generic_section_write(appname, f, heading, headings)
    for heading in sorted(headings):
        if heading in first_headings:
            continue
        if heading in last_headings:
            continue
        _generic_section_write(appname, f, heading, headings)

    for heading in last_headings:
        if heading in nongeneric_sections:
            nongeneric_sections[heading](appname, f, heading, headings)
        else:
            _generic_section_write(appname, f, heading, headings)

def getopt_options_from_manpage(options):
    """parse_options_from_manpage(options)
    options: The OPTIONS section from the manpage structure
    returns string, array

for use in the getopt() call"""

    shortoptions = ""
    longoptions = []
    if "OPTIONS" in options:
        options = options["OPTIONS"]
    for item in options:
        optionoptions = item["option"].split()
        if "-" not in optionoptions[-1]:
            hasarg = True
            del optionoptions[-1]
        else:
            hasarg = False
        for thisopt in optionoptions:
            if "--" in thisopt:
                longopt = thisopt.strip(",")[2:]
                if hasarg:
                    longopt += "="
                longoptions.append(longopt)
            elif "-" in thisopt:
                shortoptions += thisopt[1]
                if hasarg:
                    shortoptions += ":"
    return shortoptions, longoptions

def usage(appname, options):
    """usage(appname, options)
    appname: the name of the application
    options: the option section of the manpage structure

To print usage message"""

    print synopsis_from_options(appname, options)

# Now we have all the functions, we can make the function call list section
manpage["$3$.FUNCTION CALLS"] = [synopsis_from_options.__doc__,
    getopt_options_from_manpage.__doc__,
    usage.__doc__,
    make_manpage.__doc__,
]

if __name__ == "__main__":
    (shortopts, longopts) = getopt_options_from_manpage(manpage["OPTIONS"])
    try:
        opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
    except getopt.GetoptError, err:
        print str(err)
        sys.exit(2)

    manpath = "."
    section = 1
    version = 1.0
    dateis = None

    filename = ""
    stdout_only = False

    for o, a in opts:
        if o in ("-h", "-?", "--help"):
            usage(os.path.basename(__file__), manpage["OPTIONS"])
            sys.exit()
        elif o in ("-p", "--path"):
            manpath = a
        elif o in ("-f", "--file"):
            filename = a
        elif o in ("-s", "--section"):
            section = a
        elif o in ("-v", "--version"):
            version = a
        elif o in ("-d", "--date"):
            dateis = a
        elif o in ("-o", "--stdout"):
            stdout_only = True
        else:
            usage(os.path.basename(__file__), manpage["OPTIONS"])
            sys.exit(2)
    if args:
        app_path = os.path.dirname(args[0])
        if app_path == "":
            app_path = "./"
        appname = os.path.basename(args[0])
    else:
        usage(os.path.basename(__file__), manpage["OPTIONS"])
        sys.exit(0)

    if os.path.exists(args[0]):
        dateis = time.strftime("%d %B %Y", time.gmtime(os.path.getmtime(args[0])))

    if filename == "":
        filename = "%s/cat%d/%s.%d" % (manpath, section, appname, section)

    if stdout_only:
        filename = None

    if appname == os.path.basename(__file__):
        make_manpage(appname, manpage, None, version, section, dateis)
    else:
        sys.path.append(app_path)
        c = __import__(os.path.splitext(appname)[0])
        if "make_manpage" in dir(c) and type(c.make_manpage) is types.FunctionType:
            c.make_manpage(appname, c.manpage, filename, version, section, dateis)
        else:
            make_manpage(appname, c.manpage, filename, version, section, dateis)
