# -*- encoding: utf-8 -*-
# vim: sts=2:ts=2:sw=2

#
# Copyright 2008 The ndb Authors. 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.

"""MessageProperty -- a property storing ProtoRPC Message objects.

Basic usage:

Let's assume you have a protorpc.messages.Message subclass, like this:

  from protorpc import messages

  class Note(messages.Message):
    text = messages.StringField(1, required=True)
    when = messages.IntegerField(2)

Now suppose you'd like to store Notes in the datastore.  Create a
model class to hold your notes, as follows:

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

  class NoteStore(ndb.Model):
    note = msgprop.MessageProperty(Note)
    name = ndb.StringProperty()

(The class name, 'NoteStore', and the property name, 'note', are yours
to choose.)

To store, a Note message, create a NoteStore entity and write it:

  ns = NoteStore(note=my_note, name='foo')
  key = ns.put()

To later retrieve the Note, read the NoteStore entity back:

  ns = key.get()
  my_note = ns.note

The MessageProperty class has many of the usual Property options:
  - name: optional datastore name
  - repeated: if True, stores a list of message values
  - required: if True, the message value cannot be None
  - default: optional default message value to store
  - choices: optional list of allowed choices (must all be messages)
  - validator: optional function to validate message values
  - verbose_name: optional long name for the property

However, MessageProperty does not support the 'indexed' option.
Instead, you can specify a list of field names that will be indexed,
like this:

  class MyStore(ndb.Model):
    author = ndb.StringProperty()
    note = msgprop.MessageProperty(Note, indexed_fields=['text', 'when'])

Now you can query for field values, like this:

  stores = MyStore.query(MyStore.note.when >= 123).fetch()

Note the similarity with StructuredProperty -- in fact,
MessageProperty inherits from StructuredProperty.  The main difference
is that StructuredProperty takes an Model subclass instead of a
protorpc.messages.Message subclass; and StructuredProperty doesn't
index any fields by default.

It works for nested messages (using MessageField) as well:

  class Notes(messages.Message):
    notes = messages.MessageField(Note, 1, repeated=True)

  class MyNotesStore(ndb.Model):
    author = ndb.StringProperty()
    foo = msgprop.MessageProperty(Notes,
                                  indexed_fields=['notes.text, 'notes.when'])

And given this value for indexed_fields, in this example you can also
query for subfields:

  stores = MyNoteStore.query(MyNoteStore.foo.notes.when < 123).fetch()

A final option for MessageProperty is 'protocol'.  This lets you
specify how the message object is serialized to the datastore.  The
values are protocol names as used by protorpc.remote.Protocols class.
Supported protocol names are 'protobuf' and 'protojson'; the default
is 'protobuf'.  (In the future this will use the global protocols
registry that is being added to protorpc; then any registered protocol
name will be acceptable.)

There is also an EnumProperty, which can be used to store a
messages.Enum value without wrapping it in a Message object.  Example:

  class Color(messages.Enum):
    RED = 620
    GREEN = 495
    BLUE = 450

  class Part(ndb.Model):
    name = ndb.StringProperty()
    color = msgprop.EnumProperty(Color, required=True)

  p1 = Part(name='foo', color=Color.RED)
  key1 = p1.put()
  ...
  p2 = key1.get()
  print p2.name, p2.color  # prints "foo RED"

The EnumProperty stores the value as an integer; in fact, EnumProperty
is a subclass of IntegerProperty.  This is handy to know, since it
implies that you can rename your enum values without having to modify
already-stored entities, but you cannot renumber them.

The EnumProperty supports the following standard options:
  - name
  - indexed
  - repeated
  - required
  - default
  - choices
  - validator
  - verbose_name
"""

from protorpc import messages

from . import model
from . import utils

_default_protocol = 'protobuf'

class ProtocolProtobuf(object):

  @staticmethod
  def encode_message(msg):
    return msg.SerializeToString()

  @staticmethod
  def decode_message(msg_type, blob):
    recovered = msg_type()
    recovered.ParseFromString(blob)

    return recovered

class Protocols(object):
  def __init__(self):
    self.__by_name = {'protobuf': ProtocolProtobuf}

  def lookup_by_name(self, name):
    return self.__by_name[name.lower()]

  def add_protocol(self, protocol, name):
    self.__by_name[name] = protocol

  @property
  def names(self):
    return tuple(sorted(self.__by_name))

_protocols_registry = Protocols()


