# coding=utf-8
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
                        unicode_literals, with_statement)

import inspect
import re
from collections import OrderedDict

from docutils.core import publish_parts
from six import string_types
from six.moves import range

from pants.base.build_environment import get_buildroot
from pants.base.build_manual import get_builddict_info
from pants.base.exceptions import TaskError
from pants.base.generator import TemplateData
from pants.build_graph.target import Target
from pants.goal.goal import Goal
from pants.help.help_info_extracter import HelpInfoExtracter
from pants.option.arg_splitter import GLOBAL_SCOPE
from pants.option.options_bootstrapper import OptionsBootstrapper


# TODO(benjy): Rewrite this to handle subsystem options, and recursive/advanced options.
# Ideally the latter would be expandable or linked or something, so there's not a ton of clutter.
# Also make this code better (at least wrap it in a class and give the methods comprehensible
# names), and make its output prettier and more dynamic.
# This might make a good documentation hack week project for someone.


buildroot = get_buildroot()


# Our CLI help and doc-website-gen use this to get useful help text.
def indent_docstring_by_n(s, n=1):
  """Given a non-empty docstring, return version indented N spaces.
  Given an empty thing, return the thing itself."""
  # In reST, it's useful to have strings that are similarly-indented.
  # If we have a classdoc indented by 2 next to an __init__ funcdoc indented
  # by 4, reST doesn't format things nicely. Oh, totally-dedenting doesn't
  # format nicely either.

  # Docstring indentation: more gnarly than you'd think:
  # http://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation
  if not s: return s
  # Convert tabs to spaces (following the normal Python rules)
  # and split into a list of lines:
  lines = s.expandtabs().splitlines()
  # Determine minimum indentation (first line doesn't count):
  indent = 999
  for line in lines[1:]:
    stripped = line.lstrip()
    if stripped:
      indent = min(indent, len(line) - len(stripped))
  # Remove indentation (first line is special):
  trimmed = [lines[0].strip()]
  if indent < 999:
    for line in lines[1:]:
      trimmed.append(line[indent:].rstrip())
  # Strip off trailing and leading blank lines:
  while trimmed and not trimmed[-1]:
    trimmed.pop()
  while trimmed and not trimmed[0]:
    trimmed.pop(0)
  # Return a single string:
  indent = n * " "
  return '\n'.join([indent + t for t in trimmed])


def dedent_docstring(s):
  return indent_docstring_by_n(s, 0)


def rst_to_html(in_rst):
  """Returns HTML rendering of an RST fragment.

  :param in_rst: rst-formatted string
  """
  if not in_rst:
    return ''
  return publish_parts(in_rst, writer_name='html')['body'].strip()


def entry(nom, classdoc_rst=None, classdoc_html=None,
          msg_rst=None, msg_html=None, argspec=None,
          funcdoc_rst=None, funcdoc_html=None, methods=None, paramdocs=None,
          impl=None, indent=1):
  """Create a struct that our template expects to see.

  :param nom: Symbol name, e.g. python_binary
  :param classdoc_rst: plain text appears above argspec
  :param msg_rst: reST. useful in hand-crafted entries
  :param argspec: arg string like (x, y="deflt")
  :param funcdoc_rst: function's __doc__, plain text
  :param methods: list of entries for class' methods
  :param impl: name of thing that implements this.
     E.g., "pants.backend.core.tasks.builddict.BuildBuildDictionary"
  :param indent: spaces to indent; rst uses this for outline level
  """

  return TemplateData(
    nom=nom.strip(),
    classdoc_rst=indent_docstring_by_n(classdoc_rst),
    classdoc_html=classdoc_html,
    msg_html=msg_html,
    msg_rst=indent_docstring_by_n(msg_rst, indent),
    argspec=argspec,
    funcdoc_html=funcdoc_html,
    funcdoc_rst=indent_docstring_by_n(funcdoc_rst, indent),
    methods=methods,
    showmethods=len(methods or []) > 0,
    paramdocs=paramdocs,
    showparams=paramdocs and (len(paramdocs) > 0),
    impl=impl)


