#!/usr/bin/env python

# (c) 2012, Jan-Piet Mens <jpmens () gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
#

import os
import sys
import textwrap
import re
import optparse
import datetime
import subprocess
from ansible import utils
from ansible.utils import module_docs
import ansible.constants as C
from ansible.utils import version
import traceback

MODULEDIR = C.DEFAULT_MODULE_PATH

BLACKLIST_EXTS = ('.swp', '.bak', '~', '.rpm')

_ITALIC = re.compile(r"I\(([^)]+)\)")
_BOLD   = re.compile(r"B\(([^)]+)\)")
_MODULE = re.compile(r"M\(([^)]+)\)")
_URL    = re.compile(r"U\(([^)]+)\)")
_CONST  = re.compile(r"C\(([^)]+)\)")
PAGER   = 'less'
LESS_OPTS = 'FRSX' # -F (quit-if-one-screen) -R (allow raw ansi control chars)
                   # -S (chop long lines) -X (disable termcap init and de-init)

def pager_print(text):
    ''' just print text '''
    print text

def pager_pipe(text, cmd):
    ''' pipe text through a pager '''
    if 'LESS' not in os.environ:
        os.environ['LESS'] = LESS_OPTS
    try:
        cmd = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=sys.stdout)
        cmd.communicate(input=text)
    except IOError:
        pass
    except KeyboardInterrupt:
        pass

def pager(text):
    ''' find reasonable way to display text '''
    # this is a much simpler form of what is in pydoc.py
    if not sys.stdout.isatty():
        pager_print(text)
    elif 'PAGER' in os.environ:
        if sys.platform == 'win32':
            pager_print(text)
        else:
            pager_pipe(text, os.environ['PAGER'])
    elif hasattr(os, 'system') and os.system('(less) 2> /dev/null') == 0:
        pager_pipe(text, 'less')
    else:
        pager_print(text)

def tty_ify(text):

    t = _ITALIC.sub("`" + r"\1" + "'", text)    # I(word) => `word'
    t = _BOLD.sub("*" + r"\1" + "*", t)         # B(word) => *word*
    t = _MODULE.sub("[" + r"\1" + "]", t)       # M(word) => [word]
    t = _URL.sub(r"\1", t)                      # U(word) => word
    t = _CONST.sub("`" + r"\1" + "'", t)        # C(word) => `word'

    return t

def get_man_text(doc):

    opt_indent="        "
    text = []
    text.append("> %s\n" % doc['module'].upper())

    desc = "".join(doc['description'])

    text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent="  ", subsequent_indent="  "))
    
    if 'option_keys' in doc and len(doc['option_keys']) > 0:
        text.append("Options (= is mandatory):\n")

    for o in sorted(doc['option_keys']):
        opt = doc['options'][o]

        if opt.get('required', False):
            opt_leadin = "="
        else:
            opt_leadin = "-"

        text.append("%s %s" % (opt_leadin, o))

        desc = "".join(opt['description'])

        if 'choices' in opt:
            choices = ", ".join(str(i) for i in opt['choices'])
            desc = desc + " (Choices: " + choices + ")"
        text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent=opt_indent,
                             subsequent_indent=opt_indent))

    if 'notes' in doc and len(doc['notes']) > 0:
        notes = "".join(doc['notes'])
        text.append("Notes:%s\n" % textwrap.fill(tty_ify(notes), initial_indent="  ",
                            subsequent_indent=opt_indent))


    if 'requirements' in doc and doc['requirements'] is not None and len(doc['requirements']) > 0:
        req = ", ".join(doc['requirements'])
        text.append("Requirements:%s\n" % textwrap.fill(tty_ify(req), initial_indent="  ",
                            subsequent_indent=opt_indent))

    if 'examples' in doc and len(doc['examples']) > 0:
        text.append("Example%s:\n" % ('' if len(doc['examples']) < 2 else 's'))
        for ex in doc['examples']:
            text.append("%s\n" % (ex['code']))

    if 'plainexamples' in doc and doc['plainexamples'] is not None:
        text.append(doc['plainexamples'])
    text.append('')

    return "\n".join(text)


