#!/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.
#


"""Models and helper functions for access to app's datastore metadata.

These entities cannot be created by users, but are created as results of
`__namespace__`, `__kind__` and `__property__` metadata queries such as

  # Find all namespaces
  q = db.GqlQuery("SELECT * FROM __namespace__")
  for p in q.run():
    print "namespace:", repr(p.namespace_name)

or

  # Find all properties of A whose name starts with a lower-case letter
  q = db.GqlQuery("SELECT __key__ from __property__ " +
                  "WHERE __key__ >= :1 AND __key__ < :2",
                  Property.key_for_property("A", "a"),
                  Property.key_for_property("A", chr(ord("z") + 1)))
  for p in q.run():
    print "%s: %s" % (Property.key_to_kind(p), Property.key_to_property(p))

or, using `Query` objects

  # Find all kinds >= "a"
  q = metadata.Kind().all()
  q.filter("__key__ >=", metadata.Kind.key_for_kind("a"))
  for p in q.run():
    print "kind:", repr(p.kind_name)
"""









from google.appengine.api import datastore_types
from google.appengine.ext import db


class BaseMetadata(db.Model):
  """Base class for all metadata models."""


  KIND_NAME = '__BaseMetadata__'

  @classmethod
  def kind(cls):
    """Kind name override."""
    return cls.KIND_NAME


class Namespace(BaseMetadata):
  """Model for `__namespace__` metadata query results."""

  KIND_NAME = '__namespace__'
  EMPTY_NAMESPACE_ID = datastore_types._EMPTY_NAMESPACE_ID

  @property
  def namespace_name(self):
    """Return the namespace name specified by this entity's key."""
    return self.key_to_namespace(self.key())

  @classmethod
  def key_for_namespace(cls, namespace):
    """Return the `__namespace__` key for namespace.

    Args:
      namespace: namespace whose key is requested.

    Returns:
      The key for namespace.
    """
    if namespace:
      return db.Key.from_path(cls.KIND_NAME, namespace)
    else:
      return db.Key.from_path(cls.KIND_NAME, cls.EMPTY_NAMESPACE_ID)

  @classmethod
  def key_to_namespace(cls, key):
    """Return the namespace specified by a given `__namespace__` key.

    Args:
      key: key whose name is requested.

    Returns:
      The namespace specified by key.
    """
    return key.name() or ''


class Kind(BaseMetadata):
  """Model for `__kind__` metadata query results."""

  KIND_NAME = '__kind__'

  @property
  def kind_name(self):
    """Return the kind name specified by this entity's key."""
    return self.key_to_kind(self.key())

  @classmethod
  def key_for_kind(cls, kind):
    """Return the `__kind__` key for kind.

    Args:
      kind: kind whose key is requested.

    Returns:
      The key for kind.
    """
    return db.Key.from_path(cls.KIND_NAME, kind)

  @classmethod
  def key_to_kind(cls, key):
    """Return the kind specified by a given `__kind__` key.

    Args:
      key: key whose name is requested.

    Returns:
      The kind specified by key.
    """
    return key.name()


class Property(BaseMetadata):
  """Model for `__property__` metadata query results."""

  KIND_NAME = '__property__'

  @property
  def property_name(self):
    """Return the property name specified by this entity's key."""
    return self.key_to_property(self.key())

  @property
  def kind_name(self):
    """Return the kind name specified by this entity's key."""
    return self.key_to_kind(self.key())

  property_representation = db.StringListProperty()

  @classmethod
  def key_for_kind(cls, kind):
    """Return the `__property__` key for kind.

    Args:
      kind: kind whose key is requested.

    Returns:
      The parent key for `__property__` keys of kind.
    """
    return db.Key.from_path(Kind.KIND_NAME, kind)

  @classmethod
  def key_for_property(cls, kind, property_):
    """Return the `__property__` key for property of kind.

    Args:
      kind: kind whose key is requested.
      property_: property whose key is requested.

    Returns:
      The key for property of kind.
    """
    return db.Key.from_path(Kind.KIND_NAME, kind, Property.KIND_NAME, property_)

  @classmethod
  def key_to_kind(cls, key):
    """Return the kind specified by a given `__property__` key.

    Args:
      key: key whose kind name is requested.

    Returns:
      The kind specified by key.
    """
    if key.kind() == Kind.KIND_NAME:
      return key.name()
    else:
      return key.parent().name()

  @classmethod
  def key_to_property(cls, key):
    """Return the property specified by a given `__property__` key.

    Args:
      key: key whose property name is requested.

    Returns:
      property specified by key, or `None` if the key specified only a kind.
    """
    if key.kind() == Kind.KIND_NAME:
      return None
    else:
      return key.name()


