# Copyright 2016 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Common functions for skydoc."""

import collections
import re
import textwrap
from xml.sax.saxutils import escape


ARGS_HEADING = "Args:"
EXAMPLES_HEADING = "Examples:"
EXAMPLE_HEADING = "Example:"
OUTPUTS_HEADING = "Outputs:"


class InputError(Exception):
  pass


class ExtractedDocs(object):
  """Simple class to contain the documentation extracted from a docstring."""

  def __init__(self, doc, attr_docs, example_doc, output_docs):
    self.doc = doc
    self.attr_docs = attr_docs
    self.example_doc = example_doc
    self.output_docs = output_docs


def leading_whitespace(line):
  """Returns the number of leading whitespace in the line."""
  return len(line) - len(line.lstrip())


def validate_strip_prefix(strip_prefix, bzl_files):
  if not strip_prefix:
    return strip_prefix
  prefix = strip_prefix if strip_prefix.endswith('/') else strip_prefix + '/'
  for path in bzl_files:
    if not path.startswith(prefix):
      raise InputError(
          'Input file %s does not have path prefix %s. Directory prefix set '
          'with --strip_prefix must be common to all input files.'
          % (path, strip_prefix))
  return prefix

def _parse_attribute_docs(attr_docs, lines, index):
  """Extracts documentation in the form of name: description.

  This includes documentation for attributes and outputs.

  Args:
    attr_docs: A dict used to store the extracted documentation.
    lines: List containing the input docstring split into lines.
    index: The index in lines containing the heading that begins the
        documentation, such as "Args:" or "Outputs:".

  Returns:
    Returns the next index after the documentation to resume processing
    documentation in the caller.
  """
  attr = None  # Current attribute name
  desc = None  # Description for current attribute
  args_leading_ws = leading_whitespace(lines[index])
  i = index + 1
  while i < len(lines):
    line = lines[i]
    # If a blank line is encountered, we have finished parsing the "Args"
    # section.
    if line.strip() and leading_whitespace(line) == args_leading_ws:
      break
    # In practice, users sometimes add a "-" prefix, so we strip it even
    # though it is not recommended by the style guide
    pattern = re.compile(r"""
        # Any amount of leading whitespace, plus an optional "-" prefix.
        ^\s*-?\s*
        # The attribute name, plus an optional "**" prefix for a **kwargs
        # attribute.
        ((?:\*\*)?[`\{\}\%\.\w\*]+)
        # A colon plus any amount of whitespace to separate the attribute name
        # from the description text.
        :\s*
        # The attribute description text.
        (.*)
    """, re.VERBOSE)
    match = re.search(pattern, line)
    if match:  # We have found a new attribute
      if attr:
        attr_docs[attr] = escape(desc)
      attr, desc = match.group(1), match.group(2)
    elif attr:
      # Merge documentation when it is multiline
      desc = desc + "\n" + line.strip()
    i += + 1

  if attr:
    attr_docs[attr] = escape(desc).strip()

  return i


def _parse_example_docs(examples, lines, index):
  """Extracts example documentation.

  Args:
    examples: A list to contain the lines containing the example documentation.
    lines: List containing the input docstring split into lines.
    index: The index in lines containing "Example[s]:", which begins the
        example documentation.

  Returns:
    Returns the next index after the attribute documentation to resume
    processing documentation in the caller.
  """
  heading_leading_ws = leading_whitespace(lines[index])
  i = index + 1
  while i < len(lines):
    line = lines[i]
    if line.strip() and leading_whitespace(line) == heading_leading_ws:
      break
    examples.append(line)
    i += 1

  return i


def parse_docstring(doc):
  """Analyzes the documentation string for attributes.

  This looks for the "Args:" separator to fetch documentation for each
  attribute. The "Args" section ends at the first blank line.

  Args:
    doc: The documentation string

  Returns:
    The new documentation string and a dictionary that maps each attribute to
    its documentation
  """
  attr_docs = collections.OrderedDict() if hasattr(collections, 'OrderedDict') else {}
  output_docs = collections.OrderedDict() if hasattr(collections, 'OrderedDict') else {}
  examples = []
  lines = doc.split("\n")
  docs = []
  i = 0
  while i < len(lines):
    line = lines[i]
    if line.strip() == ARGS_HEADING:
      i = _parse_attribute_docs(attr_docs, lines, i)
      continue
    elif line.strip() == EXAMPLES_HEADING or line.strip() == EXAMPLE_HEADING:
      i = _parse_example_docs(examples, lines, i)
      continue
    elif line.strip() == OUTPUTS_HEADING:
      i = _parse_attribute_docs(output_docs, lines, i)
      continue

    docs.append(line)
    i += 1

  doc = "\n".join(docs).strip()
  examples_doc = textwrap.dedent("\n".join(examples)).strip()
  return ExtractedDocs(doc, attr_docs, examples_doc, output_docs)