def msg_entry(nom, msg_rst, msg_html):
  """For hard-wired entries a la "See Instead" or other simple stuff

  :param nom: name
  :param msg_rst: restructured text message
  :param msg_html: HTML message; by default, convert from rst"""
  return entry(nom, msg_rst=msg_rst, msg_html=msg_html)


def entry_for_one_func(nom, func):
  """Generate a BUILD dictionary entry for a function
  nom: name like 'python_binary'
  func: function object"""
  args, varargs, varkw, defaults = inspect.getargspec(func)
  argspec = inspect.formatargspec(args, varargs, varkw, defaults)
  funcdoc_body_rst = docstring_to_body(dedent_docstring(func.__doc__))
  funcdoc_body_html = rst_to_html(funcdoc_body_rst)
  param_docshards = shard_param_docstring(dedent_docstring(func.__doc__))
  paramdocs = param_docshards_to_template_datas(param_docshards)
  return entry(nom,
               argspec=argspec,
               funcdoc_html=funcdoc_body_html,
               funcdoc_rst=func.__doc__ or '',
               impl="{0}.{1}".format(func.__module__, func.__name__),
               paramdocs=paramdocs)


def entry_for_one_method(nom, method):
  """Generate a BUILD dictionary entry for a method
  nom: name like 'with_description'
  method: method object"""
  # TODO(lhosken) : This is darned similar to entry_for_one_func. Merge 'em?
  #                 (Punted so far since funcdoc indentation made my head hurt,
  #                 but that will go away when we stop generating RST)
  assert inspect.ismethod(method)
  args, varargs, varkw, defaults = inspect.getargspec(method)
  # args[:1] instead of args to discard "self" arg
  argspec = inspect.formatargspec(args[1:], varargs, varkw, defaults)
  funcdoc_body_rst = docstring_to_body(dedent_docstring(method.__doc__))
  funcdoc_body_html = rst_to_html(funcdoc_body_rst)
  param_docshards = shard_param_docstring(dedent_docstring(method.__doc__))
  paramdocs = param_docshards_to_template_datas(param_docshards)
  return entry(nom,
               argspec=argspec,
               funcdoc_html=funcdoc_body_html,
               funcdoc_rst=(method.__doc__ or ""),
               paramdocs=paramdocs,
               indent=2)


# regex for docstring lines of the forms
# :param foo: blah blah blah
# :param string foo: blah blah blah
param_re = re.compile(r':param (?P<type>[A-Za-z0-9_]* )?(?P<param>[^:]*):(?P<desc>.*)')


# regex for docstring lines of the form
# :type foo: list of strings
type_re = re.compile(r':type (?P<param>[^:]*):(?P<type>.*)')


def docstring_to_body(docstring):
  """Passed a sphinx-flavored docstring, return just the "body" part.

  Filter out the :param...: and :type...: part, if any.
  """
  docstring = docstring or ''
  body = ''  # return value
  recording_state = True  # are we "recording" or not
  for line in docstring.splitlines():
    if line and not line[0].isspace():
      if any(r.match(line) for r in [param_re, type_re]):
        recording_state = False
      else:
        recording_state = True
    if recording_state:
      body += line + '\n'
  return body


