##############################################################################
#
# Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""Very Simple Mapping ZODB storage

The Mapping storage provides an extremely simple storage implementation that
doesn't provide undo or version support.

It is meant to illustrate the simplest possible storage.

The Mapping storage uses a single data structure to map object ids to data.
"""

from ZODB.utils import u64, z64
from ZODB.BaseStorage import BaseStorage
from ZODB import POSException
from persistent.TimeStamp import TimeStamp


class MappingStorage(BaseStorage):

    def __init__(self, name='Mapping Storage'):
        BaseStorage.__init__(self, name)
        # ._index maps an oid to a string s.  s[:8] is the tid of the
        # transaction that created oid's current state, and s[8:] is oid's
        # current state.
        self._index = {}
        self._clear_temp()
        self._ltid = None
        # Note: If you subclass this and use a persistent mapping facility
        # (e.g. a dbm file), you will need to get the maximum key and save it
        # as self._oid.  See dbmStorage.

    def __len__(self):
        return len(self._index)

    def getSize(self):
        self._lock_acquire()
        try:
            # These constants are for Python object memory overheads.  Heh.
            s = 32
            for p in self._index.itervalues():
                s += 56 + len(p)
            return s
        finally:
            self._lock_release()

    def load(self, oid, version):
        self._lock_acquire()
        try:
            p = self._index[oid]
            return p[8:], p[:8] # pickle, serial
        finally:
            self._lock_release()

    def loadEx(self, oid, version):
        # Since we don't support versions, just tack the empty version
        # string onto load's result.
        return self.load(oid, version) + ("",)

    def getTid(self, oid):
        self._lock_acquire()
        try:
            # The tid is the first 8 bytes of the buffer.
            return self._index[oid][:8]
        finally:
            self._lock_release()

    def store(self, oid, serial, data, version, transaction):
        if transaction is not self._transaction:
            raise POSException.StorageTransactionError(self, transaction)

        if version:
            raise POSException.Unsupported("Versions aren't supported")

        self._lock_acquire()
        try:
            if oid in self._index:
                oserial = self._index[oid][:8]
                if serial != oserial:
                    raise POSException.ConflictError(oid=oid,
                                                     serials=(oserial, serial),
                                                     data=data)
            self._tindex[oid] = self._tid + data
        finally:
            self._lock_release()
        return self._tid

    def _clear_temp(self):
        # store() saves data in _tindex; if the transaction completes
        # successfully, _finish() merges _tindex into _index.
        self._tindex = {}

    def _finish(self, tid, user, desc, ext):
        self._index.update(self._tindex)
        self._ltid = self._tid

    def lastTransaction(self):
        return self._ltid

    def pack(self, t, referencesf):
        self._lock_acquire()
        try:
            if not self._index:
                return
            # Build an index of *only* those objects reachable from the root.
            rootl = [z64]
            pindex = {}
            while rootl:
                oid = rootl.pop()
                if oid not in pindex:
                    # Scan non-version pickle for references.
                    r = self._index[oid]
                    pindex[oid] = r
                    referencesf(r[8:], rootl)
            self._index = pindex

        finally:
            self._lock_release()

    def _splat(self):
        """Spit out a string showing state."""
        o = ['Index:']
        keys = self._index.keys()
        keys.sort()
        for oid in keys:
            r = self._index[oid]
            o.append('  %s: %s, %s' %
                     (u64(oid), TimeStamp(r[:8]), repr(r[8:])))

        return '\n'.join(o)
