#!/usr/bin/python2.5
#
# Copyright 2008 the Melange authors.
#
# 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.

"""Helpers functions for updating different kinds of models in datastore.
"""

__authors__ = [
  '"Todd Larsen" <tlarsen@google.com>',
  '"Sverre Rabbelier" <sverre@rabbelier.nl>',
  '"Pawel Solyga" <pawel.solyga@gmail.com>',
  ]


from google.appengine.ext import db


def getFullClassName(cls):
  """Returns fully-qualified module.class name string.""" 

  return '%s.%s' % (cls.__module__, cls.__name__) 


def buildTypedQueryString(base_class, derived_class=None):
  """Returns a GQL query string compatible with PolyModel.
  
  Args:
    base_class: Model class that inherits directly from
      polymodel.PolyModel, such as soc.models.work.Work
    derived_class: optional more-specific Model class that
      derives from base_class, such as soc.models.document.Document;
      default is None, in which case the inheritance_line
      property is *not* tested by the returned query string
  """

  query_str_parts = ['SELECT * FROM ', base_class.__name__]

  if derived_class:
    query_str_parts.extend(
      [" WHERE inheritance_line = '", getFullClassName(derived_class), "'"])

  return ''.join(query_str_parts)


def buildOrderedQueryString(base_class, derived_class=None, order_by=None):
  """Returns a an ordered GQL query string compatible with PolyModel.
  
  Args:
    base_class, derived_class: see buildTypedQueryString()
    order_by: optional field name by which to order the query results;
      default is None, in which case no ORDER BY clause is placed in
      the query string
  """

  query_str_parts = [
    buildTypedQueryString(base_class, derived_class=derived_class)]

  if order_by:
    query_str_parts.extend([' ORDER BY ', order_by])

  return ''.join(query_str_parts)


def getEntitiesForLimitAndOffset(base_class, limit, offset=0,
                                 derived_class=None, order_by=None):
  """Returns entities for given offset and limit or None if not found.
    
  Args:
    limit: max amount of entities to return
    offset: optional offset in entities list which defines first entity to
      return; default is zero (first entity)
  """

  query_string = buildOrderedQueryString(
      base_class, derived_class=derived_class, order_by=order_by)

  query = db.GqlQuery(query_string)

  return query.fetch(limit, offset)


def getNearestEntities(base_class, fields_to_try, derived_class=None):
  """Get entities just before and just after the described entity.
    
  Args:
    fields_to_try: ordered list of key/value pairs that "describe"
      the desired entity (which may not necessarily exist), where key is
      the name of the field, and value is an instance of that field
      used in the comparison; if value is None, that field is skipped

  Returns:
    a two-tuple: ([nearest_entities], 'field_name')
    
    nearest_entities: list of entities being those just before and just
      after the (possibly non-existent) entity identified by the first
      of the supplied (non-None) fields
        OR
      possibly None if query had no results for the supplied field
      that was used.
  """

  # SELECT * FROM base_class WHERE inheritance_line = 'derived_class'
  typed_query_str = buildTypedQueryString(
    base_class, derived_class=derived_class)
  
  if derived_class:
    typed_query_str = typed_query_str + ' AND '
  else:
    typed_query_str = typed_query_str + ' WHERE '
    
  for field, value in fields_to_try:
    if value is None:
      # skip this not-supplied field
      continue

    query = db.GqlQuery('%s%s > :1' % (typed_query_str, field), value)
    return query.fetch(1), field

  # all fields exhausted, and all had None values
  return (None, None)


def findNearestEntitiesOffset(width, base_class, fields_to_try,
                              derived_class=None):
  """Finds offset of beginning of a range of entities around the nearest.
  
  Args:
    width: the width of the "found" window around the nearest User found
    base_class, fields_to_try, derived_class:  see getNearestEntities()
    
  Returns:
    an offset into the list of entities that is width/2 less than the
    offset of the first entity returned by getNearestEntities(), or zero
    if that offset would be less than zero
      OR
    None if there are no nearest entities or the offset of the beginning of
    the range cannot be found for some reason 
  """

  # find entity "nearest" to supplied fields
  nearest_entities, field = getNearestEntities(
      base_class, fields_to_try, derived_class=derived_class)
  
  if not nearest_entities:
    # no "nearest" entity, so indicate that with None
    return None

  nearest_entity = nearest_entities[0]

  # start search for beginning of nearest Users range at offset zero
  offset = 0
  entities = getEntitiesForLimitAndOffset(
      base_class, width, offset=offset, derived_class=derived_class)
  
  while True:
    for entity in entities:
      if getattr(nearest_entity, field) == getattr(entity, field):
        # nearest User found in current search range, so return a range start
        return max(0, (offset - (width/2)))

      offset = offset + 1

    # nearest User was not in the current search range, so fetch the next set
    entities = getEntitiesForLimitAndOffset(
        base_class, width, offset=offset, derived_class=derived_class)

    if not entities:
      # nearest User never found, so indicate that with None
      break

  return None
