# -*- coding: utf-8 -*- #
# Copyright 2013 Google LLC. 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.

"""Generate usage text for displaying to the user."""


import argparse
import collections
import copy
import difflib
import enum
import io
import re
import sys
import textwrap

from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import arg_parsers_usage_text
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import parser_arguments
from googlecloudsdk.calliope.concepts import util as format_util
import six

LINE_WIDTH = 80
HELP_INDENT = 25
# Used to offset second-line indentation of arg choices in markdown.
_CHOICE_OFFSET = 3
_ARG_DETAILS_OFFSET = 1


class HelpInfo(object):
  """A class to hold some the information we need to generate help text."""

  def __init__(self, help_text, is_hidden, release_track):
    """Create a HelpInfo object.

    Args:
      help_text: str, The text of the help message.
      is_hidden: bool, True if this command or group has been marked as hidden.
      release_track: calliope.base.ReleaseTrack, The maturity level of this
        command.
    """
    self.help_text = help_text or ''
    self.is_hidden = is_hidden
    self.release_track = release_track


class TextChoiceSuggester(object):
  """Utility to suggest mistyped commands."""

  def __init__(self, choices=None):
    # A mapping of 'thing typed' to the suggestion that should be offered.
    # Often, these will be the same, but this allows for offering more currated
    # suggestions for more commonly misused things.
    self._choices = {}
    if choices:
      self.AddChoices(choices)

  def AddChoices(self, choices):
    """Add a set of valid things that can be suggested.

    Args:
      choices: [str], The valid choices.
    """
    for choice in choices:
      if choice not in self._choices:
        # Keep the first choice mapping that was added so later aliases don't
        # clobber real choices.
        self._choices[choice] = choice

  def AddAliases(self, aliases, suggestion):
    """Add an alias that is not actually a valid choice, but will suggest one.

    This should be called after AddChoices() so that aliases will not clobber
    any actual choices.

    Args:
      aliases: [str], The aliases for the valid choice.  This is something
        someone will commonly type when they actually mean something else.
      suggestion: str, The valid choice to suggest.
    """
    for alias in aliases:
      if alias not in self._choices:
        self._choices[alias] = suggestion

  def GetSuggestion(self, arg):
    """Find the item that is closest to what was attempted.

    Args:
      arg: str, The argument provided.

    Returns:
      str, The closest match.
    """
    if not self._choices:
      return None

    match = difflib.get_close_matches(
        arg.lower(), [six.text_type(c) for c in self._choices], 1
    )
    if match:
      choice = [c for c in self._choices if six.text_type(c) == match[0]][0]
      return self._choices[choice]
    return self._choices[match[0]] if match else None


class ArgumentWrapper(parser_arguments.Argument):
  pass


def _ApplyMarkdownItalic(msg):
  return re.sub(
      r'(\b[a-zA-Z][-a-zA-Z_0-9]*)',
      base.MARKDOWN_ITALIC + r'\1' + base.MARKDOWN_ITALIC,
      msg,
  )


def GetPositionalUsage(arg, markdown=False):
  """Create the usage help string for a positional arg.

  Args:
    arg: parser_arguments.Argument, The argument object to be displayed.
    markdown: bool, If true add markdowns.

  Returns:
    str, The string representation for printing.
  """
  var = arg.metavar or arg.dest.upper()
  if markdown:
    var = _ApplyMarkdownItalic(var)
  if arg.nargs == '+':
    return '{var} [{var} ...]'.format(var=var)
  elif arg.nargs == '*':
    return '[{var} ...]'.format(var=var)
  elif arg.nargs == argparse.REMAINDER:
    return '[-- {var} ...]'.format(var=var)
  elif arg.nargs == '?':
    return '[{var}]'.format(var=var)
  else:
    return var


def _GetFlagMetavar(flag, metavar=None, name=None, markdown=False):
  """Returns a usage-separator + metavar for flag."""
  if metavar is None:
    metavar = flag.metavar or flag.dest.upper()
  separator = '=' if name and name.startswith('--') else ' '
  if isinstance(flag.type, arg_parsers_usage_text.ArgTypeUsage):
    metavar = flag.type.GetUsageMetavar(bool(flag.metavar), metavar) or metavar
  if metavar == ' ':
    return ''
  if markdown:
    metavar = _ApplyMarkdownItalic(metavar)
  if separator == '=':
    metavar = separator + metavar
    separator = ''
  if flag.nargs in ('?', '*'):
    metavar = '[' + metavar + ']'
    separator = ''
  return separator + metavar


