# Lint as: python2, python3
# Copyright 2015 The TensorFlow 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.
# ==============================================================================
"""A module for converting parsed doc content into markdown pages.

The adjacent `parser` module creates `PageInfo` objects, containing all data
necessary to document an element of the TensorFlow API.

This module contains one public function, which handels the conversion of these
`PageInfo` objects into a markdown string:

    md_page = build_md_page(page_info)
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import textwrap

import six


def build_md_page(page_info):
  """Given a PageInfo object, return markdown for the page.

  Args:
    page_info: must be a `parser.FunctionPageInfo`, `parser.ClassPageInfo`, or
        `parser.ModulePageInfo`

  Returns:
    Markdown for the page

  Raises:
    ValueError: if `page_info` is an instance of an unrecognized class
  """
  if page_info.for_function():
    return _build_function_page(page_info)

  if page_info.for_class():
    return _build_class_page(page_info)

  if page_info.for_module():
    return _build_module_page(page_info)

  raise ValueError('Unknown Page Info Type: %s' % type(page_info))


def _build_function_page(page_info):
  """Given a FunctionPageInfo object Return the page as an md string."""
  parts = ['# %s\n\n' % page_info.full_name]

  parts.append(_build_aliases(page_info.aliases))

  if page_info.signature is not None:
    parts.append(_build_signature(page_info))

  if page_info.defined_in:
    parts.append('\n\n')
    parts.append(str(page_info.defined_in))

  parts.append(page_info.guides)
  parts.append(page_info.doc.docstring)
  parts.append(_build_function_details(page_info.doc.function_details))
  parts.append(_build_compatibility(page_info.doc.compatibility))

  return ''.join(parts)


def _build_class_page(page_info):
  """Given a ClassPageInfo object Return the page as an md string."""
  parts = ['# {page_info.full_name}\n\n'.format(page_info=page_info)]

  parts.append('## Class `%s`\n\n' %
               six.ensure_str(page_info.full_name).split('.')[-1])
  if page_info.bases:
    parts.append('Inherits From: ')

    link_template = '[`{short_name}`]({url})'
    parts.append(', '.join(
        link_template.format(**base._asdict()) for base in page_info.bases))

  parts.append('\n\n')

  # Sort the methods list, but make sure constructors come first.
  constructor_names = ['__init__', '__new__']
  constructors = sorted(
      method for method in page_info.methods
      if method.short_name in constructor_names)
  other_methods = sorted(
      method for method in page_info.methods
      if method.short_name not in constructor_names)

  parts.append(_build_aliases(page_info.aliases))

  if page_info.defined_in is not None:
    parts.append('\n\n')
    parts.append(str(page_info.defined_in))

  parts.append(page_info.guides)
  parts.append(page_info.doc.docstring)
  parts.append(_build_function_details(page_info.doc.function_details))
  parts.append(_build_compatibility(page_info.doc.compatibility))

  parts.append('\n\n')

  if constructors:
    for method_info in constructors:
      parts.append(_build_method_section(method_info, heading_level=2))
    parts.append('\n\n')

  if page_info.classes:
    parts.append('## Child Classes\n')

    link_template = ('[`class {class_info.short_name}`]'
                     '({class_info.url})\n\n')
    class_links = sorted(
        link_template.format(class_info=class_info)
        for class_info in page_info.classes)

    parts.extend(class_links)

  if page_info.properties:
    parts.append('## Properties\n\n')
    for prop_info in page_info.properties:
      h3 = '<h3 id="{short_name}"><code>{short_name}</code></h3>\n\n'
      parts.append(h3.format(short_name=prop_info.short_name))

      parts.append(prop_info.doc.docstring)
      parts.append(_build_function_details(prop_info.doc.function_details))
      parts.append(_build_compatibility(prop_info.doc.compatibility))

      parts.append('\n\n')

    parts.append('\n\n')

  if other_methods:
    parts.append('## Methods\n\n')

    for method_info in other_methods:
      parts.append(_build_method_section(method_info))
    parts.append('\n\n')

  if page_info.other_members:
    parts.append('## Class Members\n\n')

    # TODO(markdaoust): Document the value of the members,
    #                   at least for basic types.

    h3 = '<h3 id="{short_name}"><code>{short_name}</code></h3>\n\n'
    others_member_headings = (h3.format(short_name=info.short_name)
                              for info in sorted(page_info.other_members))
    parts.extend(others_member_headings)

  return ''.join(parts)


def _build_method_section(method_info, heading_level=3):
  """Generates a markdown section for a method.

  Args:
    method_info: A `MethodInfo` object.
    heading_level: An Int, which HTML heading level to use.

  Returns:
    A markdown string.
  """
  parts = []
  heading = ('<h{heading_level} id="{short_name}">'
             '<code>{short_name}</code>'
             '</h{heading_level}>\n\n')
  parts.append(heading.format(heading_level=heading_level,
                              **method_info._asdict()))

  if method_info.signature is not None:
    parts.append(_build_signature(method_info, use_full_name=False))

  parts.append(method_info.doc.docstring)
  parts.append(_build_function_details(method_info.doc.function_details))
  parts.append(_build_compatibility(method_info.doc.compatibility))
  parts.append('\n\n')
  return ''.join(parts)


def _build_module_page(page_info):
  """Given a ClassPageInfo object Return the page as an md string."""
  parts = ['# Module: {full_name}\n\n'.format(full_name=page_info.full_name)]

  parts.append(_build_aliases(page_info.aliases))

  if page_info.defined_in is not None:
    parts.append('\n\n')
    parts.append(str(page_info.defined_in))

  parts.append(page_info.doc.docstring)
  parts.append(_build_compatibility(page_info.doc.compatibility))

  parts.append('\n\n')

  if page_info.modules:
    parts.append('## Modules\n\n')
    template = '[`{short_name}`]({url}) module'

    for item in page_info.modules:
      parts.append(template.format(**item._asdict()))

      if item.doc.brief:
        parts.append(': ' + six.ensure_str(item.doc.brief))

      parts.append('\n\n')

  if page_info.classes:
    parts.append('## Classes\n\n')
    template = '[`class {short_name}`]({url})'

    for item in page_info.classes:
      parts.append(template.format(**item._asdict()))

      if item.doc.brief:
        parts.append(': ' + six.ensure_str(item.doc.brief))

      parts.append('\n\n')

  if page_info.functions:
    parts.append('## Functions\n\n')
    template = '[`{short_name}(...)`]({url})'

    for item in page_info.functions:
      parts.append(template.format(**item._asdict()))

      if item.doc.brief:
        parts.append(': ' + six.ensure_str(item.doc.brief))

      parts.append('\n\n')

  if page_info.other_members:
    # TODO(markdaoust): Document the value of the members,
    #                   at least for basic types.
    parts.append('## Other Members\n\n')

    h3 = '<h3 id="{short_name}"><code>{short_name}</code></h3>\n\n'
    for item in page_info.other_members:
      parts.append(h3.format(**item._asdict()))

  return ''.join(parts)


def _build_signature(obj_info, use_full_name=True):
  """Returns a md code block showing the function signature."""
  # Special case tf.range, since it has an optional first argument
  if obj_info.full_name == 'tf.range':
    return (
        '``` python\n'
        "tf.range(limit, delta=1, dtype=None, name='range')\n"
        "tf.range(start, limit, delta=1, dtype=None, name='range')\n"
        '```\n\n')

  parts = ['``` python']
  parts.extend(['@' + six.ensure_str(dec) for dec in obj_info.decorators])
  signature_template = '{name}({sig})'

  if not obj_info.signature:
    sig = ''
  elif len(obj_info.signature) == 1:
    sig = obj_info.signature[0]
  else:
    sig = ',\n'.join('    %s' % sig_item for sig_item in obj_info.signature)
    sig = '\n'+sig+'\n'

  if use_full_name:
    obj_name = obj_info.full_name
  else:
    obj_name = obj_info.short_name
  parts.append(signature_template.format(name=obj_name, sig=sig))
  parts.append('```\n\n')

  return '\n'.join(parts)


def _build_compatibility(compatibility):
  """Return the compatibility section as an md string."""
  parts = []
  sorted_keys = sorted(compatibility.keys())
  for key in sorted_keys:

    value = compatibility[key]
    # Dedent so that it does not trigger markdown code formatting.
    value = textwrap.dedent(value)
    parts.append('\n\n#### %s Compatibility\n%s\n' % (key.title(), value))

  return ''.join(parts)


def _build_function_details(function_details):
  """Return the function details section as an md string."""
  parts = []
  for detail in function_details:
    sub = []
    sub.append('#### ' + six.ensure_str(detail.keyword) + ':\n\n')
    sub.append(textwrap.dedent(detail.header))
    for key, value in detail.items:
      sub.append('* <b>`%s`</b>: %s' % (key, value))
    parts.append(''.join(sub))

  return '\n'.join(parts)


def _build_aliases(aliases):
  aliases = sorted(aliases, key=lambda x: ('compat.v' in x, x))
  parts = []
  if len(aliases) > 1:
    parts.append('**Aliases**: ')
    parts.extend(', '.join('`{}`'.format(name) for name in aliases))
    parts.append('\n\n')

  return ''.join(parts)
