# -*- 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.

"""Resource property Get."""

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

import json
import re

import six
from six.moves import range  # pylint: disable=redefined-builtin


_SNAKE_RE = re.compile(
    '((?<=[a-z0-9])[A-Z]+(?=[A-Z][a-z]|$)|(?!^)[A-Z](?=[a-z]))')


def _GetMetaDict(items, key, value):
  """Gets the dict in items that contains key==value.

  A metadict object is a list of dicts of the form:
    [
      {key: value-1, ...},
      {key: value-2, ...},
      ...
    ]

  Args:
    items: A list of dicts.
    key: The dict key name.
    value: The dict key value.

  Returns:
    The dict in items that contains key==value or None if no match or not a
    metadict.
  """
  try:
    for item in items:
      if item.get(key) == value:
        return item
  except (AttributeError, IndexError, TypeError, ValueError):
    pass
  return None


def _GetMetaDataValue(items, name, deserialize=False):
  """Gets the metadata value for the item in items with key == name.

  A metadata object is a list of dicts of the form:
    [
      {'key': key-name-1, 'value': field-1-value-string},
      {'key': key-name-2, 'value': field-2-value-string},
      ...
    ]

  Examples:
    x.metadata[windows-keys].email
      Deserializes the 'windows-keys' metadata value and gets the email value.
    x.metadata[windows-keys]
      Gets the 'windows-key' metadata string value.
    x.metadata[windows-keys][]
      Gets the deserialized 'windows-key' metadata value.

  Args:
    items: The metadata items list.
    name: The metadata name (which must match one of the 'key' values).
    deserialize: If True then attempt to deserialize a compact JSON string.

  Returns:
    The metadata value for name or None if not found or if items is not a
    metadata dict list.
  """
  item = _GetMetaDict(items, 'key', name)
  if item is None:
    return None
  value = item.get('value', None)
  if deserialize:
    try:
      return json.loads(value)
    except (TypeError, ValueError):
      pass
  return value


def ConvertToCamelCase(name):
  """Converts snake_case name to camelCase."""
  part = name.split('_')
  return part[0] + ''.join(x.title() for x in part[1:])


def ConvertToSnakeCase(name):
  """Converts camelCase name to snake_case."""
  return _SNAKE_RE.sub(r'_\1', name).lower()


def ConvertToAngrySnakeCase(name):
  """Converts camelCase name to ANGRY_SNAKE_CASE."""
  return _SNAKE_RE.sub(r'_\1', name).upper()


def GetMatchingIndex(index, func):
  """Returns index converted to a case that satisfies func."""
  if func(index):
    return index
  if not isinstance(index, six.string_types):
    return None
  for convert in [ConvertToCamelCase, ConvertToSnakeCase]:
    name = convert(index)
    if func(name):
      return name
  return None


def GetMatchingIndexValue(index, func):
  """Returns the first non-None func value for case-converted index."""
  value = func(index)
  if value:
    return value
  if not isinstance(index, six.string_types):
    return None
  for convert in [ConvertToCamelCase, ConvertToSnakeCase]:
    value = func(convert(index))
    if value:
      return value
  return None


def GetMessageFieldType(resource_key, message):
  """Returns the messages module type for key in message and the actual key.

  Handles camelCase/snake_case key name variants for OnePlatform compatibility.
  Indices and slices in resource_key are ignored -- they are not needed for
  repeated field queries.

  Args:
    resource_key: Ordered list of key names/indices, applied left to right. Each
      element in the list may be one of:
        str - A resource property name. This could be a class attribute name or
          a dict index.
        int - A list index. Selects one member is the list. Negative indices
          count from the end of the list, starting with -1 for the last element
          in the list. An out of bounds index is not an error; it produces the
          value None.
        None - A list slice. Selects all members of a list or dict like object.
          A slice of an empty dict or list is an empty dict or list.
    message: The known proto message type if not None.

  Raises:
    KeyError: If key is not in message.

  Returns:
    (type, actual_key), the messages module type for key in message and the
      actual key (names in the proper case, indices omitted).
  """
  actual_key = []
  for name in resource_key:
    if not isinstance(name, six.string_types):
      # Ignore indices and slices.
      continue
    for convert in (lambda x: x, ConvertToCamelCase, ConvertToSnakeCase):
      actual_name = convert(name)
      try:
        message = message.field_by_name(actual_name).type
      except (AttributeError, KeyError):
        pass
      else:
        break
    else:
      raise KeyError('Field {} not in message.'.format(name))
    actual_key.append(actual_name)
  if message == six.integer_types:
    # A distinction we don't need, especially since python 3 only has "int".
    # Also, dealing with a type tuple is a pain for callers.
    message = int
  return message, actual_key