def _QuoteValue(value):
  """Returns value quoted, with preference for "..."."""
  quoted = repr(value)
  if quoted.startswith("'") and '"' not in value:
    quoted = '"' + quoted[1:-1] + '"'
  return quoted


def _FilterFlagNames(names):
  """Mockable flag name list filter."""
  return names


class InvertedValue(enum.Enum):
  NORMAL = 0
  INVERTED = 1
  BOTH = 2


def GetFlagUsage(
    arg, brief=False, markdown=False, inverted=InvertedValue.NORMAL, value=True
):
  """Returns the usage string for a flag arg.

  Args:
    arg: parser_arguments.Argument, The argument object to be displayed.
    brief: bool, If true, only display one version of a flag that has multiple
      versions, and do not display the default value.
    markdown: bool, If true add markdowns.
    inverted: InvertedValue, If INVERTED display the --no-* inverted name. If
      NORMAL display the normal name. If BOTH, display both.
    value: bool, If true display flag name=value for non-Boolean flags.

  Returns:
    str, The string representation for printing.
  """
  if inverted is InvertedValue.BOTH:
    names = [x.replace('--', '--[no-]', 1) for x in sorted(arg.option_strings)]
  elif inverted is InvertedValue.INVERTED:
    names = [x.replace('--', '--no-', 1) for x in sorted(arg.option_strings)]
  else:
    names = sorted(arg.option_strings)
  names = _FilterFlagNames(names)
  metavar = arg.metavar or arg.dest.upper()
  if not value or brief:
    try:
      long_string = names[0]
    except IndexError:
      long_string = ''
    if not value or arg.nargs == 0:
      return long_string
    flag_metavar = _GetFlagMetavar(arg, metavar, name=long_string)
    return '{flag}{metavar}'.format(flag=long_string, metavar=flag_metavar)
  if arg.nargs == 0:
    if markdown:
      usage = ', '.join(
          [base.MARKDOWN_BOLD + x + base.MARKDOWN_BOLD for x in names]
      )
    else:
      usage = ', '.join(names)
  else:
    usage_list = []
    for name in names:
      flag_metavar = _GetFlagMetavar(arg, metavar, name=name, markdown=markdown)
      usage_list.append(
          '{bb}{flag}{be}{flag_metavar}'.format(
              bb=base.MARKDOWN_BOLD if markdown else '',
              flag=name,
              be=base.MARKDOWN_BOLD if markdown else '',
              flag_metavar=flag_metavar,
          )
      )
    usage = ', '.join(usage_list)
    if arg.default and not getattr(
        arg, 'is_required', getattr(arg, 'required', False)
    ):
      if isinstance(arg.default, list):
        default = ','.join(arg.default)
      elif isinstance(arg.default, dict):
        default = ','.join([
            '{0}={1}'.format(k, v)
            for k, v in sorted(six.iteritems(arg.default))
        ])
      else:
        default = arg.default
      default_text = _QuoteValue(default)
      # Use codeblock if necessary to display literal * _ ` markdown characters.
      if markdown and re.search(
          f'[{base.MARKDOWN_BOLD}{base.MARKDOWN_ITALIC}{base.MARKDOWN_CODE}]',
          default_text):
        default_text = '```{}```'.format(default_text)
      usage += '; default={0}'.format(default_text)
  return usage


def _GetInvertedFlagName(flag):
  """Returns the inverted flag name for flag."""
  return flag.option_strings[0].replace('--', '--no-', 1)


def _Punctuate(help_message):
  if not help_message or help_message.endswith('.'):
    return help_message
  else:
    return help_message + '.'


def _AppendExtraHelp(help_message, extra_help_message):
  """Appends extra_help_message into help_message."""
  if help_message in extra_help_message:
    return extra_help_message
  elif (newline_index := help_message.rfind('\n')) and help_message[
      newline_index + 1
  ] == ' ':
    # Preserve example markdown at end of help_message.
    # NOTE: punctuation is not added due to pre-existing inconsistencies.
    # Long-term, we should be more consistent in formatting.
    return f'{help_message}\n\n{extra_help_message}\n'
  elif help_message.rfind('\n\n') > 0:
    # help_message has multiple paragraphs. Put extra_help in a new
    # paragraph.
    return f'{_Punctuate(help_message)}\n\n{extra_help_message}'
  else:
    return f'{_Punctuate(help_message)} {extra_help_message}'


