# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""Classes to define how concept args are added to argparse.

A PresentationSpec is used to define how a concept spec is presented in an
individual command, such as its help text. ResourcePresentationSpecs are
used for resource specs.
"""


from googlecloudsdk.calliope.concepts import util
from googlecloudsdk.command_lib.util.concepts import info_holders


class PresentationSpec(object):
  """Class that defines how concept arguments are presented in a command.

  Attributes:
    name: str, the name of the main arg for the concept. Can be positional or
      flag style (UPPER_SNAKE_CASE or --lower-train-case).
    concept_spec: googlecloudsdk.calliope.concepts.ConceptSpec, The spec that
      specifies the concept.
    group_help: str, the help text for the entire arg group.
    prefixes: bool, whether to use prefixes before the attribute flags, such as
      `--myresource-project`.
    required: bool, whether the anchor argument should be required. If True, the
      command will fail at argparse time if the anchor argument isn't given.
    plural: bool, True if the resource will be parsed as a list, False
      otherwise.
    group: the parser or subparser for a Calliope command that the resource
      arguments should be added to. If not provided, will be added to the main
      parser.
    attribute_to_args_map: {str: str}, dict of attribute names to names of
      associated arguments.
    hidden: bool, True if the arguments should be hidden.
  """

  def __init__(self,
               name,
               concept_spec,
               group_help,
               prefixes=False,
               required=False,
               flag_name_overrides=None,
               plural=False,
               group=None,
               hidden=False):
    """Initializes a ResourcePresentationSpec.

    Args:
      name: str, the name of the main arg for the concept.
      concept_spec: googlecloudsdk.calliope.concepts.ConceptSpec, The spec that
        specifies the concept.
      group_help: str, the help text for the entire arg group.
      prefixes: bool, whether to use prefixes before the attribute flags, such
        as `--myresource-project`. This will match the "name" (in flag format).
      required: bool, whether the anchor argument should be required.
      flag_name_overrides: {str: str}, dict of attribute names to the desired
        flag name. To remove a flag altogether, use '' as its rename value.
      plural: bool, True if the resource will be parsed as a list, False
        otherwise.
      group: the parser or subparser for a Calliope command that the resource
        arguments should be added to. If not provided, will be added to the main
        parser.
      hidden: bool, True if the arguments should be hidden.
    """
    self.name = name
    self._concept_spec = concept_spec
    self.group_help = group_help
    self.prefixes = prefixes
    self.required = required
    self.plural = plural
    self.group = group
    self._attribute_to_args_map = self._GetAttributeToArgsMap(
        flag_name_overrides)
    self.hidden = hidden

  @property
  def concept_spec(self):
    """The ConceptSpec associated with the PresentationSpec.

    Returns:
      (googlecloudsdk.calliope.concepts.ConceptSpec) the concept spec.
    """
    return self._concept_spec

  @property
  def attribute_to_args_map(self):
    """The map of attribute names to associated args.

    Returns:
      {str: str}, the map.
    """
    return self._attribute_to_args_map

  def _GenerateInfo(self, fallthroughs_map):
    """Generate a ConceptInfo object for the ConceptParser.

    Must be overridden in subclasses.

    Args:
      fallthroughs_map: {str: [googlecloudsdk.calliope.concepts.deps.
        _FallthroughBase]}, dict keyed by attribute name to lists of
        fallthroughs.

    Returns:
      info_holders.ConceptInfo, the ConceptInfo object.
    """
    raise NotImplementedError

  def _GetAttributeToArgsMap(self, flag_name_overrides):
    """Generate a map of attributes to primary arg names.

    Must be overridden in subclasses.

    Args:
      flag_name_overrides: {str: str}, the dict of flags to overridden names.

    Returns:
      {str: str}, dict from attribute names to arg names.
    """
    raise NotImplementedError


class ResourcePresentationSpec(PresentationSpec):
  """Class that specifies how resource arguments are presented in a command."""

  def _ValidateFlagNameOverrides(self, flag_name_overrides):
    if not flag_name_overrides:
      return
    for attribute_name in flag_name_overrides.keys():
      for attribute in self.concept_spec.attributes:
        if attribute.name == attribute_name:
          break
      else:
        raise ValueError(
            'Attempting to override the name for an attribute not present in '
            'the concept: [{}]. Available attributes: [{}]'.format(
                attribute_name,
                ', '.join([attribute.name
                           for attribute in self.concept_spec.attributes])))

  def _GetAttributeToArgsMap(self, flag_name_overrides):
    self._ValidateFlagNameOverrides(flag_name_overrides)
    # Create a rename map for the attributes to their flags.
    attribute_to_args_map = {}
    for i, attribute in enumerate(self._concept_spec.attributes):
      is_anchor = i == len(self._concept_spec.attributes) - 1
      name = self.GetFlagName(
          attribute.name, self.name, flag_name_overrides, self.prefixes,
          is_anchor=is_anchor)
      if name:
        attribute_to_args_map[attribute.name] = name
    return attribute_to_args_map

  @staticmethod
  def GetFlagName(attribute_name, presentation_name, flag_name_overrides=None,
                  prefixes=False, is_anchor=False):
    """Gets the flag name for a given attribute name.

    Returns a flag name for an attribute, adding prefixes as necessary or using
    overrides if an override map is provided.

    Args:
      attribute_name: str, the name of the attribute to base the flag name on.
      presentation_name: str, the anchor argument name of the resource the
        attribute belongs to (e.g. '--foo').
      flag_name_overrides: {str: str}, a dict of attribute names to exact string
        of the flag name to use for the attribute. None if no overrides.
      prefixes: bool, whether to use the resource name as a prefix for the flag.
      is_anchor: bool, True if this it he anchor flag, False otherwise.

    Returns:
      (str) the name of the flag.
    """
    flag_name_overrides = flag_name_overrides or {}
    if attribute_name in flag_name_overrides:
      return flag_name_overrides.get(attribute_name)
    if attribute_name == 'project':
      return ''
    if is_anchor:
      return presentation_name
    prefix = util.PREFIX
    if prefixes:
      if presentation_name.startswith(util.PREFIX):
        prefix += presentation_name[len(util.PREFIX):] + '-'
      else:
        prefix += presentation_name.lower().replace('_', '-') + '-'
    return prefix + attribute_name

  def _GenerateInfo(self, fallthroughs_map):
    """Gets the ResourceInfo object for the ConceptParser.

    Args:
      fallthroughs_map: {str: [googlecloudsdk.calliope.concepts.deps.
        _FallthroughBase]}, dict keyed by attribute name to lists of
        fallthroughs.

    Returns:
      info_holders.ResourceInfo, the ResourceInfo object.
    """
    return info_holders.ResourceInfo(
        self.name,
        self.concept_spec,
        self.group_help,
        self.attribute_to_args_map,
        fallthroughs_map,
        required=self.required,
        plural=self.plural,
        group=self.group,
        hidden=self.hidden)

  def __eq__(self, other):
    if not isinstance(other, type(self)):
      return False
    return (self.name == other.name and
            self.concept_spec == other.concept_spec and
            self.group_help == other.group_help and
            self.prefixes == other.prefixes and self.plural == other.plural and
            self.required == other.required and self.group == other.group and
            self.hidden == other.hidden)


class InvalidPresentationSpecError(Exception):
  """Error for invalid presentation spec."""


# Currently no other type of multitype concepts have been implemented.
class MultitypeResourcePresentationSpec(PresentationSpec):
  """A resource-specific presentation spec."""

  def _GetAttributeToArgsMap(self, flag_name_overrides):
    # Create a rename map for the attributes to their flags.
    attribute_to_args_map = {}
    leaf_anchors = [a for a in self._concept_spec.attributes
                    if self._concept_spec.IsLeafAnchor(a)]

    if len(leaf_anchors) > 1 and util.IsPositional(self.name):
      anchor_names = ', '.join(a.name for a in leaf_anchors)
      raise InvalidPresentationSpecError(
          f'Multitype resource has anchors [{anchor_names}] and positional '
          f'name [{self.name}]. Multitype resource can only be non-positional '
          'or have a single anchor. Update multitype collections or change '
          f'the presentation name to {util.FlagNameFormat(self.name)}.')

    for attribute in self._concept_spec.attributes:
      is_anchor = [attribute] == leaf_anchors
      name = self.GetFlagName(
          attribute.name, self.name, flag_name_overrides=flag_name_overrides,
          prefixes=self.prefixes, is_anchor=is_anchor)
      if name:
        attribute_to_args_map[attribute.name] = name
    return attribute_to_args_map

  @staticmethod
  def GetFlagName(attribute_name, presentation_name, flag_name_overrides=None,
                  prefixes=False, is_anchor=False):
    """Gets the flag name for a given attribute name.

    Returns a flag name for an attribute, adding prefixes as necessary or using
    overrides if an override map is provided.

    Args:
      attribute_name: str, the name of the attribute to base the flag name on.
      presentation_name: str, the anchor argument name of the resource the
        attribute belongs to (e.g. '--foo').
      flag_name_overrides: {str: str}, a dict of attribute names to exact string
        of the flag name to use for the attribute. None if no overrides.
      prefixes: bool, whether to use the resource name as a prefix for the flag.
      is_anchor: bool, True if this is the anchor flag, False otherwise.

    Returns:
      (str) the name of the flag.
    """
    flag_name_overrides = flag_name_overrides or {}
    if attribute_name in flag_name_overrides:
      return flag_name_overrides.get(attribute_name)
    if is_anchor:
      return presentation_name
    if attribute_name == 'project':
      return ''

    if prefixes:
      return util.FlagNameFormat('-'.join([presentation_name, attribute_name]))
    else:
      return util.FlagNameFormat(attribute_name)

  def _GenerateInfo(self, fallthroughs_map):
    """Gets the MultitypeResourceInfo object for the ConceptParser.

    Args:
      fallthroughs_map: {str: [googlecloudsdk.calliope.concepts.deps.
        _FallthroughBase]}, dict keyed by attribute name to lists of
        fallthroughs.

    Returns:
      info_holders.MultitypeResourceInfo, the ResourceInfo object.
    """
    return info_holders.MultitypeResourceInfo(
        self.name,
        self.concept_spec,
        self.group_help,
        self.attribute_to_args_map,
        fallthroughs_map,
        required=self.required,
        plural=self.plural,
        group=self.group)

  def __eq__(self, other):
    if not isinstance(other, type(self)):
      return False
    return (self.name == other.name and
            self.concept_spec == other.concept_spec and
            self.group_help == other.group_help and
            self.prefixes == other.prefixes and self.plural == other.plural and
            self.required == other.required and self.group == other.group and
            self.hidden == other.hidden)
