#!/usr/bin/env python
#
# Copyright 2007 Google LLC
#
# 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.
#




"""Utilities for converting between v3 and v1 datastore protocol buffers.

This module is internal and should not be used by client applications.
"""

import six














from google.appengine.datastore import datastore_v4_pb2
from google.appengine.datastore import entity_v4_pb2
from google.appengine.datastore import entity_bytes_pb2 as entity_pb2





_MIN_CLOUD_DATASTORE_VERSION = (6, 0, 0)
_CLOUD_DATASTORE_ENABLED = False

try:
  
  
  import googledatastore

  if googledatastore.VERSION >= _MIN_CLOUD_DATASTORE_VERSION:
    _CLOUD_DATASTORE_ENABLED = True
except ImportError:
  pass
except AttributeError:

  pass

MISSING_CLOUD_DATASTORE_MESSAGE = (
    'Could not import googledatastore. This library must be installed with '
    'version >= %s to use the Cloud Datastore API.' %
    '.'.join([str(v) for v in _MIN_CLOUD_DATASTORE_VERSION]))


MEANING_ATOM_CATEGORY = 1
MEANING_URL = 2
MEANING_ATOM_TITLE = 3
MEANING_ATOM_CONTENT = 4
MEANING_ATOM_SUMMARY = 5
MEANING_ATOM_AUTHOR = 6
MEANING_NON_RFC_3339_TIMESTAMP = 7
MEANING_GD_EMAIL = 8
MEANING_GEORSS_POINT = 9
MEANING_GD_IM = 10
MEANING_GD_PHONENUMBER = 11
MEANING_GD_POSTALADDRESS = 12
MEANING_PERCENT = 13
MEANING_TEXT = 15
MEANING_BYTESTRING = 16
MEANING_BLOBKEY = 17
MEANING_INDEX_ONLY = 18
MEANING_PREDEFINED_ENTITY_USER = 20
MEANING_PREDEFINED_ENTITY_POINT = 21
MEANING_ZLIB = 22
MEANING_POINT_WITHOUT_V3_MEANING = 23
MEANING_EMPTY_LIST = 24


URI_MEANING_ZLIB = 'ZLIB'



MAX_URL_CHARS = 2083
MAX_INDEXED_STRING_CHARS = 500
MAX_INDEXED_BLOB_BYTES = 500
MAX_PARTITION_ID_LENGTH = 100
MAX_DATASET_ID_SECTION_LENGTH = 100



MAX_DATASET_ID_LENGTH = MAX_DATASET_ID_SECTION_LENGTH * 3 + 2
MAX_KEY_PATH_LENGTH = 100


PROPERTY_NAME_X = 'x'
PROPERTY_NAME_Y = 'y'


PROPERTY_NAME_EMAIL = 'email'
PROPERTY_NAME_AUTH_DOMAIN = 'auth_domain'
PROPERTY_NAME_USER_ID = 'user_id'
PROPERTY_NAME_INTERNAL_ID = 'internal_id'
PROPERTY_NAME_FEDERATED_IDENTITY = 'federated_identity'
PROPERTY_NAME_FEDERATED_PROVIDER = 'federated_provider'


PROPERTY_NAME_KEY = '__key__'

DEFAULT_GAIA_ID = 0


RFC_3339_MIN_MICROSECONDS_INCLUSIVE = -62135596800 * 1000 * 1000
RFC_3339_MAX_MICROSECONDS_INCLUSIVE = 253402300799 * 1000 * 1000 + 999999


def v4_key_to_string(v4_key):
  """Generates a string representing a key's path.

  The output makes no effort to qualify special characters in strings.

  The key need not be valid, but if any of the key path elements have
  both a name and an ID the name is ignored.

  Args:
    v4_key: An `entity_v4_pb2.Key`.

  Returns:
    A string representing the key's path.
  """
  path_element_strings = []
  for path_element in v4_key.path_element:
    if path_element.HasField('id'):
      id_or_name = str(path_element.id)
    elif path_element.HasField('name'):
      id_or_name = path_element.name
    else:
      id_or_name = ''
    path_element_strings.append('%s: %s' % (path_element.kind, id_or_name))
  return '[%s]' % ', '.join(path_element_strings)


def is_complete_v4_key(v4_key):
  """Returns True if a key specifies an ID or name, False otherwise.

  Args:
    v4_key: An `entity_v4_pb2.Key`.

  Returns:
    `True` if the key specifies an ID or name, `False` otherwise.
  """
  assert len(v4_key.path_element) >= 1
  last_element = v4_key.path_element(len(v4_key.path_element) - 1)
  return last_element.HasField('id') or last_element.HasField('name')


def v1_key_to_string(v1_key):
  """Generates a string representing a key's path.

  The output makes no effort to qualify special characters in strings.

  The key need not be valid, but if any of the key path elements have
  both a name and an ID the name is ignored.

  Args:
    v1_key: An `googledatastore.Key`.

  Returns:
    A string representing the key's path.
  """
  path_element_strings = []
  for path_element in v1_key.path:
    field = path_element.WhichOneof('id_type')
    if field == 'id':
      id_or_name = str(path_element.id)
    elif field == 'name':
      id_or_name = path_element.name
    else:
      id_or_name = ''
    path_element_strings.append('%s: %s' % (path_element.kind, id_or_name))
  return '[%s]' % ', '.join(path_element_strings)