def GetArgDetails(arg, depth=0):
  """Returns the help message with autogenerated details for arg."""
  help_text = getattr(arg, 'hidden_help', arg.help)
  if callable(help_text):
    help_text = help_text()
  help_message = textwrap.dedent(help_text) if help_text else ''
  if arg.is_hidden:
    return help_message
  if arg.is_group or arg.is_positional:
    choices = None
    hidden_choices = None
  elif arg.choices:
    choices = arg.choices
    hidden_choices = getattr(arg, 'hidden_choices', [])
  else:
    try:
      choices = arg.type.choices
    except AttributeError:
      choices = None
      hidden_choices = None
    else:
      hidden_choices = getattr(arg.type, 'hidden_choices', [])
  extra_help = []
  if hasattr(arg, 'store_property'):
    prop, _, _ = arg.store_property
    # Don't add help if there's already explicit help.
    if six.text_type(prop) not in help_message:
      extra_help.append(
          'Overrides the default *{0}* property value'
          ' for this command invocation.'.format(prop)
      )
      # '?' in Boolean flag check to cover legacy choices={'true', 'false'}
      # flags. They are the only flags with nargs='?'. This would have been
      # much easier if argparse had a first class Boolean flag attribute.
      if prop.default and arg.nargs in (0, '?'):
        extra_help.append(
            'Use *{}* to disable.'.format(_GetInvertedFlagName(arg))
        )
  elif arg.is_group or arg.is_positional or arg.nargs:
    # Not a Boolean flag.
    pass
  elif arg.default is True:
    extra_help.append(
        'Enabled by default, use *{0}* to disable.'.format(
            _GetInvertedFlagName(arg)
        )
    )
  elif isinstance(arg, arg_parsers.StoreTrueFalseAction):
    # This would be a "tri-valued" (True, False, None) command.
    extra_help.append(
        'Use *{0}* to enable and *{1}* to disable.'.format(
            arg.option_strings[0], _GetInvertedFlagName(arg)
        )
    )
  if choices:
    metavar = arg.metavar or arg.dest.upper()
    if metavar != ' ':
      choices = getattr(arg, 'choices_help', choices)
      if len(choices) - len(hidden_choices) > 1:
        one_of = 'one of'
      else:
        # TBD I guess?
        one_of = '(only one value is supported)'
      if isinstance(choices, dict):
        choices_iteritems = six.iteritems(choices)
        if not isinstance(choices, collections.OrderedDict):
          choices_iteritems = sorted(choices_iteritems)
        choices = []
        for name, desc in choices_iteritems:
          if name in hidden_choices:
            continue
          dedented_desc = textwrap.dedent(desc)
          choice_help = '*{name}*{depth} {desc}'.format(
              name=name,
              desc=dedented_desc,
              depth=':' * (depth + _CHOICE_OFFSET),
          )
          choices.append(choice_help)
        # Append marker to indicate end of list.
        choices.append(':' * (depth + _CHOICE_OFFSET))
        extra_help.append(
            '_{metavar}_ must be {one_of}:\n\n{choices}\n\n'.format(
                metavar=metavar, one_of=one_of, choices='\n'.join(choices)
            )
        )
      else:
        extra_help.append(
            '_{metavar}_ must be {one_of}: {choices}.'.format(
                metavar=metavar,
                one_of=one_of,
                choices=', '.join([
                    '*{0}*'.format(x) for x in choices
                    if x not in hidden_choices
                ]),
            )
        )

  arg_type = getattr(arg, 'type', None)
  if isinstance(arg_type, arg_parsers_usage_text.ArgTypeUsage):
    arg_name = arg.option_strings[0] if arg.option_strings else None
    field_name = arg_name and format_util.NamespaceFormat(arg_name)
    type_help_text = arg.type.GetUsageHelpText(
        field_name=field_name, required=arg.is_required, flag_name=arg_name
    )

    if type_help_text:
      extra_help.append(
          arg_parsers_usage_text.IndentAsciiDoc(
              type_help_text, depth + _ARG_DETAILS_OFFSET
          )
      )

  extra_help_message = ' '.join(extra_help)
  stripped_help = help_message.rstrip()

  if extra_help_message and stripped_help:
    all_help = _AppendExtraHelp(stripped_help, extra_help_message)
  elif extra_help_message:
    all_help = extra_help_message
  else:
    all_help = stripped_help

  return all_help.replace('\n\n', '\n+\n').strip()