def LookupField(resource_key, fields):
  """Returns the actual_key match of resource_key in fields.

  Handles camelCase/snake_case key name variants for OnePlatform compatibility.
  Indices and slices in resource_key are ignored to normalize the lookup. This
  means that the lookup can determine the existence of an attribute name, but
  not a specific value among all repeated values.

  Args:
    resource_key: Ordered list of key names/indices, applied left to right. Each
      element in the list may be one of:
        str - A resource property name. This could be a class attribute name or
          a dict index.
        int - A list index. Selects one member is the list. Negative indices
          count from the end of the list, starting with -1 for the last element
          in the list. An out of bounds index is not an error; it produces the
          value None.
        None - A list slice. Selects all members of a list or dict like object.
          A slice of an empty dict or list is an empty dict or list.
    fields: The set of dotted field names to match against.

  Returns:
    The actual_key match of resource_key in fields or None if no match.
  """
  for convert in (lambda x: x, ConvertToCamelCase, ConvertToSnakeCase):
    actual_key = [convert(name) if isinstance(name, six.string_types) else name
                  for name in resource_key]
    lookup_key = '.'.join([name for name in actual_key
                           if isinstance(name, six.string_types)])
    if lookup_key in fields:
      return actual_key
  return None


def Get(resource_obj, resource_key, default=None):
  """Gets the value referenced by key in the object resource.

  Since it is common for resource instances to be sparse it is not an error if
  a key is not present in a particular resource instance, or if an index does
  not match the resource type.

  Args:
    resource_obj: The resource object possibly containing a value for key.
    resource_key: Ordered list of key names/indices, applied left to right. Each
      element in the list may be one of:
        str - A resource property name. This could be a class attribute name or
          a dict index.
        int - A list index. Selects one member is the list. Negative indices
          count from the end of the list, starting with -1 for the last element
          in the list. An out of bounds index is not an error; it produces the
          value None.
        None - A list slice. Selects all members of a list or dict like object.
          A slice of an empty dict or list is an empty dict or list.
    default: Get() returns this value if key is not in resource.

  Returns:
    The value, None if any of the given keys are not found. This is
      intentionally not an error. In this context a value can be any data
      object: dict, list, tuple, class, str, int, float, ...
  """
  key = list(resource_key)
  resource = resource_obj
  while key:

    # Get the next key index. Some iterations may access the next key.
    index = key.pop(0)

    # Serialized sets are sorted lists.
    if isinstance(resource, set):
      resource = sorted(resource)

    # This if ordering checks builtin object attributes last. For
    # example, with resource = {'items': ...}, Get() treats 'items' as a dict
    # key rather than the builtin 'items' attribute of resource.

    if resource is None:
      # None is different than an empty dict or list.
      return default

    if hasattr(resource, 'items'):
      # dict-like
      if index is None:
        if key:
          # Inner slice: *.[].*
          return [Get(resource, [k] + key, default) for k in resource]
        # Trailing slice: *.[]
        return resource

      name = GetMatchingIndex(index, lambda x: x in resource)
      if name:
        resource = resource[name]
        continue

      if 'items' in resource:
        # It would be nice if there were a better metadata indicator.
        # _GetMetaDataValue() returns None if resource['items'] isn't really
        # metadata, so there is a bit more verification than just 'items' in
        # resource.

        def _GetValue(index):
          # pylint: disable=cell-var-from-loop
          return _GetMetaDataValue(
              resource['items'], index, deserialize=bool(key))

        resource = GetMatchingIndexValue(index, _GetValue)
        continue

      return default

    if isinstance(index, six.string_types):
      # class-like?
      name = GetMatchingIndex(index, lambda x: hasattr(resource, x))
      if name:
        r = getattr(resource, name, default)
        if not callable(r):
          resource = r
          continue

    if hasattr(resource, '__iter__') or isinstance(resource, six.string_types):
      # list-like
      if index is None:
        if key:
          # explicit inner slice: *.[].*
          return [Get(resource, [k] + key, default)
                  for k in range(len(resource))]
        # explicit trailing slice: *.[]
        return resource

      if not isinstance(index, six.integer_types):
        if isinstance(index, six.string_types) and isinstance(resource, list):
          if len(resource) and isinstance(resource[0], dict):
            if key:
              # See the _GetMetaDict docstring for the proto meta dict layout.
              r = _GetMetaDict(resource, index, key[0])
              if r is not None:
                # meta-dict-like
                resource = r
                index = key.pop(0)
                continue
            else:
              # Handle leaf node [{'key': k, 'value': v}, ...] metadata to
              # support r.k:v filter terms.
              r = _GetMetaDataValue(resource, index)
              if r is not None:
                return r
            if index in resource[0]:
              # implicit inner slice
              # resource is a list. An explicit reference would be
              # "resource[].foo" which would be caught above. Implicit
              # "resource.foo" references are handled here.
              return [Get(resource, [k, index] + key, default)
                      for k in range(len(resource))]

            # This is the last chance for index. If we fell through the index
            # would be ignored and the resource would be returned (incorrect).
            # Instead we return the list of non-None index values from the list
            # of dicts. See
            # resource_property_test.PropertyGetTest.testGetLastDictSlice for
            # an example.
            return ([f for f in [d.get(index) for d in resource] if f]
                    or default)

        # Index mismatch.
        return default

      if index in range(-len(resource), len(resource)):
        resource = resource[index]
        continue

    # Resource or index mismatch.
    return default

  # Sets serialize to sorted lists.
  if isinstance(resource, set):
    resource = sorted(resource)

  return resource


