##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
#          Sebastien Robin <seb@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################

from Globals import PersistentMapping
from time import gmtime, strftime # for anchors
from SyncCode import SyncCode
from AccessControl import ClassSecurityInfo
from Products.CMFCore.utils import getToolByName
from Acquisition import Implicit, aq_base
from Products.ERP5Type.Core.Folder import Folder
from Products.ERP5Type.Base import Base
from Products.ERP5Type import Permissions
from Products.ERP5Type import PropertySheet
from DateTime import DateTime
from zLOG import LOG, DEBUG, INFO
import md5
from base64 import b64encode, b64decode, b16encode, b16decode

def addSubscription( self, id, title='', REQUEST=None ):
  """
  Add a new Subscription
  """
  o = Subscription(id, '', '', '', '', '', '')
  self._setObject(id, o)
  if REQUEST is not None:
    return self.manage_main(self, REQUEST, update_menu=1)
  return o

#class Subscription(SyncCode, Implicit):
#class Subscription(Folder, SyncCode, Implicit, Folder, Impli):
from XMLSyncUtils import XMLSyncUtils

class Subscription(Folder, XMLSyncUtils):
  """
    Subscription hold the definition of a master ODB
    from/to which a selection of objects will be synchronised

    Subscription defined by::

    publication_url -- a URI to a publication

    subscription_url -- URL of ourselves

    destination_path -- the place where objects are stored

    query   -- a query which defines a local set of documents which
           are going to be synchronised

    xml_mapping -- a PageTemplate to map documents to XML

    gpg_key -- the name of a gpg key to use

    Subscription also holds private data to manage
    the synchronisation. We choose to keep an MD5 value for
    all documents which belong to the synchronisation process::

    signatures -- a dictionnary which contains the signature
           of documents at the time they were synchronized

    session_id -- it defines the id of the session
         with the server.

    last_anchor - it defines the id of the last synchronisation

    next_anchor - it defines the id of the current synchronisation

  """

  meta_type = 'ERP5 Subscription'
  portal_type = 'SyncML Subscription' # may be useful in the future...
  isPortalContent = 1
  isRADContent = 1
  icon = None
  isIndexable = 0
  user = None

  # Declarative properties
  property_sheets = ( PropertySheet.Base
                    , PropertySheet.SimpleItem )

  allowed_types = ( 'Signatures',)

  # Declarative constructors
  constructors =   (addSubscription,)

  # Declarative security
  security = ClassSecurityInfo()
  security.declareProtected(Permissions.ManagePortal,
                            'manage_editProperties',
                            'manage_changeProperties',
                            'manage_propertiesForm',
                              )

  # Constructor
  def __init__(self, id, title, publication_url, subscription_url,
      destination_path, source_uri, target_uri, query, xml_mapping,
      conduit, gpg_key, id_generator, media_type, login,
      password, activity_enabled, alert_code, synchronize_with_erp5_sites,
      sync_content_type):
    """
      We need to create a dictionnary of
      signatures of documents which belong to the synchronisation
      process
    """
    self.id = id
    self.setAlertCode(alert_code)
    self.setActivityEnabled(activity_enabled)
    self.publication_url = (publication_url)
    self.subscription_url = str(subscription_url)
    self.destination_path = str(destination_path)
    self.setSourceURI(source_uri)
    self.setTargetURI(target_uri)
    self.setQuery(query)
    self.setXMLMapping(xml_mapping)
    self.anchor = None
    self.session_id = 0
    #self.signatures = PersistentMapping()
    self.last_anchor = '00000000T000000Z'
    self.next_anchor = '00000000T000000Z'
    self.setMediaType(media_type)
    self.login = login
    self.password = password
    self.domain_type = self.SUB
    self.gpg_key = gpg_key
    self.setSynchronizationIdGenerator(id_generator)
    self.setConduit(conduit)
    Folder.__init__(self, id)
    self.title = title
    self.setSyncContentType(sync_content_type)
    self.setSynchronizeWithERP5Sites(synchronize_with_erp5_sites)

  def getAlertCodeList(self):
    return self.CODE_LIST

  def getAlertCode(self):
    return getattr(self, 'alert_code', 200)

  def setAlertCode(self, value):
    self.alert_code = int(value)

  def isOneWayFromServer(self):
    return self.getDomainType() == self.SUB and \
           self.getAlertCode() == self.ONE_WAY_FROM_SERVER

  def getActivityEnabled(self):
    """
    return true if we are using activity, false otherwise
    """
    return getattr(self, 'activity_enabled', None)

  def setActivityEnabled(self, activity_enabled):
    """
    set if we are using activity or not
    """
    self.activity_enabled = activity_enabled

  def getTitle(self):
    """
    getter for title
    """
    return getattr(self, 'title', None)

  def setTitle(self, value):
    """
    setter for title
    """
    self.title = value

  def setSourceURI(self, value):
    """
    setter for source_uri
    """
    self.source_uri = value

  def getSourceURI(self):
    """
    getter for the source_uri (the local path of the subscription data base)
    """
    return getattr(self, 'source_uri', None)

  def setTargetURI(self, value):
    """
    setter for target_uri
    """
    self.target_uri = value

  def getTargetURI(self):
    """
    getter for the target_uri (the distant Publication data base we want to 
    synchronize with)
    """
    return getattr(self, 'target_uri', None)

  def setSyncContentType(self, sync_content_type):
    """
    content type used by the subscriber
    """
    self.sync_content_type = sync_content_type
    # the varible name is sync_content_type instead of content_type because
    # content_type seems to be a function name already used


  def getSyncContentType(self):
    """
    getter of the subscriber sync_content_type
    """
    return getattr(self, 'sync_content_type', 'application/vnd.syncml+xml')

  def getSynchronizationType(self, default=None):
    """
    """
    # XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    # XXX for debugging only, to be removed
    #dict_sign = {}
    #for o in self.getSignatureList():
      #dict_sign[o.getId()] = o.getStatus()
    # LOG('getSignature', DEBUG, 'signatures_status: %s' % str(dict_sign))
    # XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    code = self.SLOW_SYNC
    if len(self.getSignatureList()[:1]) > 0:
      code = self.getAlertCode()
    if default is not None:
      code = default
    #LOG('Subscription', DEBUG, 'getSynchronizationType: %s' % code)
    return code

  def setXMLMapping(self, value):
    """
    this the name of the method used in order to set the xml
    """
    if value == '':
      value = None
    self.xml_mapping = value

  def setSynchronizeWithERP5Sites(self, synchronize_with_erp5_sites):
    """
    if the synchronisation is made with another ERP5 site, 
    synchronize_with_erp5_sites is True, False in other case
    XXX in the future, the method used to sendHttpResponse will be the same
    in all cases, so this method will be useless
    """
    self.synchronize_with_erp5_sites = synchronize_with_erp5_sites

  def getSynchronizeWithERP5Sites(self):
    """
    return True if the synchronisation is between two erp5 sites
    """
    return getattr(self, 'synchronize_with_erp5_sites', True)

  def checkCorrectRemoteSessionId(self, session_id):
    """
    We will see if the last session id was the same
    wich means that the same message was sent again

    return True if the session id was not seen, False if already seen
    """
    last_session_id = getattr(self, 'last_session_id', None)
    if last_session_id == session_id:
      return False 
    self.last_session_id = session_id
    return True

  def checkCorrectRemoteMessageId(self, message_id):
    """
    We will see if the last message id was the same
    wich means that the same message was sent again

    return True if the message id was not seen, False if already seen
    """
    last_message_id = getattr(self, 'last_message_id', None)
    if last_message_id == message_id:
      return False
    self.last_message_id = message_id
    return True

  def initLastMessageId(self, last_message_id=0):
    """
    set the last message id to 0
    """
    self.last_message_id = last_message_id

  def getLastSentMessage(self):
    """
    This is the getter for the last message we have sent
    """
    return getattr(self, 'last_sent_message', '')

  def setLastSentMessage(self, xml):
    """
    This is the setter for the last message we have sent
    """
    self.last_sent_message = xml

  def getDomainType(self):
    """
      return the ID
    """
    return self.domain_type

  def getId(self):
    """
      return the ID
    """
    return self.id

  def setId(self, id):
    """
      set the ID
    """
    self.id = id

  def setConduit(self, value):
    """
      set the Conduit
    """
    self.conduit = value

  def getConduit(self):
    """
      get the Conduit
    """
    return getattr(self, 'conduit', None)

  def getQuery(self):
    """
      return the query
    """
    return self.query

  def getGPGKey(self):
    """
      return the gnupg key name
    """
    return getattr(self, 'gpg_key', '')

  def setGPGKey(self, value):
    """
      setter for the gnupg key name
    """
    self.gpg_key = value

  def setQuery(self, query):
    """
      set the query
    """
    if query == '':
      query = None
    self.query = query

  def getPublicationUrl(self):
    """
      return the publication url
    """
    return self.publication_url

  def getLocalUrl(self):
    """
      return the publication url
    """
    return self.publication_url

  def setPublicationUrl(self, publication_url):
    """
      set the publication url
    """
    self.publication_url = publication_url

  def getXMLMapping(self, force=0):
    """
      return the xml mapping
    """
    if self.isOneWayFromServer() and force == 0:
      return None
    xml_mapping = getattr(self, 'xml_mapping', None)
    return xml_mapping

  def getMediaType(self):
    """
    This method return the type of media used in this session,
    for example, it could be "text/vcard" or "xml/text",...
    """
    return getattr(self, 'media_type', self.MEDIA_TYPE['TEXT_XML'])

  def setMediaType(self, media_type):
    """
    set the type of media used
    """
    if media_type in (None, ''):
      media_type = self.MEDIA_TYPE['TEXT_XML']
    self.media_type = media_type

  def getLogin(self):
    """
    This method return the login of this subscription
    """
    return getattr(self, 'login', '')

  def setLogin(self, new_login):
    """
    set the login at new_login
    """
    self.login = new_login

  def getPassword(self):
    """
    This method return the password of this subscription
    """
    return getattr(self, 'password', '')

  def setPassword(self, new_password):
    """
    set the password at new_password
    """
    self.password = new_password

  def getZopeUser(self):
    """
    This method return the zope user who begin the synchronization session
    """
    return getattr(self, 'zope_user_name', None)

  def setZopeUser(self, user_name):
    """
    This method set the zope user_name
    """
    self.zope_user_name = user_name

  def getAuthenticationFormat(self):
    """
      return the format of authentication
    """
    return getattr(self, 'authentication_format', 'b64')

  def getAuthenticationType(self):
    """
      return the type of authentication
    """
    return getattr(self, 'authentication_type', 'syncml:auth-basic')

  def setAuthenticationFormat(self, authentication_format):
    """
      set the format of authentication
    """
    if authentication_format in (None, ''):
      self.authentication_format = 'b64'
    else:
      self.authentication_format = authentication_format

  def setAuthenticationType(self, authentication_type):
    """
      set the type of authentication
    """
    if authentication_type in (None, ''):
      self.authentication_type = 'syncml:auth-basic'
    else:
      self.authentication_type = authentication_type

  def getGidFromObject(self, object):
    """
      Returns the object gid 
    """
    o_base = aq_base(object)
    o_gid = None
    conduit_name = self.getConduit()
    conduit = self.getConduitByName(conduit_name)
    gid_gen = getattr(conduit, 'getGidFromObject', None)
    if callable(gid_gen):
      o_gid = gid_gen(object)
    else:
      raise ValueError, "The conduit "+conduit_name+"seems to not have a \
          getGidFromObject method and it must"
    o_gid = b16encode(o_gid)
    #LOG('getGidFromObject returning', DEBUG, o_gid)
    return o_gid

  def getObjectFromGid(self, gid):
    """
    This tries to get the object with the given gid
    This uses the query if it exist
    """
    if len(gid)%2 != 0:
    #something encode in base 16 is always a even number of number
    #if not, b16decode will failed
      return None
    signature = self.getSignatureFromGid(gid)
    # First look if we do already have the mapping between
    # the id and the gid
    destination = self.getDestination()
    if signature is not None and signature.getPath() is not None:
      o = None
      try:
        o = destination.getPortalObject().restrictedTraverse(signature.getPath())
      except (AttributeError, KeyError, TypeError):
        pass
      o_id = signature.getObjectId()
      #try with id param too, because gid is not catalogged
      object_list = self.getObjectList(gid=b16decode(gid), id=o_id)
      if o is not None and o in object_list:
        return o
    #LOG('entering in the slow loop of getObjectFromGid !!!',0,'')
    object_list = self.getObjectList(gid=b16decode(gid))
    for o in object_list:
      o_gid = self.getGidFromObject(o)
      if o_gid == gid:
        return o
    #LOG('getObjectFromGid', DEBUG, 'returning None')
    return None

  def getObjectFromId(self, id):
    """
    return the object corresponding to the id
    """
    object_list = self.getObjectList(id=id)
    o = None
    for object in object_list:
      if object.getId() == id:
        o = object
        break
    return o

  def getObjectList(self, **kw):
    """
    This returns the list of sub-object corresponding
    to the query
    """
    destination = self.getDestinationPath()
    query = self.getQuery()
    if query is not None and isinstance(query, str):
      count = query.count("/")
      if count > 0:
        # There is a query path different than destination path change
        # destination path for this query
        cut_query = query.split('/')
        if destination.endswith('/'):
          destination = "%s%s" % (destination, "/".join(cut_query[:count]))
        else:  
          destination = "%s/s" % (destination, "/".join(cut_query[:count]))
        query = cut_query[count]
      query_list = []
      destination = self.unrestrictedTraverse(destination)
      query_method = getattr(destination, query, None)
      if query_method is not None:
        query_list = query_method(**kw)
      else:
        raise KeyError, 'This Subscriber %s provide no Query: %s'\
          % (self.getTitle(), query)
    elif callable(query): # used in the test
      destination = self.unrestrictedTraverse(destination)
      query_list = query(destination)
    else:
      raise KeyError, 'This Subscriber %s provide no Query with id: %s'\
        % (self.getTitle(), query)
    return [x for x in query_list
              if not getattr(x, '_conflict_resolution', False)]

  def generateNewIdWithGenerator(self, object=None, gid=None):
    """
    This tries to generate a new Id
    """
    id_generator = self.getSynchronizationIdGenerator()
    if id_generator is not None:
      o_base = aq_base(object)
      new_id = None
      if callable(id_generator):
        new_id = id_generator(object, gid=gid)
      elif getattr(o_base, id_generator, None) is not None:
        generator = getattr(object, id_generator)
        new_id = generator()
      else: 
        # This is probably a python script
        generator = getattr(object, id_generator)
        new_id = generator(object=object, gid=gid)
      #LOG('generateNewIdWithGenerator, new_id: ', DEBUG, new_id)
      return new_id
    return None

  def setSynchronizationIdGenerator(self, method):
    """
    This set the method name wich allows to generate
    a new id
    """
    if method in ('', 'None'):
      method = None
    self.synchronization_id_generator = method

  def getSynchronizationIdGenerator(self):
    """
    This get the method name wich allows to generate a new id
    """
    return getattr(self, 'synchronization_id_generator', None)

  def getSubscriptionUrl(self):
    """
      return the subscription url
    """
    return self.subscription_url

  def setSubscriptionUrl(self, subscription_url):
    """
      set the subscription url
    """
    self.subscription_url = subscription_url

  def getDestinationPath(self):
    """
      return the destination path
    """
    return self.destination_path

  def getDestination(self):
    """
      return the destination object itself
    """
    return self.unrestrictedTraverse(self.getDestinationPath())

  def setDestinationPath(self, destination_path):
    """
      set the destination path
    """
    self.destination_path = destination_path

  def getSubscription(self):
    """
      return the current subscription
    """
    return self

  def setSessionId(self, session_id):
    """
      set the session id
    """
    self.session_id = session_id

  def getSessionId(self):
    """
      return the session id
    """
    #self.session_id += 1 #to be commented
    return self.session_id

  def incrementSessionId(self):
    """
      increment and return the session id
    """
    self.session_id += 1
    self.resetMessageId() # for a new session, the message Id must be reset
    return self.session_id

  def incrementMessageId(self):
    """
      return the message id
    """
    value = getattr(self, 'message_id', 0)
    self.message_id = value +1
    return self.message_id

  def getMessageId(self):
    """
      increment and return the message id
    """
    return self.message_id

  def resetMessageId(self):
    """
      set the message id to 0
    """
    self.message_id = 0

  def setMessageId(self, message_id):
    """
      set the message id to message_id
    """
    self.message_id = message_id

  def getLastAnchor(self):
    """
      return the id of the last synchronisation
    """
    return self.last_anchor

  def getNextAnchor(self):
    """
      return the id of the current synchronisation
    """
    return self.next_anchor

  def setLastAnchor(self, last_anchor):
    """
      set the value last anchor
    """
    self.last_anchor = last_anchor

  def setNextAnchor(self, next_anchor):
    """
      set the value next anchor
    """
    # We store the old next anchor as the new last one
    self.last_anchor = self.next_anchor
    self.next_anchor = next_anchor

  def NewAnchor(self):
    """
      set a new anchor
    """
    self.last_anchor = self.next_anchor
    self.next_anchor = strftime("%Y%m%dT%H%M%SZ", gmtime())

  def resetAnchors(self):
    """
      reset both last and next anchors
    """
    self.last_anchor = self.NULL_ANCHOR
    self.next_anchor = self.NULL_ANCHOR

  def addSignature(self, signature):
    """
      add a Signature to the subscription
    """
    if self.getSignatureFromGid(signature.getGid()) is not None:
      self.delSignature(signature.getGid())
    self._setObject(signature.getGid(), aq_base(signature))

  def delSignature(self, gid):
    """
      del a Signature of the subscription
    """
    self._delObject(gid)

  def getSignatureFromObjectId(self, id):
    """
    return the signature corresponding to the id
    """
    o = None
    # XXX very slow
    for signature in self.getSignatureList():
      if id == signature.getObjectId():
        o = signature
        break
    return o

  def getSignatureFromGid(self, gid):
    """
    return the signature corresponding to the gid
    """
    return getattr(self, gid, None)

  def getSignatureFromRid(self, rid):
    """
    return the signature corresponding to the rid
    """
    o = None
    # XXX very slow
    for signature in self.getSignatureList():
      if rid == signature.getRid():
        o = signature
        break
    return o

  def getGidList(self):
    """
    Returns the list of gids from signature
    """
    return [id for id in self.getObjectIds()]

  def getSignatureList(self):
    """
      Returns the list of Signatures
    """
    return self.objectValues()

  def hasSignature(self, gid):
    """
      Check if there's a signature with this uid
    """
    return self.getSignatureFromGid(gid) is not None

  def resetAllSignatures(self):
    """
      Reset all signatures in activities
    """
    object_id_list = [id for id in self.getObjectIds()]
    object_list_len = len(object_id_list)
    for i in xrange(0, object_list_len, self.MAX_OBJECTS):
      current_id_list = object_id_list[i:i+self.MAX_OBJECTS]
      self.activate(activity='SQLQueue',
                    tag=self.getId(),
                    priority=self.PRIORITY).manage_delObjects(current_id_list)

  def getConflictList(self):
    """
    Return the list of all conflicts from all signatures
    """
    conflict_list = []
    for signature in self.getSignatureList():
      conflict_list.extend(signature.getConflictList())
    return conflict_list

  def getRemainingObjectPathList(self):
    """
    We should now wich objects should still
    synchronize
    """
    return getattr(self, 'remaining_object_path_list', None)

  def setRemainingObjectPathList(self, value):
    """
    We should now wich objects should still
    synchronize
    """
    setattr(self, 'remaining_object_path_list', value)

  def removeRemainingObjectPath(self, object_path):
    """
    We should now wich objects should still
    synchronize
    """
    remaining_object_list = self.getRemainingObjectPathList()
    if remaining_object_list is not None:
      new_list = []
      new_list.extend(remaining_object_list)
      while object_path in new_list:
        new_list.remove(object_path)
      self.setRemainingObjectPathList(new_list)

  def startSynchronization(self):
    """
    Set the status of every object as NOT_SYNCHRONIZED
    """
    for s in self.getSignatureList():
      # Change the status only if we are not in a conflict mode
      if s.getStatus() not in (self.CONFLICT,
                               self.PUB_CONFLICT_MERGE,
                               self.PUB_CONFLICT_CLIENT_WIN):
        s.setStatus(self.NOT_SYNCHRONIZED)
        s.setPartialXML(None)
        s.setTempXML(None)
    self.setRemainingObjectPathList(None)

  def isAuthenticated(self):
    """
    return True if the subscriber is authenticated for this session, False 
    in other case
    """
    return getattr(self, 'is_authenticated', None)

  def setAuthenticated(self, value):
    """
      set at True or False the value of is_authenticated is the subscriber
      is authenticated for this session or not
    """
    self.is_authenticated = value

  def encode(self, format, string_to_encode):
    """
      return the string_to_encode encoded with format format
    """
    if format in ('', None):
      return string_to_encode
    if format == 'b64':
      return b64encode(string_to_encode)
    #elif format is .... put here the other formats
    else:#if there is no format corresponding with format, raise an error
      LOG('encode : unknown or not implemented format : ', INFO, format)
      raise ValueError, "Sorry, the server ask for the format %s but \
            it's unknow or not implemented" % format

  def decode(self, format, string_to_decode):
    """
      return the string_to_decode decoded with format format
    """
    string_to_decode = string_to_decode.encode('utf-8')
    if format in ('', None):
      return string_to_decode
    if format == 'b64':
      return b64decode(string_to_decode)
    #elif format is .... put here the other formats
    else:#if there is no format corresponding with format, raise an error
      LOG('decode : unknown or not implemented format :', INFO, format)
      raise ValueError, "Sorry, the format %s is unknow or \
            not implemented" % format

  def isDecodeEncodeTheSame(self, string_encoded, string_decoded, format):
    """
      return True if the string_encoded is equal to string_decoded encoded 
      in format
    """
    return self.encode(format, string_decoded) == string_encoded

  def setUser(self, user):
    """
      save the user logged in to log him on each transaction
    """
    self.user = user

  def getUser(self):
    """
      retrun the user logged in
    """
    return getattr(self, 'user', None)

  def getConduitByName(self, conduit_name):
    """
    Get Conduit Object by given name.
    The Conduit can be located in Any Products according to naming Convention
    Products.<Product Name>.Conduit.<Conduit Module> ,if conduit_name equal module's name.
    By default Conduit must be defined in Products.ERP5SyncML.Conduit.<Conduit Module>
    """
    from Products.ERP5SyncML import Conduit
    if conduit_name.startswith('Products'):
      path = conduit_name
      conduit_name = conduit_name.split('.')[-1]
      conduit_module = __import__(path, globals(), locals(), [''])
      conduit = getattr(conduit_module, conduit_name)()
    else:
      conduit_module = __import__('.'.join([Conduit.__name__, conduit_name]),
                                  globals(), locals(), [''])
      conduit = getattr(conduit_module, conduit_name)()
    return conduit

#XXX backwards compatibility
from Products.ERP5SyncML import Signature, Conflict
Signature = Signature.Signature
Conflict = Conflict.Conflict