def _IsPositional(arg):
  """Returns True if arg is a positional or group that contains a positional."""
  if arg.is_hidden:
    return False
  if arg.is_positional:
    return True
  if arg.is_group:
    for a in arg.arguments:
      if _IsPositional(a):
        return True
  return False


def _GetArgUsageSortKey(name):
  """Arg name usage string key function for sorted."""
  if not name:
    return 0, ''  # paranoid fail safe check -- should not happen
  elif name.startswith('--no-'):
    return 3, name[5:], 'x'  # --abc --no-abc
  elif name.startswith('--'):
    return 3, name[2:]
  elif name.startswith('-'):
    return 4, name[1:]
  elif name[0].isalpha():
    return 1, ''  # stable sort for positionals
  else:
    return 5, name


def GetSingleton(args):
  """Returns the single non-hidden arg in args.arguments or None."""
  singleton = None
  for arg in args.arguments:
    if arg.is_hidden:
      continue
    if arg.is_group:
      arg = GetSingleton(arg)
      if not arg:
        return None
    if singleton:
      return None
    singleton = arg

  if (
      singleton
      and not isinstance(args, ArgumentWrapper)
      and singleton.is_required != args.is_required
  ):
    singleton = copy.copy(singleton)
    singleton.is_required = args.is_required

  return singleton


def GetArgSortKey(arg):
  """Arg key function for sorted."""
  name = re.sub(
      ' +',
      ' ',
      re.sub('[](){}|[]', '', GetArgUsage(arg, value=False, hidden=True) or ''),
  )
  if arg.is_group:
    singleton = GetSingleton(arg)
    if singleton:
      arg = singleton
  if arg.is_group:
    if _IsPositional(arg):
      return 1, ''  # stable sort for positionals
    if arg.is_required:
      return 6, name
    return 7, name
  elif arg.nargs == argparse.REMAINDER:
    return 8, name
  if arg.is_positional:
    return 1, ''  # stable sort for positionals
  if arg.is_required:
    return 2, name
  return _GetArgUsageSortKey(name)


def _MarkOptional(usage):
  """Returns usage enclosed in [...] if it hasn't already been enclosed."""

  # If the leading bracket matches the trailing bracket its already marked.
  if re.match(r'^\[[^][]*(\[[^][]*\])*[^][]*\]$', usage):
    return usage
  return '[{}]'.format(usage)


