##############################################################################
#
# 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 zLOG import LOG

import md5

class Conflict(SyncCode):
  """
    object_path : the path of the obect
    keyword : an identifier of the conflict
    local_value : the value that we have locally
    remote_value : the value sent by the remote box

  """
  def __init__(self, object_path=None, keyword=None, local_value=None,\
               remote_value=None, domain=None, domain_id=None):
    self.object_path=object_path
    self.keyword = keyword
    self.setLocalValue(local_value)
    self.setRemoteValue(remote_value)
    self.domain = domain
    self.domain_id = domain_id

  def getObjectPath(self):
    """
    get the domain
    """
    return self.object_path

  def getLocalValue(self):
    """
    get the domain
    """
    return self.local_value

  def setLocalValue(self, value):
    """
    get the domain
    """
    try:
      self.local_value = value
    except TypeError: # It happens when we try to store StringIO
      self.local_value = None

  def getRemoteValue(self):
    """
    get the domain
    """
    return self.remote_value

  def setRemoteValue(self, value):
    """
    get the domain
    """
    try:
      self.remote_value = value
    except TypeError: # It happens when we try to store StringIO
      self.remote_value = None

  def setDomain(self, domain):
    """
    set the domain
    """
    self.domain = domain

  def getDomain(self):
    """
    get the domain
    """
    return self.domain

  def getKeyword(self):
    """
    get the domain
    """
    return self.keyword

  def getDomainId(self):
    """
    get the domain id
    """
    return self.domain_id

  def setDomainId(self, domain_id):
    """
    set the domain
    """
    self.domain_id = domain_id

  def applyRemoteValue():
    """
    We will take the remote value for this conflict
    """
    pass

  def applyLocalValue():
    """
    We will take the local value for this conflict
    """
    pass

class Signature(SyncCode):
  """
    status -- SENT, CONFLICT...
    md5_object -- An MD5 value of a given document
    #uid -- The UID of the document
    id -- the ID of the document
    rid -- the uid of the document on the remote database,
        only needed on the server.
    xml -- the xml of the object at the time where it was synchronized
  """

  # Constructor
  def __init__(self,id=None, status=None, xml_string=None):
    self.id = id
    self.status = status
    self.setXML(xml_string)
    self.partial_xml = None
    self.action = None
    self.setTempXML(None)
    self.setTempXML(None)
    self.resetConflictList()
    self.md5_string = None
    self.force = 0

  #def __init__(self,object=None, status=None, xml_string=None):
  #  self.uid = object.uid
  #  self.id = object.id
  #  self.status = status
  #  self.setXML(xml_string)

  def setStatus(self, status):
    """
      set the Status (see SyncCode for numbers)
    """
    self.status = status
    if status == self.SYNCHRONIZED:
      temp_xml = self.getTempXML()
      self.setForce(0)
      if temp_xml is not None:
        # This happens when we have sent the xml
        # and we just get the confirmation
        self.setXML(self.getTempXML())
      self.setTempXML(None)
      if len(self.getConflictList())>0:
        self.resetConflictList()
    elif status in (self.PUB_CONFLICT_MERGE,self.SENT):
      # We have a solution for the conflict, don't need to keep the list
      self.resetConflictList()

  def getStatus(self):
    """
      get the Status (see SyncCode for numbers)
    """
    return self.status

  def getForce(self):
    """
      get the force value (if we need to force update or not)
    """
    return self.force

  def setForce(self, force):
    """
      set the force value (if we need to force update or not)
    """
    self.force = force

  def setXML(self, xml):
    """
      set the XML corresponding to the object
    """
    self.xml = xml
    if self.xml != None:
      self.setTempXML(None) # We make sure that the xml will not be erased
      self.setMD5(xml)

  def getXML(self):
    """
      set the XML corresponding to the object
    """
    return self.xml

  def setTempXML(self, xml):
    """
      This is the xml temporarily saved, it will
      be stored with setXML when we will receive
      the confirmation of synchronization
    """
    self.temp_xml = xml

  def getTempXML(self):
    """
      get the temp xml
    """
    return self.temp_xml

  def setMD5(self, xml):
    """
      set the MD5 object of this signature
    """
    self.md5_string = md5.new(xml).digest()

  def getMD5(self):
    """
      get the MD5 object of this signature
    """
    return self.md5_string

  def checkMD5(self, xml_string):
    """
    check if the given md5_object returns the same things as
    the one stored in this signature, this is very usefull
    if we want to know if an objects has changed or not
    Returns 1 if MD5 are equals, else it returns 0
    """
    return ((md5.new(xml_string).digest()) == self.getMD5())

  def setRid(self, rid):
    """
      set the rid
    """
    self.rid = rid

  def getRid(self):
    """
      get the rid
    """
    return self.rid

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

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

  def setPartialXML(self, xml):
    """
    Set the partial string we will have to
    deliver in the future
    """
    #LOG('Subscriber.setPartialXML before',0,'partial_xml: %s' % str(self.partial_xml))
    self.partial_xml = xml
    #LOG('Subscriber.setPartialXML after',0,'partial_xml: %s' % str(self.partial_xml))

  def getPartialXML(self):
    """
    Set the partial string we will have to
    deliver in the future
    """
    #LOG('Subscriber.getPartialXML',0,'partial_xml: %s' % str(self.partial_xml))
    return self.partial_xml

  def getAction(self):
    """
    Return the actual action for a partial synchronization
    """
    return self.action

  def setAction(self, action):
    """
    Return the actual action for a partial synchronization
    """
    self.action = action

  def getConflictList(self):
    """
    Return the actual action for a partial synchronization
    """
    conflict_list = []
    if len(self.conflict_list)>0:
      for conflict in self.conflict_list:
        conflict_list += [conflict]
    return conflict_list

  def resetConflictList(self):
    """
    Return the actual action for a partial synchronization
    """
    self.conflict_list = PersistentMapping()

  def setConflictList(self, conflict_list):
    """
    Return the actual action for a partial synchronization
    """
    if conflict_list is None:
      self.resetConflictList()
    else:
      self.conflict_list = conflict_list

