#!/usr/bin/env python2.5
#
# Copyright 2007 Google Inc.
#
# 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.
#

"""Classes for common kinds, including Contact, Message, and Event.

Most of these kinds are based on the gd namespace "kinds" from GData:

  http://code.google.com/apis/gdata/common-elements.html
"""





import types
import urlparse
from xml.sax import saxutils
from google.appengine.datastore import datastore_pb
from google.appengine.api import datastore
from google.appengine.api import datastore_errors
from google.appengine.api import datastore_types

class GdKind(datastore.Entity):
  """ A base class for gd namespace kinds.

  This class contains common logic for all gd namespace kinds. For example,
  this class translates datastore (app id, kind, key) tuples to tag:
  URIs appropriate for use in <key> tags.
  """

  HEADER = u"""<entry xmlns:gd='http://schemas.google.com/g/2005'>
  <category scheme='http://schemas.google.com/g/2005#kind'
            term='http://schemas.google.com/g/2005#%s' />"""
  FOOTER = u"""
</entry>"""

  _kind_properties = set()
  _contact_properties = set()

  def __init__(self, kind, title, kind_properties, contact_properties=[]):
    """ Ctor.

    title is the name of this particular entity, e.g. Bob Jones or Mom's
    Birthday Party.

    kind_properties is a list of property names that should be included in
    this entity's XML encoding as first-class XML elements, instead of
    <property> elements. 'title' and 'content' are added to kind_properties
    automatically, and may not appear in contact_properties.

    contact_properties is a list of property names that are Keys that point to
    Contact entities, and should be included in this entity's XML encoding as
    <gd:who> elements. If a property name is included in both kind_properties
    and contact_properties, it is treated as a Contact property.

    Args:
    kind: string
    title: string
    kind_properties: list of strings
    contact_properties: list of string
    """
    datastore.Entity.__init__(self, kind)

    if not isinstance(title, types.StringTypes):
      raise datastore_errors.BadValueError(
        'Expected a string for title; received %s (a %s).' %
        (title, datastore_types.typename(title)))
    self['title'] = title
    self['content'] = ''

    self._contact_properties = set(contact_properties)
    assert not self._contact_properties.intersection(self.keys())

    self._kind_properties = set(kind_properties) - self._contact_properties
    self._kind_properties.add('title')
    self._kind_properties.add('content')

  def _KindPropertiesToXml(self):
    """ Convert the properties that are part of this gd kind to XML. For
    testability, the XML elements in the output are sorted alphabetically
    by property name.

    Returns:
    string  # the XML representation of the gd kind properties
    """
    properties = self._kind_properties.intersection(set(self.keys()))

    xml = u''
    for prop in sorted(properties):
      prop_xml = saxutils.quoteattr(prop)[1:-1]

      value = self[prop]
      has_toxml = (hasattr(value, 'ToXml') or
                   isinstance(value, list) and hasattr(value[0], 'ToXml'))

      for val in self._XmlEscapeValues(prop):
        if has_toxml:
          xml += '\n  %s' % val
        else:
          xml += '\n  <%s>%s</%s>' % (prop_xml, val, prop_xml)

    return xml


  def _ContactPropertiesToXml(self):
    """ Convert this kind's Contact properties kind to XML. For testability,
    the XML elements in the output are sorted alphabetically by property name.

    Returns:
    string  # the XML representation of the Contact properties
    """
    properties = self._contact_properties.intersection(set(self.keys()))

    xml = u''
    for prop in sorted(properties):
      values = self[prop]
      if not isinstance(values, list):
        values = [values]

      for value in values:
        assert isinstance(value, datastore_types.Key)
        xml += """
  <gd:who rel="http://schemas.google.com/g/2005#%s.%s>
    <gd:entryLink href="%s" />
  </gd:who>""" % (self.kind().lower(), prop, value.ToTagUri())

    return xml


  def _LeftoverPropertiesToXml(self):
    """ Convert all of this entity's properties that *aren't* part of this gd
    kind to XML.

    Returns:
    string  # the XML representation of the leftover properties
    """
    leftovers = set(self.keys())
    leftovers -= self._kind_properties
    leftovers -= self._contact_properties
    if leftovers:
      return u'\n  ' + '\n  '.join(self._PropertiesToXml(leftovers))
    else:
      return u''

  def ToXml(self):
    """ Returns an XML representation of this entity, as a string.
    """
    xml = GdKind.HEADER % self.kind().lower()
    xml += self._KindPropertiesToXml()
    xml += self._ContactPropertiesToXml()
    xml += self._LeftoverPropertiesToXml()
    xml += GdKind.FOOTER
    return xml


class Message(GdKind):
  """A message, such as an email, a discussion group posting, or a comment.

  Includes the message title, contents, participants, and other properties.

  This is the gd Message kind. See:
  http://code.google.com/apis/gdata/common-elements.html#gdMessageKind

  These properties are meaningful. They are all optional.

  property name  property type    meaning
  -------------------------------------
  title          string         message subject
  content        string         message body
  from           Contact*       sender
  to             Contact*       primary recipient
  cc             Contact*       CC recipient
  bcc            Contact*       BCC recipient
  reply-to       Contact*       intended recipient of replies
  link           Link*          attachment
  category       Category*      tag or label associated with this message
  geoPt          GeoPt*         geographic location the message was posted from
  rating         Rating*        message rating, as defined by the application

  * means this property may be repeated.

  The Contact properties should be Keys of Contact entities. They are
  represented in the XML encoding as linked <gd:who> elements.
  """
  KIND_PROPERTIES = ['title', 'content', 'link', 'category', 'geoPt', 'rating']
  CONTACT_PROPERTIES = ['from', 'to', 'cc', 'bcc', 'reply-to']

  def __init__(self, title, kind='Message'):
    GdKind.__init__(self, kind, title, Message.KIND_PROPERTIES,
                    Message.CONTACT_PROPERTIES)