def GetArgUsage(
    arg,
    brief=False,
    definition=False,
    markdown=False,
    optional=True,
    top=False,
    remainder_usage=None,
    value=True,
    hidden=False,
):
  """Returns the argument usage string for arg or all nested groups in arg.

  Mutually exclusive args names are separated by ' | ', otherwise ' '.
  Required groups are enclosed in '(...)', otherwise '[...]'. Required args
  in a group are separated from the optional args by ' : '.

  Args:
    arg: The argument to get usage from.
    brief: bool, If True, only display one version of a flag that has multiple
      versions, and do not display the default value.
    definition: bool, Definition list usage if True.
    markdown: bool, Add markdown if True.
    optional: bool, Include optional flags if True.
    top: bool, True if args is the top level group.
    remainder_usage: [str], Append REMAINDER usage here instead of the return.
    value: bool, If true display flag name=value for non-Boolean flags.
    hidden: bool, Include hidden args if True.

  Returns:
    The argument usage string for arg or all nested groups in arg.
  """
  if arg.is_hidden and not hidden:
    return ''
  if arg.is_group:
    singleton = GetSingleton(arg)
    if singleton and (
        singleton.is_group or singleton.nargs != argparse.REMAINDER
    ):
      arg = singleton
  if not arg.is_group:
    # A single argument.
    if arg.is_positional:
      usage = GetPositionalUsage(arg, markdown=markdown)
    else:
      if isinstance(arg, arg_parsers.StoreTrueFalseAction):
        inverted = InvertedValue.BOTH
      else:
        if not definition and getattr(arg, 'inverted_synopsis', False):
          inverted = InvertedValue.INVERTED
        else:
          inverted = InvertedValue.NORMAL
      usage = GetFlagUsage(
          arg, brief=brief, markdown=markdown, inverted=inverted, value=value
      )
    if usage and top and not arg.is_required:
      usage = _MarkOptional(usage)
    return usage

  # An argument group.
  sep = ' | ' if arg.is_mutex else ' '
  positional_args = []
  required_usage = []
  optional_usage = []
  if remainder_usage is None:
    include_remainder_usage = True
    remainder_usage = []
  else:
    include_remainder_usage = False
  arguments = (
      sorted(arg.arguments, key=GetArgSortKey)
      if arg.sort_args
      else arg.arguments
  )
  for a in arguments:
    if a.is_hidden and not hidden:
      continue
    if a.is_group:
      singleton = GetSingleton(a)
      if singleton:
        a = singleton
    if not a.is_group and a.nargs == argparse.REMAINDER:
      remainder_usage.append(
          GetArgUsage(a, markdown=markdown, value=value, hidden=hidden)
      )
    elif _IsPositional(a):
      positional_args.append(a)
    else:
      usage = GetArgUsage(a, markdown=markdown, value=value, hidden=hidden)
      if not usage:
        continue
      if a.is_required:
        if usage not in required_usage:
          required_usage.append(usage)
      else:
        if top:
          usage = _MarkOptional(usage)
        if usage not in optional_usage:
          optional_usage.append(usage)
  positional_usage = []
  all_other_usage = []
  nesting = 0
  optional_positionals = False
  if positional_args:
    nesting = 0
    for a in positional_args:
      usage = GetArgUsage(a, markdown=markdown, hidden=hidden)
      if not usage:
        continue
      if not a.is_required:
        optional_positionals = True
        usage_orig = usage
        usage = _MarkOptional(usage)
        if usage != usage_orig:
          nesting += 1
      positional_usage.append(usage)
    if nesting:
      positional_usage[-1] = '{}{}'.format(positional_usage[-1], ']' * nesting)
  if required_usage:
    all_other_usage.append(sep.join(required_usage))
  if optional_usage:
    if optional:
      if not top and (
          positional_args and not optional_positionals or required_usage
      ):
        all_other_usage.append(':')
      all_other_usage.append(sep.join(optional_usage))
    elif brief and top:
      all_other_usage.append('[optional flags]')
  if brief:
    all_usage = positional_usage + sorted(
        all_other_usage, key=_GetArgUsageSortKey
    )
  else:
    all_usage = positional_usage + all_other_usage
  if remainder_usage and include_remainder_usage:
    all_usage.append(' '.join(remainder_usage))
  usage = ' '.join(all_usage)
  if arg.is_required:
    return '({})'.format(usage)
  if not top and len(all_usage) > 1:
    usage = _MarkOptional(usage)
  return usage


def GetFlags(arg, optional=False):
  """Returns the list of all flags in arg.

  Args:
    arg: The argument to get flags from.
    optional: Do not include required flags if True.

  Returns:
    The list of all/optional flags in arg.
  """
  flags = set()
  if optional:
    flags.add('--help')

  def _GetFlagsHelper(arg, level=0, required=True):
    """GetFlags() helper that adds to flags."""
    if arg.is_hidden:
      return
    if arg.is_group:
      if level and required:
        # level==0 is always required
        required = arg.is_required
      for arg in arg.arguments:
        _GetFlagsHelper(arg, level=level + 1, required=required)
    else:
      show_inverted = getattr(arg, 'show_inverted', None)
      if show_inverted:
        arg = show_inverted
      # A singleton optional flag in a required group is technically required
      # but is treated as optional here. We shouldn't see this in practice.
      if (
          arg.option_strings
          and not arg.is_positional
          and not arg.is_global
          and (not optional or not required or not arg.is_required)
      ):
        flags.add(sorted(arg.option_strings)[0])

  _GetFlagsHelper(arg)
  return sorted(flags, key=_GetArgUsageSortKey)


class Section(object):
  """A positional/flag section.

  Attribute:
    heading: str, The section heading.
    args: [Argument], The sorted list of args in the section.
  """

  def __init__(self, heading, args):
    self.heading = heading
    self.args = args


