"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/dulcinea/lib/extent.py $
$Id: extent.py 22651 2003-09-30 02:28:27Z dbinger $

Provides the Extent class.
"""

from __future__ import generators

from time import time, ctime
from ZODB.FileStorage import FileStorage
from dulcinea import base
from dulcinea.database import pack_oid, unpack_oid

class EmptyExtent (Exception):
    """
    Instance attributes: none
    """
    pass

class Extent (base.DulcineaPersistent):
    """
    An Extent maintains a mapping of class name to oid list
    for the objects present in the database.

    Instance attributes:
      mapping : PersistentMapping {class_name:string : {oid:string:boolean}}
      start_oid_scan : long
        The oid to start scanning from. A value of 0L indicates no objects
        have been scanned.
    """

    def __init__ (self):
        self.clear()

    def clear (self):
        self.mapping = base.PersistentMapping()
        self.start_oid_scan = 0L

    def get_database (self):
        """() -> ObjectDatabase"""
        raise NotImplemented, 'subclass must override'

    def objects (self):
        """
        Generate a sequence of all objects.
        """
        for class_name in self.mapping.keys():
            for object in self.all(class_name):
                yield object

    def scan (self):
        """
        Examine the objects that have appeared since the
        last scan, or all objects if no scan has happened
        since the last call of the clear() method.
        Add each object's oid to the appropriate list in
        the values of self.mapping.
        """
        object_db = self.get_database()
        storage = object_db.get_storage()
        connection = object_db.get_connection()
        highest_oid = unpack_oid(storage.new_oid())
        oid = self.start_oid_scan

        while oid < highest_oid:
            packed_oid = pack_oid(oid)
            try:
                object = connection[packed_oid]
            except KeyError:
                pass
            else:
                class_name = (object.__module__ +
                              "." +
                              object.__class__.__name__)
                object._p_deactivate()
                if not self.mapping.has_key(class_name):
                    self.mapping[class_name] = {}
                else:
                    self.mapping[class_name] = self.mapping[class_name]
                self.mapping[class_name][packed_oid] = 1
            oid += 1

        if isinstance(storage, FileStorage):  # exclusive access to DB
            self.start_oid_scan = oid # start next scan here

    def all (self, *class_names):
        """
        Generate a sequence of all objects in the database with any of
        the given class names (strings).  Note that inheritance is not
        used here.
        """
        object_db = self.get_database()
        connection = object_db.get_connection()
        self.scan()

        # Construct a temporary mapping of usable synonyms.
        synonyms = {}
        for full_name in self.mapping.keys():
            class_name = full_name.split('.')[-1]
            if synonyms.has_key(class_name):
                synonyms[class_name] = None # Synonym collision.
            synonyms[class_name] = full_name

        for class_name in class_names:
            oids = (self.mapping.get(class_name) or
                    self.mapping.get(synonyms.get(class_name)))
            if oids is None:
                raise EmptyExtent("no immediate instances of %r here."
                                  % class_name)
            bad_oids = 0
            for oid in oids.keys():
                try:
                    object = connection[oid]
                except KeyError:
                    del oids[oid]
                    bad_oids += 1
                else:
                    dir(object)
                    yield object
            if bad_oids:
                self.mapping._p_changed = 1
                print ("%d instances of %s were deleted by the last pack."
                       % (bad_oids, class_name))

    def count (self, class_name):
        """
        Return the number of objects with the given class name.
        """
        result = 0
        for object in self.all(class_name):
            result += 1
        return result

    def census (self):
        """
        Returns a string containing a formatted census report.
        """
        self.scan()
        pairs = [ (self.count(name), name)
                  for name in self.mapping.keys() ]
        pairs.sort()
        s = '\n    Census (%s):\n\n' % ctime(time())
        sum = 0L
        for pair in pairs:
            sum += pair[0]
            if pair[0] == 0:
                del self.mapping[pair[1]]
            else:
                s += '%11d %s\n' % pair
        s += '    -------\n'
        s += '%11d %s\n\n' % (sum, 'Persistent')
        return s