def shard_param_docstring(docstring):
  """Shard a sphinx-flavored __init__ docstring by param

  E.g., if the docstring is

  :param float x: x coordinate
     blah blah blah
  :param y: y coordinate
  :type y: float

  should return
  OrderedDict(
    'x' : {'type': 'float', 'param': 'x coordinate\n   blah blah blah'},
    'y' : {'type': 'float', 'param': 'y coordinate'},
  )
  """
  docstring = docstring or ''

  # state: what I'm "recording" right now. Needed for multi-line fields.
  # ('x', 'param') : recording contents of a :param x: blah blah blah
  # ('x', 'type') : recording contents of a :type x: blah blah blah
  # ('!forget', '!') not recording useful things; purged before returning
  state = ('!forget', '!')

  # shards: return value
  shards = OrderedDict([('!forget', {'!': ''})])
  for line in docstring.splitlines():
    # If this line is indented, keep "recording" whatever we're recording:
    if line and line[0].isspace():
      param, type_or_desc = state
      shards[param][type_or_desc] += '\n' + line
    else:  # line not indented, starting something new
      # if a :param foo: line...
      if param_re.match(line):
        param_m = param_re.match(line)
        param_name = param_m.group('param')
        state = (param_name, 'param')
        if not param_name in shards:
          shards[param_name] = {}
        if param_m.group('type'):
          shards[param_name]['type'] = param_m.group('type')
        shards[param_name]['param'] = param_m.group('desc')
      # if a :type foo: line...
      elif type_re.match(line):
        type_m = type_re.match(line)
        param_name = type_m.group('param')
        state = (param_name, 'type')
        if not param_name in shards:
          shards[param_name] = {}
        shards[param_name]['type'] = type_m.group('type')
      # else, nothing that we want to "record"
      else:
        state = ('!forget', '!')
  del shards['!forget']
  return shards


def param_docshards_to_template_datas(funcdoc_shards):
  template_datas = []
  if funcdoc_shards:
    for param, parts in funcdoc_shards.items():
      if 'type' in parts:
        type_ = parts['type']
      else:
        type_ = None
      if 'param' in parts:
        desc = rst_to_html(dedent_docstring(parts['param']))
      else:
        desc = None
      template_datas.append(TemplateData(param=param, typ=type_, desc=desc))
  return template_datas


def info_for_target_class(cls):
  """Walk up inheritance tree to get info about constructor args.

  Helper function for entry_for_one_class. Target classes use inheritance
  to handle constructor params. If you try to get the argspec for, e.g.,
  `JunitTests.__init__`, it won't mention the `name` parameter, because
  that's handled by the `Target` superclass.
  """
  # args to not-document. BUILD file authors shouldn't
  # use these; they're meant to be impl-only.
  ARGS_SUPPRESS = ['address', 'build_graph', 'payload']

  # "accumulate" argspec and docstring fragments going up inheritance tree.
  suppress = set(ARGS_SUPPRESS)  # only show things once. don't show silly things
  args_accumulator = []
  defaults_accumulator = []
  docs_accumulator = []
  for c in inspect.getmro(cls):
    if not issubclass(c, Target): continue
    if not inspect.ismethod(c.__init__): continue
    args, _, _, defaults = inspect.getargspec(c.__init__)
    args_that_have_defaults = args[len(args) - len(defaults or ()):]
    args_with_no_defaults = args[1:(len(args) - len(defaults or ()))]
    for i in range(len(args_that_have_defaults)):
      arg = args_that_have_defaults[i]
      if not arg in suppress:
        suppress.add(arg)
        args_accumulator.append(arg)
        defaults_accumulator.append(defaults[i])
    for arg in args_with_no_defaults:
      if not arg in suppress:
        suppress.add(arg)
        args_accumulator.insert(0, arg)
    dedented_doc = dedent_docstring(c.__init__.__doc__)
    docs_accumulator.append(shard_param_docstring(dedented_doc))
  argspec = inspect.formatargspec(args_accumulator,
                                  None,
                                  None,
                                  defaults_accumulator)
  suppress = set(ARGS_SUPPRESS)  # only show things once. don't show silly things
  funcdoc_rst = ''
  funcdoc_shards = OrderedDict()
  for shard in docs_accumulator:
    for param, parts in shard.items():
      if param in suppress:
        continue
      suppress.add(param)
      funcdoc_shards[param] = parts
      # Don't interpret param names like "type_" as links.
      if 'type' in parts:
        funcdoc_rst += '\n:type {0}: {1}'.format(param, parts['type'])