def GetArgSections(arguments, is_root, is_group, sort_top_level_args):
  """Returns the positional/flag sections in document order.

  Args:
    arguments: [Flag|Positional], The list of arguments for this command or
      group.
    is_root: bool, True if arguments are for the CLI root command.
    is_group: bool, True if arguments are for a command group.
    sort_top_level_args: bool, True if top level arguments should be sorted.

  Returns:
    ([Section] global_flags)
      global_flags - The sorted list of global flags if command is not the root.
  """
  categories = collections.OrderedDict()
  dests = set()
  global_flags = set()
  if not is_root and is_group:
    global_flags = {'--help'}
  for arg in arguments:
    if arg.is_hidden:
      continue
    if _IsPositional(arg):
      category = 'POSITIONAL ARGUMENTS'
      if category not in categories:
        categories[category] = []
      categories[category].append(arg)
      continue
    if arg.is_global and not is_root:
      for a in arg.arguments if arg.is_group else [arg]:
        if a.option_strings and not a.is_hidden:
          flag = a.option_strings[0]
          if not is_group and flag.startswith('--'):
            global_flags.add(flag)
      continue
    if arg.is_required:
      category = 'REQUIRED'
    else:
      category = getattr(arg, 'category', None) or 'OTHER'
    if hasattr(arg, 'dest'):
      if arg.dest in dests:
        continue
      dests.add(arg.dest)
    if category not in categories:
      categories[category] = []
    categories[category].append(arg)

  # Collect the priority sections first in order:
  #   POSITIONAL ARGUMENTS, REQUIRED, COMMON
  # Followed by uncategorized / categorized:
  # * If the top level args are sorted, just put uncategorized first followed by
  #   the remaining categories in alphabetical order.
  # * If the top level args shouldn't be sorted, then use the insertion order of
  #   categories so as to mirror the top level args order.
  sections = []
  if is_root:
    common = 'GLOBAL'
  else:
    common = base.COMMONLY_USED_FLAGS
  if sort_top_level_args:
    initial_categories = ['POSITIONAL ARGUMENTS', 'REQUIRED', common, 'OTHER']
    remaining_categories = sorted(
        [c for c in categories if c not in initial_categories]
    )
  else:
    initial_categories = ['POSITIONAL ARGUMENTS', 'REQUIRED', common]
    remaining_categories = [
        c for c in categories if c not in initial_categories
    ]

  def _GetArgHeading(category):
    """Returns the arg section heading for an arg category."""
    if category == 'OTHER':
      # We can be more descriptive with the OTHER flags heading, depending on
      # what other categories are present.
      if set(remaining_categories) - set(['OTHER']):  # Additional categorized.
        other_flags_heading = 'FLAGS'
      elif common in categories:
        other_flags_heading = 'OTHER FLAGS'
      elif 'REQUIRED' in categories:
        other_flags_heading = 'OPTIONAL FLAGS'
      else:
        other_flags_heading = 'FLAGS'
      return other_flags_heading
    if 'ARGUMENTS' in category or 'FLAGS' in category:
      return category
    return category + ' FLAGS'

  for category in initial_categories + remaining_categories:
    if category not in categories:
      continue
    sections.append(
        Section(
            _GetArgHeading(category),
            ArgumentWrapper(
                arguments=categories[category], sort_args=sort_top_level_args
            ),
        )
    )

  return sections, global_flags


def WrapWithPrefix(prefix, message, indent, length, spacing, writer=sys.stdout):
  """Helper function that does two-column writing.

  If the first column is too long, the second column begins on the next line.

  Args:
    prefix: str, Text for the first column.
    message: str, Text for the second column.
    indent: int, Width of the first column.
    length: int, Width of both columns, added together.
    spacing: str, Space to put on the front of prefix.
    writer: file-like, Receiver of the written output.
  """

  def W(s):
    writer.write(s)

  def Wln(s):
    W(s + '\n')

  # Reformat the message to be of rows of the correct width, which is what's
  # left-over from length when you subtract indent. The first line also needs
  # to begin with the indent, but that will be taken care of conditionally.
  message = (
      ('\n%%%ds' % indent % ' ')
      .join(
          textwrap.TextWrapper(
              break_on_hyphens=False, width=length - indent
          ).wrap(message.replace(' | ', '&| '))
      )
      .replace('&|', ' |')
  )
  if len(prefix) > indent - len(spacing) - 2:
    # If the prefix is too long to fit in the indent width, start the message
    # on a new line after writing the prefix by itself.
    Wln('%s%s' % (spacing, prefix))
    # The message needs to have the first line indented properly.
    W('%%%ds' % indent % ' ')
    Wln(message)
  else:
    # If the prefix fits comfortably within the indent (2 spaces left-over),
    # print it out and start the message after adding enough whitespace to make
    # up the rest of the indent.
    W('%s%s' % (spacing, prefix))
    Wln(
        '%%%ds %%s' % (indent - len(prefix) - len(spacing) - 1) % (' ', message)
    )


