#! /usr/bin/env python
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from __future__ import print_function

import argparse
import imp
import os
import re
import sys
import textwrap
import types

# A markdown code block template: https://goo.gl/9EsyRi
_CODE_BLOCK_FORMAT = '''```{language}
{code}
```
'''

_DEVIL_ROOT = os.path.abspath(
    os.path.join(os.path.dirname(__file__), '..', '..'))


def md_bold(raw_text):
  """Returns markdown-formatted bold text."""
  return '**%s**' % md_escape(raw_text, characters='*')


def md_code(raw_text, language):
  """Returns a markdown-formatted code block in the given language."""
  return _CODE_BLOCK_FORMAT.format(
      language=language or '', code=md_escape(raw_text, characters='`'))


def md_escape(raw_text, characters='*_'):
  """Escapes * and _."""

  def escape_char(m):
    return '\\%s' % m.group(0)

  pattern = '[%s]' % re.escape(characters)
  return re.sub(pattern, escape_char, raw_text)


def md_heading(raw_text, level):
  """Returns markdown-formatted heading."""
  adjusted_level = min(max(level, 0), 6)
  return '%s%s%s' % ('#' * adjusted_level, ' ' if adjusted_level > 0 else '',
                     raw_text)


def md_inline_code(raw_text):
  """Returns markdown-formatted inline code."""
  return '`%s`' % md_escape(raw_text, characters='`')


def md_italic(raw_text):
  """Returns markdown-formatted italic text."""
  return '*%s*' % md_escape(raw_text, characters='*')


def md_link(link_text, link_target):
  """returns a markdown-formatted link."""
  return '[%s](%s)' % (md_escape(link_text, characters=']'),
                       md_escape(link_target, characters=')'))


class MarkdownHelpFormatter(argparse.HelpFormatter):
  """A really bare-bones argparse help formatter that generates valid markdown.

  This will generate something like:

  usage

  # **section heading**:

  ## **--argument-one**

  ```
  argument-one help text
  ```

  """

  #override
  def _format_usage(self, usage, actions, groups, prefix):
    usage_text = super(MarkdownHelpFormatter, self)._format_usage(
        usage, actions, groups, prefix)
    return md_code(usage_text, language=None)

  #override
  def format_help(self):
    self._root_section.heading = md_heading(self._prog, level=1)
    return super(MarkdownHelpFormatter, self).format_help()

  #override
  def start_section(self, heading):
    super(MarkdownHelpFormatter, self).start_section(
        md_heading(heading, level=2))

  #override
  def _format_action(self, action):
    lines = []
    action_header = self._format_action_invocation(action)
    lines.append(md_heading(action_header, level=3))
    if action.help:
      lines.append(md_code(self._expand_help(action), language=None))
    lines.extend(['', ''])
    return '\n'.join(lines)


class MarkdownHelpAction(argparse.Action):
  def __init__(self,
               option_strings,
               dest=argparse.SUPPRESS,
               default=argparse.SUPPRESS,
               **kwargs):
    super(MarkdownHelpAction, self).__init__(
        option_strings=option_strings,
        dest=dest,
        default=default,
        nargs=0,
        **kwargs)

  def __call__(self, parser, namespace, values, option_string=None):
    parser.formatter_class = MarkdownHelpFormatter
    parser.print_help()
    parser.exit()


def add_md_help_argument(parser):
  """Adds --md-help to the given argparse.ArgumentParser.

  Running a script with --md-help will print the help text for that script
  as valid markdown.

  Args:
    parser: The ArgumentParser to which --md-help should be added.
  """
  parser.add_argument(
      '--md-help',
      action=MarkdownHelpAction,
      help='print Markdown-formatted help text and exit.')