class EnumProperty(model.IntegerProperty):
  """Enums are represented in the datastore as integers.

  While this is less user-friendly in the Datastore viewer, it matches
  the representation of enums in the protobuf serialization (although
  not in JSON), and it allows renaming enum values without requiring
  changes to values already stored in the Datastore.
  """

  _enum_type = None

  # Insert enum_type as an initial positional argument.
  _attributes = ['_enum_type'] + model.IntegerProperty._attributes
  _positional = 1 + model.IntegerProperty._positional

  @utils.positional(1 + _positional)
  def __init__(self, enum_type, name=None, default=None, choices=None, **kwds):
    """Constructor.

    Args:
      enum_type: A subclass of protorpc.messages.Enum.
      name: Optional datastore name (defaults to the property name).

    Additional keywords arguments specify the same options as
    supported by IntegerProperty.
    """
    self._enum_type = enum_type
    if default is not None:
      self._validate(default)
    if choices is not None:
      list(map(self._validate, choices))
    super(EnumProperty, self).__init__(name, default=default,
                                       choices=choices, **kwds)

  def _validate(self, value):
    """Validate an Enum value.

    Raises:
      TypeError if the value is not an instance of self._enum_type.
    """
    if not isinstance(value, self._enum_type):
      raise TypeError('Expected a %s instance, got %r instead' %
                      (self._enum_type.__name__, value))

  def _to_base_type(self, enum):
    """Convert an Enum value to a base type (integer) value."""
    return enum.number

  def _from_base_type(self, val):
    """Convert a base type (integer) value to an Enum value."""
    return self._enum_type(val)


def _analyze_indexed_fields(indexed_fields):
  """Internal helper to check a list of indexed fields.

  Args:
    indexed_fields: A list of names, possibly dotted names.

  (A dotted name is a string containing names separated by dots,
  e.g. 'foo.bar.baz'.  An undotted name is a string containing no
  dots, e.g. 'foo'.)

  Returns:
    A dict whose keys are undotted names.  For each undotted name in
    the argument, the dict contains that undotted name as a key with
    None as a value.  For each dotted name in the argument, the dict
    contains the first component as a key with a list of remainders as
    values.

  Example:
    If the argument is ['foo.bar.baz', 'bar', 'foo.bletch'], the return
    value is {'foo': ['bar.baz', 'bletch'], 'bar': None}.

  Raises:
    TypeError if an argument is not a string.
    ValueError for duplicate arguments and for conflicting arguments
      (when an undotted name also appears as the first component of
      a dotted name).
  """
  result = {}
  for field_name in indexed_fields:
    if not isinstance(field_name, str):
      raise TypeError('Field names must be strings; got %r' % (field_name,))
    if '.' not in field_name:
      if field_name in result:
        raise ValueError('Duplicate field name %s' % field_name)
      result[field_name] = None
    else:
      head, tail = field_name.split('.', 1)
      if head not in result:
        result[head] = [tail]
      elif result[head] is None:
        raise ValueError('Field name %s conflicts with ancestor %s' %
                         (field_name, head))
      else:
        result[head].append(tail)
  return result


def _make_model_class(message_type, indexed_fields, **props):
  """Construct a Model subclass corresponding to a Message subclass.

  Args:
    message_type: A Message subclass.
    indexed_fields: A list of dotted and undotted field names.
    **props: Additional properties with which to seed the class.

  Returns:
    A Model subclass whose properties correspond to those fields of
    message_type whose field name is listed in indexed_fields, plus
    the properties specified by the **props arguments.  For dotted
    field names, a StructuredProperty is generated using a Model
    subclass created by a recursive call.

  Raises:
    Whatever _analyze_indexed_fields() raises.
    ValueError if a field name conflicts with a name in **props.
    ValueError if a field name is not valid field of message_type.
    ValueError if an undotted field name designates a MessageField.
  """
  analyzed = _analyze_indexed_fields(indexed_fields)
  for field_name, sub_fields in analyzed.items():
    if field_name in props:
      raise ValueError('field name %s is reserved' % field_name)
    try:
      field = message_type.field_by_name(field_name)
    except KeyError:
      raise ValueError('Message type %s has no field named %s' %
                       (message_type.__name__, field_name))
    if isinstance(field, messages.MessageField):
      if not sub_fields:
        raise ValueError(
          'MessageField %s cannot be indexed, only sub-fields' % field_name)
      sub_model_class = _make_model_class(field.type, sub_fields)
      prop = model.StructuredProperty(sub_model_class, field_name,
                                      repeated=field.repeated)
    else:
      if sub_fields is not None:
        raise ValueError(
          'Unstructured field %s cannot have indexed sub-fields' % field_name)
      if isinstance(field, messages.EnumField):
        prop = EnumProperty(field.type, field_name, repeated=field.repeated)
      elif isinstance(field, messages.BytesField):
        prop = model.BlobProperty(field_name,
                                  repeated=field.repeated, indexed=True)
      else:
        # IntegerField, FloatField, BooleanField, StringField.
        prop = model.GenericProperty(field_name, repeated=field.repeated)
    props[field_name] = prop
  return model.MetaModel('_%s__Model' % message_type.__name__,
                         (model.Model,), props)