def GetUsage(command, argument_interceptor):
  """Return the command Usage string.

  Args:
    command: calliope._CommandCommon, The command object that we're helping.
    argument_interceptor: parser_arguments.ArgumentInterceptor, the object that
      tracks all of the flags for this command or group.

  Returns:
    str, The command usage string.
  """
  command.LoadAllSubElements()
  command_path = ' '.join(command.GetPath())
  topic = len(command.GetPath()) >= 2 and command.GetPath()[1] == 'topic'
  command_id = 'topic' if topic else 'command'

  buf = io.StringIO()

  buf.write('Usage: ')

  usage_parts = []

  if not topic:
    usage_parts.append(
        GetArgUsage(argument_interceptor, brief=True, optional=False, top=True)
    )

  group_helps = command.GetSubGroupHelps()
  command_helps = command.GetSubCommandHelps()

  groups = sorted(
      name
      for (name, help_info) in six.iteritems(group_helps)
      if command.IsHidden() or not help_info.is_hidden
  )
  commands = sorted(
      name
      for (name, help_info) in six.iteritems(command_helps)
      if command.IsHidden() or not help_info.is_hidden
  )

  all_subtypes = []
  if groups:
    all_subtypes.append('group')
  if commands:
    all_subtypes.append(command_id)
  if groups or commands:
    usage_parts.append('<%s>' % ' | '.join(all_subtypes))
    optional_flags = None
  else:
    optional_flags = GetFlags(argument_interceptor, optional=True)

  usage_msg = ' '.join(usage_parts)

  non_option = '{command} '.format(command=command_path)

  buf.write(non_option + usage_msg + '\n')

  if groups:
    WrapWithPrefix(
        'group may be',
        ' | '.join(groups),
        HELP_INDENT,
        LINE_WIDTH,
        spacing='  ',
        writer=buf,
    )
  if commands:
    WrapWithPrefix(
        '%s may be' % command_id,
        ' | '.join(commands),
        HELP_INDENT,
        LINE_WIDTH,
        spacing='  ',
        writer=buf,
    )
  if optional_flags:
    WrapWithPrefix(
        'optional flags may be',
        ' | '.join(optional_flags),
        HELP_INDENT,
        LINE_WIDTH,
        spacing='  ',
        writer=buf,
    )

  buf.write('\n' + GetHelpHint(command))

  return buf.getvalue()


def GetCategoricalUsage(command, categories):
  """Constructs an alternative Usage markdown string organized into categories.

  The string is formatted as a series of tables; first, there's a table for
  each category of subgroups, next, there's a table for each category of
  subcommands. Each table element is printed under the category defined in the
  surface definition of the command or group with a short summary describing its
  functionality. In either set of tables (groups or commands), if there are no
  categories to display, there will be only be one table listing elements
  lexicographically. If both the sets of tables (groups and commands) have no
  categories to display, then an empty string is returned.

  Args:
    command: calliope._CommandCommon, The command object that we're helping.
    categories: A dictionary mapping category name to the set of elements
      belonging to that category.

  Returns:
    str, The command usage markdown string organized into categories.
  """

  command_key = 'command'
  command_group_key = 'command_group'

  def _WriteTypeUsageTextToBuffer(buf, categories, key_name):
    """Writes the markdown string to the buffer passed by reference."""
    single_category_is_other = False
    if (
        len(categories[key_name]) == 1
        and base.UNCATEGORIZED_CATEGORY in categories[key_name]
    ):
      single_category_is_other = True
    buf.write('\n\n')
    buf.write(
        '# Available {type}s for {group}:\n'.format(
            type=' '.join(key_name.split('_')),
            group=' '.join(command.GetPath()),
        )
    )
    for category, elements in sorted(six.iteritems(categories[key_name])):
      if not single_category_is_other:
        buf.write('\n### {category}\n\n'.format(category=category))
      buf.write('---------------------- | ---\n')
      for element in sorted(elements, key=lambda e: e.name):
        short_help = None
        if element.name == 'alpha':
          short_help = element.short_help[10:]
        elif element.name == 'beta':
          short_help = element.short_help[9:]
        elif element.name == 'preview':
          short_help = element.short_help[12:]
        else:
          short_help = element.short_help
        buf.write(
            '{name} | {description}\n'.format(
                name=element.name.replace('_', '-'), description=short_help
            )
        )

  def _ShouldCategorize(categories):
    """Ensures the categorization has real categories and is not just all Uncategorized."""
    if (
        not categories[command_key].keys()
        and not categories[command_group_key].keys()
    ):
      return False
    if set(
        list(categories[command_key].keys())
        + list(categories[command_group_key].keys())
    ) == set([base.UNCATEGORIZED_CATEGORY]):
      return False
    return True

  if not _ShouldCategorize(categories):
    return ''

  buf = io.StringIO()
  if command_group_key in categories:
    _WriteTypeUsageTextToBuffer(buf, categories, command_group_key)
  if command_key in categories:
    _WriteTypeUsageTextToBuffer(buf, categories, command_key)
  return buf.getvalue()


