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

"""A module for walking the Cloud SDK CLI tree."""

from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

from typing import Any

from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.console import progress_tracker
import six


class Walker(object):
  """Base class for walking the Cloud SDK CLI tree.

  Attributes:
    _roots: The root elements of the CLI tree that will be walked.
    _num_nodes: The total number of nodes in the tree.
    _num_visited: The count of visited nodes so far.
    _progress_callback: The progress bar function to call to update progress.
  """

  def __init__(
      self, cli, progress_callback=None, ignore_load_errors=False, restrict=None
  ):
    """Constructor.

    Args:
      cli: The Cloud SDK CLI object.
      progress_callback: f(float), The function to call to update the progress
        bar or None for no progress bar.
      ignore_load_errors: bool, True to ignore command load failures. This
        should only be used when it is not critical that all data is returned,
        like for optimizations like static tab completion.
      restrict: Restricts the walk to the command/group dotted paths in this
        list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
        restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
        commands/groups. When provided here, any groups above the restrictions
        in the tree will not be loaded or visited.
    """
    top = cli._TopElement()  # pylint: disable=protected-access
    if restrict:
      roots = [self._GetSubElement(top, r) for r in restrict]
      self._roots = [r for r in roots if r]
    else:
      self._roots = [top]

    self._num_nodes = 0
    if progress_callback:
      with progress_tracker.ProgressTracker('Loading CLI Tree'):
        for root in self._roots:
          self._num_nodes += 1.0 + root.LoadAllSubElements(
              recursive=True, ignore_load_errors=ignore_load_errors
          )
    else:
      for root in self._roots:
        self._num_nodes += 1.0 + root.LoadAllSubElements(
            recursive=True, ignore_load_errors=ignore_load_errors
        )

    self._num_visited = 0
    self._progress_callback = (
        progress_callback or console_io.DefaultProgressBarCallback
    )

  def _GetSubElement(self, top_element, path):
    parts = path.split('.')[1:]
    current = top_element
    for part in parts:
      current = current.LoadSubElement(part)
      if not current:
        return None
    return current

  def Walk(self, hidden=False, universe_compatible=False, restrict=None):
    """Calls self.Visit() on each node in the CLI tree.

    The walk is DFS, ordered by command name for reproducability.

    Args:
      hidden: Include hidden groups and commands if True.
      universe_compatible: Exclusively include commands which are marked
        universe compatible.
      restrict: Restricts the walk to the command/group dotted paths in this
        list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
        restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
        commands/groups. When provided here, parent groups will still be visited
        as the walk progresses down to these leaves, but only parent groups
        between the restrictions and the root.

    Returns:
      The return value of the top level Visit() call.
    """

    def _IsUniverseCompatible(command: Any) -> bool:
      """Determines if a command is universe compatible.

      Args:
        command: CommandCommon command node.

      Returns:
        True if command is universe compatible.
      """
      return not isinstance(command, dict) and (command.IsUniverseCompatible())

    def _Include(command, traverse=False):
      """Determines if command should be included in the walk.

      Args:
        command: CommandCommon command node.
        traverse: If True then check traversal through group to subcommands.

      Returns:
        True if command should be included in the walk.
      """
      if not hidden and command.IsHidden():
        return False
      if universe_compatible and not _IsUniverseCompatible(command):
        return False
      if not restrict:
        return True
      path = '.'.join(command.GetPath())
      for item in restrict:
        if path.startswith(item):
          return True
        if traverse and item.startswith(path):
          return True
      return False

    def _Walk(node, parent):
      """Walk() helper that calls self.Visit() on each node in the CLI tree.

      Args:
        node: CommandCommon tree node.
        parent: The parent Visit() return value, None at the top level.

      Returns:
        The return value of the outer Visit() call.
      """
      if not node.is_group:
        self._Visit(node, parent, is_group=False)
        return parent

      parent = self._Visit(node, parent, is_group=True)
      commands_and_groups = []

      if node.commands:
        for name, command in six.iteritems(node.commands):
          if _Include(command):
            commands_and_groups.append((name, command, False))
      if node.groups:
        for name, command in six.iteritems(node.groups):
          if _Include(command, traverse=True):
            commands_and_groups.append((name, command, True))
      for _, command, is_group in sorted(commands_and_groups):
        if is_group:
          _Walk(command, parent)
        else:
          self._Visit(command, parent, is_group=False)
      return parent

    self._num_visited = 0
    parent = None
    for root in self._roots:
      parent = _Walk(root, None)
    self.Done()
    return parent

  def _Visit(self, node, parent, is_group):
    self._num_visited += 1
    self._progress_callback(self._num_visited // self._num_nodes)
    return self.Visit(node, parent, is_group)

  def Visit(self, node, parent, is_group):
    """Visits each node in the CLI command tree.

    Called preorder by WalkCLI() using DFS.

    Args:
      node: group/command CommandCommon info.
      parent: The parent Visit() return value, None at the top level.
      is_group: True if node is a group, otherwise its is a command.

    Returns:
      A new parent value for the node subtree. This value is the parent arg
      for the Visit() calls for the children of this node.
    """
    pass

  def Done(self):
    """Cleans up after all nodes in the CLI tree have been visited."""
    pass