class Event(GdKind):
  """A calendar event.

  Includes the event title, description, location, organizer, start and end
  time, and other details.

  This is the gd Event kind. See:
  http://code.google.com/apis/gdata/common-elements.html#gdEventKind

  These properties are meaningful. They are all optional.

  property name  property type    meaning
  -------------------------------------
  title          string         event name
  content        string         event description
  author         string         the organizer's name
  where          string*        human-readable location (not a GeoPt)
  startTime      timestamp      start time
  endTime        timestamp      end time
  eventStatus    string         one of the Event.Status values
  link           Link*          page with more information
  category       Category*      tag or label associated with this event
  attendee       Contact*       attendees and other related people

  * means this property may be repeated.

  The Contact properties should be Keys of Contact entities. They are
  represented in the XML encoding as linked <gd:who> elements.
  """
  KIND_PROPERTIES = ['title', 'content', 'author', 'where', 'startTime',
                     'endTime', 'eventStatus', 'link', 'category']
  CONTACT_PROPERTIES = ['attendee']

  class Status:
    CONFIRMED = 'confirmed'
    TENTATIVE = 'tentative'
    CANCELED = 'canceled'

  def __init__(self, title, kind='Event'):
    GdKind.__init__(self, kind, title, Event.KIND_PROPERTIES,
                    Event.CONTACT_PROPERTIES)

  def ToXml(self):
    """ Override GdKind.ToXml() to special-case author, gd:where, gd:when, and
    gd:eventStatus.
    """
    xml = GdKind.HEADER % self.kind().lower()

    self._kind_properties = set(Contact.KIND_PROPERTIES)
    xml += self._KindPropertiesToXml()

    if 'author' in self:
      xml += """
  <author><name>%s</name></author>""" % self['author']

    if 'eventStatus' in self:
      xml += """
  <gd:eventStatus value="http://schemas.google.com/g/2005#event.%s" />""" % (
    self['eventStatus'])

    if 'where' in self:
      lines = ['<gd:where valueString="%s" />' % val
               for val in self._XmlEscapeValues('where')]
      xml += '\n  ' + '\n  '.join(lines)

    iso_format = '%Y-%m-%dT%H:%M:%S'
    xml += '\n  <gd:when'
    for key in ['startTime', 'endTime']:
      if key in self:
        xml += ' %s="%s"' % (key, self[key].isoformat())
    xml += ' />'

    self._kind_properties.update(['author', 'where', 'startTime', 'endTime',
                                  'eventStatus'])
    xml += self._ContactPropertiesToXml()
    xml += self._LeftoverPropertiesToXml()
    xml += GdKind.FOOTER
    return xml


class Contact(GdKind):
  """A contact: a person, a venue such as a club or a restaurant, or an
  organization.

  This is the gd Contact kind. See:
  http://code.google.com/apis/gdata/common-elements.html#gdContactKind

  Most of the information about the contact is in the <gd:contactSection>
  element; see the reference section for that element for details.

  These properties are meaningful. They are all optional.

  property name  property type    meaning
  -------------------------------------
  title          string         contact's name
  content        string         notes
  email          Email*         email address
  geoPt          GeoPt*         geographic location
  im             IM*            IM address
  phoneNumber    Phonenumber*   phone number
  postalAddress  PostalAddress* mailing address
  link           Link*          link to more information
  category       Category*      tag or label associated with this contact

  * means this property may be repeated.
  """
  CONTACT_SECTION_HEADER = """
  <gd:contactSection>"""
  CONTACT_SECTION_FOOTER = """
  </gd:contactSection>"""

  KIND_PROPERTIES = ['title', 'content', 'link', 'category']

  CONTACT_SECTION_PROPERTIES = ['email', 'geoPt', 'im', 'phoneNumber',
                                'postalAddress']

  def __init__(self, title, kind='Contact'):
    GdKind.__init__(self, kind, title, Contact.KIND_PROPERTIES)

  def ToXml(self):
    """ Override GdKind.ToXml() to put some properties inside a
    gd:contactSection.
    """
    xml = GdKind.HEADER % self.kind().lower()

    self._kind_properties = set(Contact.KIND_PROPERTIES)
    xml += self._KindPropertiesToXml()

    xml += Contact.CONTACT_SECTION_HEADER
    self._kind_properties = set(Contact.CONTACT_SECTION_PROPERTIES)
    xml += self._KindPropertiesToXml()
    xml += Contact.CONTACT_SECTION_FOOTER

    self._kind_properties.update(Contact.KIND_PROPERTIES)
    xml += self._LeftoverPropertiesToXml()
    xml += GdKind.FOOTER
    return xml