def _WriteUncategorizedTable(command, elements, element_type, writer):
  """Helper method to GetUncategorizedUsage().

  The elements are written to a markdown table with a special heading. Element
  names are printed in the first column, and help snippet text is printed in the
  second. No categorization is performed.

  Args:
    command: calliope._CommandCommon, The command object that we're helping.
    elements: an iterable over backend.CommandCommon, The sub-elements that
      we're printing to the table.
    element_type: str, The type of elements we are dealing with. Usually
      'groups' or 'commands'.
    writer: file-like, Receiver of the written output.
  """
  writer.write(
      '# Available {element_type} for {group}:\n'.format(
          element_type=element_type, group=' '.join(command.GetPath())
      )
  )
  writer.write('---------------------- | ---\n')
  for element in sorted(elements, key=lambda e: e.name):
    if element.IsHidden():
      continue
    writer.write(
        '{name} | {description}\n'.format(
            name=element.name.replace('_', '-'), description=element.short_help
        )
    )


def GetUncategorizedUsage(command):
  """Constructs a Usage markdown string for uncategorized command groups.

  The string is formatted as two tables, one for the subgroups and one for the
  subcommands. Each sub-element is printed in its corresponding table together
  with a short summary describing its functionality.

  Args:
    command: calliope._CommandCommon, the command object that we're helping.

  Returns:
    str, The command Usage markdown string as described above.
  """
  buf = io.StringIO()
  if command.groups:
    _WriteUncategorizedTable(command, command.groups.values(), 'groups', buf)

  if command.commands:
    buf.write('\n')
    _WriteUncategorizedTable(
        command, command.commands.values(), 'commands', buf
    )

  return buf.getvalue()


def GetHelpHint(command):
  return """\
For detailed information on this command and its flags, run:
  {command_path} --help
""".format(command_path=' '.join(command.GetPath()))


def ExtractHelpStrings(docstring):
  """Extracts short help and long help from a docstring.

  If the docstring contains a blank line (i.e., a line consisting of zero or
  more spaces), everything before the first blank line is taken as the short
  help string and everything after it is taken as the long help string. The
  short help is flowing text with no line breaks, while the long help may
  consist of multiple lines, each line beginning with an amount of whitespace
  determined by dedenting the docstring.

  If the docstring does not contain a blank line, the sequence of words in the
  docstring is used as both the short help and the long help.

  Corner cases: If the first line of the docstring is empty, everything
  following it forms the long help, and the sequence of words of in the long
  help (without line breaks) is used as the short help. If the short help
  consists of zero or more spaces, None is used instead. If the long help
  consists of zero or more spaces, the short help (which might or might not be
  None) is used instead.

  Args:
    docstring: The docstring from which short and long help are to be taken

  Returns:
    a tuple consisting of a short help string and a long help string
  """
  if docstring:
    unstripped_doc_lines = docstring.splitlines()
    stripped_doc_lines = [s.strip() for s in unstripped_doc_lines]
    try:
      empty_line_index = stripped_doc_lines.index('')
      short_help = ' '.join(stripped_doc_lines[:empty_line_index])
      raw_long_help = '\n'.join(unstripped_doc_lines[empty_line_index + 1 :])
      long_help = textwrap.dedent(raw_long_help).strip()
    except ValueError:  # no empty line in stripped_doc_lines
      short_help = ' '.join(stripped_doc_lines).strip()
      long_help = ''
    if not short_help:  # docstring started with a blank line
      short_help = ' '.join(stripped_doc_lines[empty_line_index + 1 :]).strip()
      # words of long help as flowing text
    return (short_help, long_help or short_help)
  else:
    return ('', '')
