# Copyright 2012 Google Inc. All Rights Reserved.

"""Utility module for converting NDB properties to ProtoRPC messages/fields.

In the dictionary NDB_PROPERTY_TO_PROTO, each property defined by NDB is
registered. The registry values can either be a ProtoRPC field for simple
types/properties or a custom method for converting a property into a
ProtoRPC field.

Some properties have no corresponding implementation. These fields are
registered with a method that will raise a NotImplementedError. As of right now,
these are:
  Property -- this is the base property class and shouldn't be used
  GenericProperty -- this does not play nicely with strongly typed messages
  ModelKey -- this is only intended for the key of the instance, and doesn't
              make sense to send in messages
  ComputedProperty -- a variant of this class is needed to determine the type
                      desired of the output. Such a variant is provided in
                      properties
"""

from .. import utils

from protorpc import messages

from google.appengine.ext import ndb
from google.appengine.ext.ndb import msgprop


__all__ = []


GeoPtMessage = utils.GeoPtMessage
RaiseNotImplementedMethod = utils.RaiseNotImplementedMethod
UserMessage = utils.UserMessage

MODEL_KEY_EXPLANATION = (
    'A model key property can\'t be used to define an EndpointsModel. These '
    'are intended to be used as the lone key of an entity and all ModelKey '
    'properties on an entity will have the same value.')
COMPUTED_PROPERTY_EXPLANATION = (
    'A computed property can\'t be used to define an EndpointsModel. The type '
    'of the message field must be explicitly named; this can be done by using '
    'the property EndpointsComputedProperty.')
JSON_PROPERTY_EXPLANATION = (
    'A JSON property should not be used to define an EndpointsModel. Similar '
    'functionality can be added by using a string property. For a nested '
    'schema we recommend using ndb.StructuredProperty with other '
    'EndpointsModel subclasses.')

NDB_PROPERTY_TO_PROTO = {
    ndb.BlobKeyProperty: messages.StringField,
    ndb.BlobProperty: messages.BytesField,  # No concept of compressed here
    ndb.BooleanProperty: messages.BooleanField,
    ndb.ComputedProperty: RaiseNotImplementedMethod(
        ndb.ComputedProperty,
        explanation=COMPUTED_PROPERTY_EXPLANATION),
    ndb.DateProperty: messages.StringField,
    ndb.DateTimeProperty: messages.StringField,
    ndb.FloatProperty: messages.FloatField,
    ndb.GenericProperty: RaiseNotImplementedMethod(ndb.GenericProperty),
    ndb.IntegerProperty: messages.IntegerField,
    ndb.JsonProperty: RaiseNotImplementedMethod(
      ndb.JsonProperty,
      explanation=JSON_PROPERTY_EXPLANATION),
    ndb.KeyProperty: messages.StringField,
    ndb.ModelKey: RaiseNotImplementedMethod(
        ndb.ModelKey,
        explanation=MODEL_KEY_EXPLANATION),
    ndb.PickleProperty: messages.StringField,
    ndb.Property: RaiseNotImplementedMethod(ndb.Property),
    ndb.StringProperty: messages.StringField,
    ndb.TextProperty: messages.StringField,  # No concept of compressed here
    ndb.TimeProperty: messages.StringField,
}


def GetKeywordArgs(prop, include_default=True):
  """Captures attributes from an NDB property to be passed to a ProtoRPC field.

  Args:
    prop: The NDB property which will have its attributes captured.
    include_default: An optional boolean indicating whether or not the default
        value of the property should be included. Defaults to True, and is
        intended to be turned off for special ProtoRPC fields which don't take
        a default.

  Returns:
    A dictionary of attributes, intended to be passed to the constructor of a
        ProtoRPC field as keyword arguments.
  """
  kwargs = {
      'required': prop._required,
      'repeated': prop._repeated,
  }
  if include_default and hasattr(prop, '_default'):
    kwargs['default'] = prop._default
  if hasattr(prop, '_variant'):
    kwargs['variant'] = prop._variant
  return kwargs