class Subscription(SyncCode):
  """
    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

    subsribtion_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

    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

  """

  signatures = PersistentMapping()

  # Constructor
  def __init__(self, id, publication_url, subscription_url, destination_path, query, xml_mapping):
    """
      We need to create a dictionnary of
      signatures of documents which belong to the synchronisation
      process
    """
    self.id = id
    self.publication_url = (publication_url)
    self.subscription_url = str(subscription_url)
    self.destination_path = str(destination_path)
    self.query = query
    self.xml_mapping = xml_mapping
    self.anchor = None
    self.session_id = 0
    self.signatures = PersistentMapping()
    self.last_anchor = '00000000T000000Z'
    self.next_anchor = '00000000T000000Z'
    self.domain_type = self.SUB
    #self.signatures = PersitentMapping()

  # Accessors
  def getRemoteId(self, id, path=None):
    """
      Returns the remote id from a know local id
      Returns None if...
      path allows to implement recursive sync
    """
    pass

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

  def getLocalId(self, rid, path=None):
    """
      Returns the local id from a know remote id
      Returns None if...
    """
    pass

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

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

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

  def setQuery(self, query):
    """
      set the query
    """
    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):
    """
      return the publication url
    """
    self.publication_url = publication_url

  def getXML_Mapping(self):
    """
      return the xml mapping
    """
    return self.xml_mapping

  def setXML_Mapping(self, xml_mapping):
    """
      return the xml mapping
    """
    self.xml_mapping = xml_mapping

  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 setDestinationPath(self, destination_path):
    """
      set the destination path
    """
    self.destination_path = destination_path

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

  def getSessionId(self):
    """
      return the session id
    """
    self.session_id += 1
    return self.session_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
    """
    self.signatures[signature.id] = signature

  def delSignature(self, id):
    """
      add a Signature to the subscription
    """
    del self.signatures[id]

  def getSignature(self, id):
    """
      add a Signature to the subscription
    """
    # This is just a test XXX To be removed
    #dict = {}
    #for key in self.signatures.keys():
    #  dict[key]=self.signatures[key].getPartialXML()
    #LOG('Subscription',0,'dict: %s' % str(dict))
    if self.signatures.has_key(id):
      return self.signatures[id]
    return None

  def getSignatureList(self):
    """
      add a Signature to the subscription
    """
    signature_list = []
    for key in self.signatures.keys():
      signature_list += [self.signatures[key]]
    return signature_list

  def hasSignature(self, id):
    """
      Check if there's a signature with this uid
    """
    LOG('Subscription',0,'keys: %s' % str(self.signatures.keys()))
    return self.signatures.has_key(id)

  def resetAllSignatures(self):
    """
      Reset all signatures
    """
    self.signatures = PersistentMapping()

  def getIdList(self):
    """
    Returns the list of ids from signature
    """
    return self.signatures.keys()

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

  def startSynchronization(self):
    """
    Set the status of every object as NOT_SYNCHRONIZED
    """
    # XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    # XXX for debugging only, to be removed
    dict_sign = {}
    for object_id in self.signatures.keys():
      dict_sign[object_id] = self.signatures[object_id].getStatus()
    LOG('startSynchronization',0,'signatures_status: %s' % str(dict_sign))
    # XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    for object_id in self.signatures.keys():
      # Change the status only if we are not in a conflict mode
      if not(self.signatures[object_id].getStatus() in (self.CONFLICT,self.PUB_CONFLICT_MERGE,
                                                        self.PUB_CONFLICT_CLIENT_WIN)):
        self.signatures[object_id].setStatus(self.NOT_SYNCHRONIZED)
        self.signatures[object_id].setPartialXML(None)
        self.signatures[object_id].setTempXML(None)