def is_complete_v1_key(v1_key):
  """Returns `True` if a key specifies an ID or name, `False` otherwise.

  Args:
    v1_key: An `googledatastore.Key`.

  Returns:
    `True` if the key specifies an ID or name, `False` otherwise.
  """
  assert len(v1_key.path) >= 1
  last_element = v1_key.path[len(v1_key.path) - 1]
  return last_element.WhichOneof('id_type') is not None


def is_complete_v3_key(v3_key):
  """Returns `True` if a key specifies an ID or name, `False` otherwise.

  Args:
    v3_key: A `datastore_pb.Reference`.

  Returns:
    `True` if the key specifies an ID or name, `False` otherwise.
  """
  assert len(v3_key.path.element) >= 1
  last_element = v3_key.path.element[-1]
  return ((last_element.HasField('id') and last_element.id) or
          (last_element.HasField('name') and last_element.name))


def get_v1_mutation_key_and_entity(v1_mutation):
  """Returns the v1 key and entity for a v1 mutation proto, if applicable.

  Args:
    v1_mutation: A `googledatastore.Mutation`.

  Returns:
    A tuple `(googledatastore.Key for this mutation,
             googledatastore.Entity or None if the mutation is a deletion)`.
  """
  if v1_mutation.HasField('delete'):
    return v1_mutation.delete, None
  else:
    v1_entity = getattr(v1_mutation, v1_mutation.WhichOneof('operation'))
    return v1_entity.key, v1_entity


def is_valid_utf8(s):
  if isinstance(s, six.text_type):
    return True
  try:
    s.decode('utf-8')
    return True
  except UnicodeDecodeError:
    return False


def check_conversion(condition, message):
  """Asserts a conversion condition and raises an error if it's not met.

  Args:
    condition: A boolean condition to enforce.
    message: Error message.

  Raises:
    InvalidConversionError: if condition is not met
  """
  if not condition:
    raise InvalidConversionError(message)


def is_in_rfc_3339_bounds(microseconds):
  return (RFC_3339_MIN_MICROSECONDS_INCLUSIVE <= microseconds
          <= RFC_3339_MAX_MICROSECONDS_INCLUSIVE)



class InvalidConversionError(Exception):
  """Raised when conversion fails."""
  pass


class IdResolver(object):
  """A class that can handle project id <--> application id transformations."""

  def __init__(self, app_ids=()):
    """Create a new `IdResolver`.

    Args:
     app_ids: A list of application ids with application id shard set. i.e.
       s~my_app or e~my_app.
    """
    resolver_map = {}
    for app_id in app_ids:
      resolver_map[self.resolve_project_id(app_id)] = app_id
    self._resolver_map = resolver_map

  def resolve_project_id(self, app_id):
    """Converts an application id to a project id.

    Args:
      app_id: The application id.
    Returns:
      The project id.
    """
    return app_id.rsplit('~')[-1]

  def resolve_app_id(self, project_id):
    """Converts a project id to an application id.

    Args:
      project_id: The project id.
    Returns:
      The application id.
    Raises:
      InvalidConversionError: if the application is unknown for the project id.
    """
    check_conversion(project_id in self._resolver_map,
                     'Cannot determine application id for provided project id: '
                     '"%s".'
                     % project_id)
    return self._resolver_map[project_id]


class _IdentityIdResolver(IdResolver):
  """An `IdResolver` that resolve app_id == project_id."""

  def resolve_project_id(self, app_id):
    return app_id

  def resolve_app_id(self, project_id):
    return project_id


class _EntityConverter(object):
  """Converter for entities and keys."""

  def __init__(self, id_resolver):
    """Creates a new `EntityConverter`.

    Args:
      id_resolver: An `IdResolver` object for converting.
        `project_id <--> application_id`.
    """
    self._id_resolver = id_resolver

  def v4_to_v3_reference(self, v4_key, v3_ref):
    """Converts a v4 Key to a v3 Reference.

    Args:
      v4_key: An `entity_v4_pb2.Key`.
      v3_ref: An `entity_pb2.Reference` to populate.
    """
    v3_ref.Clear()
    if v4_key.HasField('partition_id'):
      if v4_key.partition_id.HasField('dataset_id'):
        v3_ref.app = v4_key.partition_id.dataset_id
      if v4_key.partition_id.HasField('namespace'):
        v3_ref.name_space = v4_key.partition_id.namespace
    for v4_element in v4_key.path_element:
      v3_element = v3_ref.path.element.add()
      v3_element.type = v4_element.kind
      if v4_element.HasField('id'):
        v3_element.id = v4_element.id
      if v4_element.HasField('name'):
        v3_element.name = v4_element.name

  def v4_to_v3_references(self, v4_keys):
    """Converts a list of v4 Keys to a list of v3 References.