def MessageFromSimpleField(field, prop, index):
  """Converts a property to the corresponding field of specified type.

  Assumes index is the only positional argument needed to create an instance
  of {field}, hence only simple fields will work and an EnumField or
  MessageField will fail.

  Args:
    field: A ProtoRPC field type.
    prop: The NDB property to be converted.
    index: The index of the property within the message.

  Returns:
    An instance of field with attributes corresponding to those in prop and
        index corresponding to that which was passed in.
  """
  return field(index, **GetKeywordArgs(prop))


def StructuredPropertyToProto(prop, index):
  """Converts a structured property to the corresponding message field.

  Args:
    prop: The NDB property to be converted.
    index: The index of the property within the message.

  Returns:
    A message field with attributes corresponding to those in prop, index
        corresponding to that which was passed in and with underlying message
        class equal to the message class produced by the model class, which
        should be a subclass of EndpointsModel.

  Raises:
    TypeError if the model class of the property does not have a callable
        ProtoModel method. This is because we expected a subclass of
        EndpointsModel set on the structured property.
  """
  modelclass = prop._modelclass
  try:
    property_proto_method = modelclass.ProtoModel
    property_proto = property_proto_method()
  except (AttributeError, TypeError):
    error_msg = ('Structured properties must receive a model class with a '
                 'callable ProtoModel attribute. The class %s has no such '
                 'attribute.' % (modelclass.__name__,))
    raise TypeError(error_msg)

  # No default for {MessageField}s
  kwargs = GetKeywordArgs(prop, include_default=False)
  return messages.MessageField(property_proto, index, **kwargs)
NDB_PROPERTY_TO_PROTO[ndb.StructuredProperty] = StructuredPropertyToProto
# Ignore fact that LocalStructuredProperty is just a blob in the datastore
NDB_PROPERTY_TO_PROTO[ndb.LocalStructuredProperty] = StructuredPropertyToProto


def EnumPropertyToProto(prop, index):
  """Converts an enum property from a model to a message field.

  Args:
    prop: The NDB enum property to be converted.
    index: The index of the property within the message.

  Returns:
    An enum field with attributes corresponding to those in prop, index
        corresponding to that which was passed in and with underlying enum type
        equal to the enum type set in the enum property.
  """
  enum = prop._enum_type
  kwargs = GetKeywordArgs(prop)
  return messages.EnumField(enum, index, **kwargs)
NDB_PROPERTY_TO_PROTO[msgprop.EnumProperty] = EnumPropertyToProto


def MessagePropertyToProto(prop, index):
  """Converts a message property from a model to a message field.

  Args:
    prop: The NDB message property to be converted.
    index: The index of the property within the message.

  Returns:
    A message field with attributes corresponding to those in prop, index
        corresponding to that which was passed in and with underlying message
        class equal to the message type set in the message property.
  """
  message_type = prop._message_type
  # No default for {MessageField}s
  kwargs = GetKeywordArgs(prop, include_default=False)
  return messages.MessageField(message_type, index, **kwargs)
NDB_PROPERTY_TO_PROTO[msgprop.MessageProperty] = MessagePropertyToProto


def GeoPtPropertyToProto(prop, index):
  """Converts a model property to a Geo Point message field.

  Args:
    prop: The NDB property to be converted.
    index: The index of the property within the message.

  Returns:
    A message field with attributes corresponding to those in prop, index
        corresponding to that which was passed in and with underlying message
        class equal to GeoPtMessage.
  """
  # No default for {MessageField}s
  kwargs = GetKeywordArgs(prop, include_default=False)
  return messages.MessageField(GeoPtMessage, index, **kwargs)
NDB_PROPERTY_TO_PROTO[ndb.GeoPtProperty] = GeoPtPropertyToProto


def UserPropertyToProto(prop, index):
  """Converts a model property to a user message field.

  Args:
    prop: The NDB property to be converted.
    index: The index of the property within the message.

  Returns:
    A message field with attributes corresponding to those in prop, index
        corresponding to that which was passed in and with underlying message
        class equal to UserMessage.
  """
  # No default for {MessageField}s
  kwargs = GetKeywordArgs(prop, include_default=False)
  return messages.MessageField(UserMessage, index, **kwargs)
NDB_PROPERTY_TO_PROTO[ndb.UserProperty] = UserPropertyToProto
