##############################################################################
#
# Copyright (c) 2001, 2002 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
#
##############################################################################

"""Tools for using FileStorage data files.

TODO:  This module needs tests.
Caution:  This file needs to be kept in sync with FileStorage.py.
"""

import cPickle
import struct

from ZODB.FileStorage.format import TRANS_HDR, DATA_HDR, TRANS_HDR_LEN, \
     DATA_HDR_LEN, DATA_VERSION_HDR_LEN
from ZODB.utils import u64
from persistent.TimeStamp import TimeStamp

class TxnHeader:
    """Object representing a transaction record header.

    Attribute   Position  Value
    ---------   --------  -----
    tid           0- 8    transaction id
    length        8-16    length of entire transaction record - 8
    status       16-17    status of transaction (' ', 'u', 'p'?)
    user_len     17-19    length of user field (pack code H)
    descr_len    19-21    length of description field (pack code H)
    ext_len      21-23    length of extensions (pack code H)
    """

    def __init__(self, file, pos):
        self._file = file
        self._pos = pos
        self._read_header()

    def _read_header(self):
        self._file.seek(self._pos)
        self._hdr = self._file.read(TRANS_HDR_LEN)
        (self.tid, self.length, self.status, self.user_len, self.descr_len,
         self.ext_len) = struct.unpack(TRANS_HDR, self._hdr)

    def read_meta(self):
        """Load user, descr, and ext attributes."""
        self.user = ""
        self.descr = ""
        self.ext = {}
        if not (self.user_len or self.descr_len or self.ext_len):
            return
        self._file.seek(self._pos + TRANS_HDR_LEN)
        if self.user_len:
            self.user = self._file.read(self.user_len)
        if self.descr_len:
            self.descr = self._file.read(self.descr_len)
        if self.ext_len:
            self._ext = self._file.read(self.ext_len)
            self.ext = cPickle.loads(self._ext)

    def get_data_offset(self):
        return (self._pos + TRANS_HDR_LEN + self.user_len + self.descr_len
                + self.ext_len)

    def get_timestamp(self):
        return TimeStamp(self.tid)

    def get_raw_data(self):
        data_off = self.get_data_offset()
        data_len = self.length - (data_off - self._pos)
        self._file.seek(data_off)
        return self._file.read(data_len)

    def next_txn(self):
        off = self._pos + self.length + 8
        self._file.seek(off)
        s = self._file.read(8)
        if not s:
            return None
        return TxnHeader(self._file, off)

    def prev_txn(self):
        if self._pos == 4:
            return None
        self._file.seek(self._pos - 8)
        tlen = u64(self._file.read(8))
        return TxnHeader(self._file, self._pos - (tlen + 8))

class DataHeader:
    """Object representing a data record header.

    Attribute         Position  Value
    ---------         --------  -----
    oid                 0- 8    object id
    serial              8-16    object serial numver
    prev_rec_pos       16-24    position of previous data record for object
    txn_pos            24-32    position of txn header
    version_len        32-34    length of version
    data_len           34-42    length of data
    nonversion_pos     42-50*   position of nonversion data record
    prev_version_pos   50-58*   pos of previous version data record

    * these attributes are only present if version_len != 0.
    """

    def __init__(self, file, pos):
        self._file = file
        self._pos = pos
        self._read_header()

    def _read_header(self):
        self._file.seek(self._pos)
        self._hdr = self._file.read(DATA_VERSION_HDR_LEN)
        # always read the longer header, just in case
        (self.oid, self.serial, prev_rec_pos, txn_pos, self.version_len,
         data_len) = struct.unpack(DATA_HDR, self._hdr[:DATA_HDR_LEN])
        self.prev_rec_pos = u64(prev_rec_pos)
        self.txn_pos = u64(txn_pos)
        self.data_len = u64(data_len)
        if self.version_len:
            s = self._hdr[DATA_HDR_LEN:]
            self.nonversion_pos = u64(s[:8])
            self.prev_version_pos = u64(s[8:])
        else:
            self.nonversion_pos = None
            self.prev_version_pos = None

    def next_offset(self):
        """Return offset of next record."""
        off = self._pos + self.data_len
        if self.version_len:
            off += self.version_len + DATA_VERSION_HDR_LEN
        else:
            off += DATA_HDR_LEN
        if self.data_len == 0:
            off += 8 # backpointer
        return off

def prev_txn(f):
    """Return transaction located before current file position."""
    f.seek(-8, 1)
    tlen = u64(f.read(8)) + 8
    return TxnHeader(f, f.tell() - tlen)