def get_snippet_text(doc):

    text = []
    desc = tty_ify("".join(doc['short_description']))
    text.append("- name: %s" % (desc))
    text.append("  action: %s" % (doc['module']))

    for o in sorted(doc['options'].keys()):
        opt = doc['options'][o]
        desc = tty_ify("".join(opt['description']))

        if opt.get('required', False):
            s = o + "="
        else:
            s = o

        text.append("      %-20s   # %s" % (s, desc))
    text.append('')

    return "\n".join(text)

def get_module_list_text(module_list):
    text = []
    for module in sorted(set(module_list)):

        if module in module_docs.BLACKLIST_MODULES:
            continue

        filename = utils.plugins.module_finder.find_plugin(module)

        if filename is None:
            continue
        if os.path.isdir(filename):
            continue

        try:
            doc, plainexamples = module_docs.get_docstring(filename)
            desc = tty_ify(doc.get('short_description', '?'))
            if len(desc) > 55:
                desc = desc + '...'
            text.append("%-20s %-60.60s" % (module, desc))
        except:
            traceback.print_exc()
            sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module)
            pass
    return "\n".join(text)

def main():

    p = optparse.OptionParser(
        version=version("%prog"),
        usage='usage: %prog [options] [module...]',
        description='Show Ansible module documentation',
    )

    p.add_option("-M", "--module-path",
            action="store",
            dest="module_path",
            default=MODULEDIR,
            help="Ansible modules/ directory")
    p.add_option("-l", "--list",
            action="store_true",
            default=False,
            dest='list_dir',
            help='List available modules')
    p.add_option("-s", "--snippet",
            action="store_true",
            default=False,
            dest='show_snippet',
            help='Show playbook snippet for specified module(s)')
    p.add_option('-v', action='version', help='Show version number and exit')

    (options, args) = p.parse_args()

    if options.module_path is not None:
        for i in options.module_path.split(os.pathsep):
            utils.plugins.module_finder.add_directory(i)

    if options.list_dir:
        # list all modules
        paths = utils.plugins.module_finder._get_paths()
        module_list = []
        for path in paths:
            # os.system("ls -C %s" % (path))
            if os.path.isdir(path):
                for module in os.listdir(path):
                    if any(module.endswith(x) for x in BLACKLIST_EXTS):
                        continue
                    module_list.append(module)

        pager(get_module_list_text(module_list))
        sys.exit()

    if len(args) == 0:
        p.print_help()
   
    def print_paths(finder):
        ''' Returns a string suitable for printing of the search path '''

        # Uses a list to get the order right
        ret = []
        for i in finder._get_paths():
            if i not in ret:
                ret.append(i)
        return os.pathsep.join(ret)
 
    text = ''
    for module in args:

        filename = utils.plugins.module_finder.find_plugin(module)
        if filename is None:
            sys.stderr.write("module %s not found in %s\n" % (module,
                    print_paths(utils.plugins.module_finder)))
            continue

        if any(filename.endswith(x) for x in BLACKLIST_EXTS):
            continue

        try:
            doc, plainexamples = module_docs.get_docstring(filename)
        except:
            traceback.print_exc()
            sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module)
            continue

        if doc is not None:

            all_keys = []
            for (k,v) in doc['options'].iteritems():
                all_keys.append(k)
            all_keys = sorted(all_keys)
            doc['option_keys'] = all_keys

            doc['filename']         = filename
            doc['docuri']           = doc['module'].replace('_', '-')
            doc['now_date']         = datetime.date.today().strftime('%Y-%m-%d')
            doc['plainexamples']    = plainexamples

            if options.show_snippet:
                text += get_snippet_text(doc)
            else:
                text += get_man_text(doc)
        else:
            # this typically means we couldn't even parse the docstring, not just that the YAML is busted,
            # probably a quoting issue.
            sys.stderr.write("ERROR: module %s missing documentation (or could not parse documentation)\n" % module)
    pager(text)

if __name__ == '__main__':
    main()