def load_module_from_path(module_path):
  """Load a module given only the path name.

  Also loads package modules as necessary.

  Args:
    module_path: An absolute path to a python module.
  Returns:
    The module object for the given path.
  """
  module_names = [os.path.splitext(os.path.basename(module_path))[0]]
  d = os.path.dirname(module_path)

  while os.path.exists(os.path.join(d, '__init__.py')):
    module_names.append(os.path.basename(d))
    d = os.path.dirname(d)

  d = [d]

  module = None
  full_module_name = ''
  for package_name in reversed(module_names):
    if module:
      d = module.__path__
      full_module_name += '.'
    r = imp.find_module(package_name, d)
    full_module_name += package_name
    module = imp.load_module(full_module_name, *r)
  return module


def md_module(module_obj, module_link=None):
  """Write markdown documentation for a module.

  Documents public classes and functions.

  Args:
    module_obj: a module object that should be documented.
  Returns:
    A list of markdown-formatted lines.
  """

  def should_doc(name):
    return (not isinstance(module_obj.__dict__[name], types.ModuleType)
            and not name.startswith('_'))

  stuff_to_doc = [
      obj for name, obj in sorted(module_obj.__dict__.items())
      if should_doc(name)
  ]

  classes_to_doc = []
  functions_to_doc = []

  for s in stuff_to_doc:
    if isinstance(s, type):
      classes_to_doc.append(s)
    elif isinstance(s, types.FunctionType):
      functions_to_doc.append(s)

  heading_text = module_obj.__name__
  if module_link:
    heading_text = md_link(heading_text, module_link)

  content = [
      md_heading(heading_text, level=1),
      '',
      md_italic('This page was autogenerated. '
                'Run `devil/bin/generate_md_docs` to update'),
      '',
  ]

  for c in classes_to_doc:
    content += md_class(c)
  for f in functions_to_doc:
    content += md_function(f)

  print('\n'.join(content))

  return 0


def md_class(class_obj):
  """Write markdown documentation for a class.

  Documents public methods. Does not currently document subclasses.

  Args:
    class_obj: a types.TypeType object for the class that should be
      documented.
  Returns:
    A list of markdown-formatted lines.
  """
  content = [md_heading(md_escape(class_obj.__name__), level=2)]
  content.append('')
  if class_obj.__doc__:
    content.extend(md_docstring(class_obj.__doc__))

  def should_doc(name, obj):
    return (isinstance(obj, types.FunctionType)
            and (name.startswith('__') or not name.startswith('_')))

  methods_to_doc = [
      obj for name, obj in sorted(class_obj.__dict__.items())
      if should_doc(name, obj)
  ]

  for m in methods_to_doc:
    content.extend(md_function(m, class_obj=class_obj))

  return content


def md_docstring(docstring):
  """Write a markdown-formatted docstring.

  Returns:
    A list of markdown-formatted lines.
  """
  content = []
  lines = textwrap.dedent(docstring).splitlines()
  content.append(md_escape(lines[0]))
  lines = lines[1:]
  while lines and (not lines[0] or lines[0].isspace()):
    lines = lines[1:]

  if not all(l.isspace() for l in lines):
    content.append(md_code('\n'.join(lines), language=None))
    content.append('')
  return content


def md_function(func_obj, class_obj=None):
  """Write markdown documentation for a function.

  Args:
    func_obj: a types.FunctionType object for the function that should be
      documented.
  Returns:
    A list of markdown-formatted lines.
  """
  if class_obj:
    heading_text = '%s.%s' % (class_obj.__name__, func_obj.__name__)
  else:
    heading_text = func_obj.__name__
  content = [md_heading(md_escape(heading_text), level=3)]
  content.append('')

  if func_obj.__doc__:
    content.extend(md_docstring(func_obj.__doc__))

  return content


def main(raw_args):
  """Write markdown documentation for the module at the provided path.

  Args:
    raw_args: the raw command-line args. Usually sys.argv[1:].
  Returns:
    An integer exit code. 0 for success, non-zero for failure.
  """
  parser = argparse.ArgumentParser()
  parser.add_argument('--module-link')
  parser.add_argument('module_path', type=os.path.realpath)
  args = parser.parse_args(raw_args)

  return md_module(
      load_module_from_path(args.module_path), module_link=args.module_link)


if __name__ == '__main__':
  sys.exit(main(sys.argv[1:]))