class MessageProperty(model.StructuredProperty):
  """Messages are represented in the datastore as structured properties.

  By default, the structured property has a single subproperty
  containing the serialized message.  This property is named 'blob_'
  in Python but __<protocol>__ in the Datastore, where <protocol> is
  the value of the protocol argument (default 'protobuf').
  """

  _message_type = None
  _indexed_fields = ()
  _protocol = _default_protocol
  _protocol_impl = None

  # *Replace* first positional argument with _message_type, since the
  # _modelclass attribute is synthetic.
  _attributes = (['_message_type'] + model.StructuredProperty._attributes[1:] +
                 ['_indexed_fields', '_protocol'])

  @utils.positional(1 + model.StructuredProperty._positional)
  def __init__(self, message_type, name=None,
               indexed_fields=None, protocol=None, **kwds):
    """Constructor.

    Args:
      message_type: A subclass of protorpc.messages.Message.
      name: Optional datastore name (defaults to the property name).
      indexed_fields: Optional list of dotted and undotted field names.
      protocol: Optional protocol name default 'protobuf'.

    Additional keywords arguments specify the same options as
    supported by StructuredProperty, except 'indexed'.
    """
    if not isinstance(message_type, type):
      raise TypeError('Wrong class')
#     if not issubclass(message_type, messages.Message):
#       raise TypeError('MessageProperty argument must be a Message subclass')
    self._message_type = message_type
    if indexed_fields is not None:
      # TODO: Check they are all strings naming fields.
      self._indexed_fields = tuple(indexed_fields)
    # NOTE: Otherwise the class default i.e. (), prevails.
    if protocol is None:
      protocol = _default_protocol
    self._protocol = protocol
    self._protocol_impl = _protocols_registry.lookup_by_name(protocol)
    blob_prop = model.BlobProperty('__%s__' % self._protocol)
    # TODO: Solve this without reserving 'blob_'.
    message_class = _make_model_class(message_type, self._indexed_fields,
                                      blob_=blob_prop)
    super(MessageProperty, self).__init__(message_class, name, **kwds)

  def _validate(self, msg):
    """Validate an Enum value.

    Raises:
      TypeError if the value is not an instance of self._message_type.
    """
    if not isinstance(msg, self._message_type):
      raise TypeError('Expected a %s instance for %s property',
                      self._message_type.__name__,
                      self._code_name or self._name)

  def _to_base_type(self, msg):
    """Convert a Message value to a Model instance (entity)."""
    ent = _message_to_entity(msg, self._modelclass)
    ent.blob_ = self._protocol_impl.encode_message(msg)
    return ent

  def _from_base_type(self, ent):
    """Convert a Model instance (entity) to a Message value."""
    if ent._projection:
      # Projection query result.  Reconstitute the message from the fields.
      return _projected_entity_to_message(ent, self._message_type)

    blob = ent.blob_
    if blob is not None:
      protocol = self._protocol_impl
    else:
      # Perhaps it was written using a different protocol.
      protocol = None
      for name in _protocols_registry.names:
        key = '__%s__' % name
        if key in ent._values:
          blob = ent._values[key]
          if isinstance(blob, model._BaseValue):
            blob = blob.b_val
          protocol = _protocols_registry.lookup_by_name(name)
          break
    if blob is None or protocol is None:
      return None  # This will reveal the underlying dummy model.
    msg = protocol.decode_message(self._message_type, blob)
    return msg


def _message_to_entity(msg, modelclass):
  """Recursive helper for _to_base_type() to convert a message to an entity.

  Args:
    msg: A Message instance.
    modelclass: A Model subclass.

  Returns:
    An instance of modelclass.
  """
  ent = modelclass()
  for prop_name, prop in modelclass._properties.items():
    if prop._code_name == 'blob_':  # TODO: Devise a cleaner test.
      continue  # That's taken care of later.
    value = getattr(msg, prop_name)
    if value is not None and isinstance(prop, model.StructuredProperty):
      if prop._repeated:
        value = [_message_to_entity(v, prop._modelclass) for v in value]
      else:
        value = _message_to_entity(value, prop._modelclass)
    setattr(ent, prop_name, value)
  return ent


def _projected_entity_to_message(ent, message_type):
  """Recursive helper for _from_base_type() to convert an entity to a message.

  Args:
    ent: A Model instance.
    message_type: A Message subclass.

  Returns:
    An instance of message_type.
  """
  msg = message_type()
  analyzed = _analyze_indexed_fields(ent._projection)
  for name, sublist in analyzed.items():
    prop = ent._properties[name]
    val = prop._get_value(ent)
    assert isinstance(prop, model.StructuredProperty) == bool(sublist)
    if sublist:
      field = message_type.field_by_name(name)
      assert isinstance(field, messages.MessageField)
      assert prop._repeated == field.repeated
      if prop._repeated:
        assert isinstance(val, list)
        val = [_projected_entity_to_message(v, field.type) for v in val]
      else:
        assert isinstance(val, prop._modelclass)
        val = _projected_entity_to_message(val, field.type)
    setattr(msg, name, val)
  return msg