def EvaluateGlobalRestriction(resource, restriction, pattern):
  """Returns True if any attribute value in resource matches the RE pattern.

  This function is called to evaluate a global restriction on a resource. For
  example, --filter="Foo.Bar" results in a call like this on each resource item:

    resource_property.EvaluateGlobalRestriction(
      resource,
      'Foo.Bar',
      re.compile(re.escape('Foo.Bar'), re.IGNORECASE),
    )

  Args:
    resource: The object to check.
    restriction: The global restriction string.
    pattern: The global restriction pattern for matcing resource values.

  Returns:
    True if any attribute value in resource matches the RE pattern.
  """
  if not resource:
    return False
  if isinstance(resource, six.string_types):
    try:
      return bool(pattern.search(resource))
    except TypeError:
      pass
  if isinstance(resource, (float, int)):
    try:
      return bool(pattern.search(str(resource)))
    except TypeError:
      pass
  try:
    for key, value in six.iteritems(resource):
      if not key.startswith('_') and EvaluateGlobalRestriction(
          value, restriction, pattern):
        return True
  except AttributeError:
    try:
      for value in resource:
        if EvaluateGlobalRestriction(value, restriction, pattern):
          return True
      return False
    except TypeError:
      pass
  try:
    for key, value in six.iteritems(resource.__dict__):
      if not key.startswith('_') and EvaluateGlobalRestriction(
          value, restriction, pattern):
        return True
  except AttributeError:
    pass
  return False


def IsListLike(resource):
  """Checks if resource is a list-like iterable object.

  Args:
    resource: The object to check.

  Returns:
    True if resource is a list-like iterable object.
  """
  return (isinstance(resource, list) or
          (hasattr(resource, '__iter__') and
           (hasattr(resource, 'next') or hasattr(resource, '__next__'))))