class EntityGroup(BaseMetadata):
  """Model for `__entity_group__` metadata (available in HR datastore only).

  This metadata contains a numeric `__version__` property that is guaranteed
  to increase on every change to the entity group. The version may increase
  even in the absence of user-visible changes to the entity group. The
  `__entity_group__` entity may not exist `None` if the entity group was never
  written to.
  """

  KIND_NAME = '__entity_group__'
  ID = 1

  version = db.IntegerProperty(name='__version__')

  @classmethod
  def key_for_entity(cls, entity_or_key):
    """Return the metadata key for the entity group containing `entity_or_key`.

    Use this key to `get()` the `__entity_group__` metadata entity for the
    entity group containing `entity_or_key`.

    Args:
      entity_or_key: a key or entity whose `__entity_group__` key you want.

    Returns:
      The `__entity_group__` key for the entity group containing
      `entity_or_key`.
    """
    if isinstance(entity_or_key, db.Model):
      key = entity_or_key.key()
    else:
      key = entity_or_key
    while key.parent():
      key = key.parent()
    return db.Key.from_path(cls.KIND_NAME, cls.ID, parent=key)


def get_namespaces(start=None, end=None):
  """Return all namespaces in the specified range.

  Args:
    start: only return namespaces >= start if start is not `None`.
    end: only return namespaces < end if end is not `None`.

  Returns:
    A list of namespace names between the (optional) start and end values.
  """
  q = Namespace.all()
  if start is not None:
    q.filter('__key__ >=', Namespace.key_for_namespace(start))
  if end is not None:
    q.filter('__key__ <', Namespace.key_for_namespace(end))

  return [x.namespace_name for x in q.run()]


def get_kinds(start=None, end=None):
  """Return all kinds in the specified range.

  Args:
    start: only return kinds >= start if start is not `None`.
    end: only return kinds < end if end is not `None`.

  Returns:
    A list of kind names between the (optional) start and end values.
  """
  q = Kind.all()
  if start:
    q.filter('__key__ >=', Kind.key_for_kind(start))
  if end is not None:
    if end == '':
      return []
    q.filter('__key__ <', Kind.key_for_kind(end))

  return [x.kind_name for x in q.run()]


def get_properties_of_kind(kind, start=None, end=None):
  """Return all properties of kind in the specified range.

  NOTE: This function does not return unindexed properties.

  Args:
    kind: name of kind whose properties you want.
    start: only return properties >= start if start is not `None`.
    end: only return properties < end if end is not `None`.

  Returns:
    A list of property names of kind between the (optional) start and end
    values.
  """
  q = Property.all(keys_only=True)
  q.ancestor(Property.key_for_kind(kind))
  if start:
    q.filter('__key__ >=', Property.key_for_property(kind, start))
  if end is not None:
    if end == '':
      return []
    q.filter('__key__ <', Property.key_for_property(kind, end))

  return [Property.key_to_property(x) for x in q.run()]


def get_representations_of_kind(kind, start=None, end=None):
  """Return all representations of properties of kind in the specified range.

  NOTE: This function does not return unindexed properties.

  Args:
    kind: name of kind whose properties you want.
    start: only return properties >= start if start is not `None`.
    end: only return properties < end if end is not `None`.

  Returns:
    A dictionary mapping property names to its list of representations.
  """
  q = Property.all()
  q.ancestor(Property.key_for_kind(kind))
  if start:
    q.filter('__key__ >=', Property.key_for_property(kind, start))
  if end is not None:
    if end == '':
      return {}
    q.filter('__key__ <', Property.key_for_property(kind, end))

  result = {}
  for property_ in q.run():
    result[property_.property_name] = property_.property_representation

  return result


def get_entity_group_version(entity_or_key):
  """Returns the version of the entity group containing `entity_or_key`.

  Args:
    entity_or_key: a key or entity whose version you want.
  Returns: The version of the entity group containing `entity_or_key`. This
    version is guaranteed to increase on every change to the entity group. The
    version may increase even in the absence of user-visible changes to the
    entity group. May return `None` if the entity group was never written to. On
    non-HR datatores, this function returns `None`.
  """

  eg = db.get(EntityGroup.key_for_entity(entity_or_key))
  if eg:
    return eg.version
  else:
    return None
