# Source code for pyzor.engines.gdbm_

    """Gdbm database engine."""

    try:
        import gdbm as gdbm

        _has_gdbm = True
    except ImportError:
        try:
            import dbm.gnu as gdbm

            _has_gdbm = True
        except ImportError:
            _has_gdbm = False

    import time
    import logging
    import datetime
    import threading

    from pyzor.engines.common import Record, DBHandle, BaseEngine

    def _dt_decode(datetime_str):
        """Decode a string into a datetime object."""
        if datetime_str == "None":
            return None
        try:
            return datetime.datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S.%f")
        except ValueError:
            return datetime.datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S")

    class GdbmDBHandle(BaseEngine):
        absolute_source = True
        handles_one_step = False

        sync_period = 60
        reorganize_period = 3600 * 24  # 1 day
        fields = (
            "r_count",
            "r_entered",
            "r_updated",
            "wl_count",
            "wl_entered",
            "wl_updated",
        )
        _fields = [
            ("r_count", int),
            ("r_entered", _dt_decode),
            ("r_updated", _dt_decode),
            ("wl_count", int),
            ("wl_entered", _dt_decode),
            ("wl_updated", _dt_decode),
        ]
        this_version = "1"
        log = logging.getLogger("pyzord")

        def __init__(self, fn, mode, max_age=None):
            self.max_age = max_age
            self.db = gdbm.open(fn, mode)
            self.reorganize_timer = None
            self.sync_timer = None
            self.start_reorganizing()
            self.start_syncing()

        def __iter__(self):
            k = self.db.firstkey()
            while k is not None:
                yield k
                k = self.db.nextkey(k)

        def _iteritems(self):
            for k in self:
                try:
                    yield k, self._really_getitem(k)
                except Exception as e:
                    self.log.warning("Invalid record %s: %s", k, e)

        def iteritems(self):
            return self._iteritems()

        def items(self):
            return list(self._iteritems())

        def apply_method(self, method, varargs=(), kwargs=None):
            if kwargs is None:
                kwargs = {}
            return method(*varargs, **kwargs)

        def __getitem__(self, key):
            return self.apply_method(self._really_getitem, (key,))

        def _really_getitem(self, key):
            return GdbmDBHandle.decode_record(self.db[key])

        def __setitem__(self, key, value):
            self.apply_method(self._really_setitem, (key, value))

        def _really_setitem(self, key, value):
            self.db[key] = GdbmDBHandle.encode_record(value)

        def __delitem__(self, key):
            self.apply_method(self._really_delitem, (key,))

        def _really_delitem(self, key):
            del self.db[key]

        def start_syncing(self):
            if self.db:
                self.apply_method(self._really_sync)
            self.sync_timer = threading.Timer(self.sync_period, self.start_syncing)
            self.sync_timer.setDaemon(True)
            self.sync_timer.start()

        def _really_sync(self):
            self.db.sync()

        def start_reorganizing(self):
            if not self.max_age:
                return
            if self.db:
                self.apply_method(self._really_reorganize)
            self.reorganize_timer = threading.Timer(
                self.reorganize_period, self.start_reorganizing
            )
            self.reorganize_timer.setDaemon(True)
            self.reorganize_timer.start()

        def _really_reorganize(self):
            self.log.debug("reorganizing the database")
            key = self.db.firstkey()
            breakpoint = time.time() - self.max_age
            while key is not None:
                rec = self._really_getitem(key)
                delkey = None
                if int(time.mktime(rec.r_updated.timetuple())) < breakpoint:
                    self.log.debug("deleting key %s", key)
                    delkey = key
                key = self.db.nextkey(key)
                if delkey:
                    self._really_delitem(delkey)
            self.db.reorganize()

        @classmethod
        def encode_record(cls, value):
            values = [cls.this_version]
            values.extend(["%s" % getattr(value, x) for x in cls.fields])
            return ",".join(values)

        @classmethod
        def decode_record(cls, s):
            try:
                s = s.decode("utf8")
            except UnicodeError:
                raise StandardError("don't know how to handle db value %s" % repr(s))
            parts = s.split(",")
            version = parts[0]
            if len(parts) == 3:
                dispatch = cls.decode_record_0
            elif version == "1":
                dispatch = cls.decode_record_1
            else:
                raise StandardError("don't know how to handle db value %s" % repr(s))
            return dispatch(s)

        @staticmethod
        def decode_record_0(s):
            r = Record()
            parts = s.split(",")
            fields = ("r_count", "r_entered", "r_updated")
            assert len(parts) == len(fields)
            for i in range(len(parts)):
                setattr(r, fields[i], int(parts[i]))
            return r

        @classmethod
        def decode_record_1(cls, s):
            r = Record()
            parts = s.split(",")[1:]
            assert len(parts) == len(cls.fields)
            for part, field in zip(parts, cls._fields):
                f, decode = field
                setattr(r, f, decode(part))
            return r

    class ThreadedGdbmDBHandle(GdbmDBHandle):
        """Like GdbmDBHandle, but handles multi-threaded access."""

        def __init__(self, fn, mode, max_age=None, bound=None):
            self.db_lock = threading.Lock()
            GdbmDBHandle.__init__(self, fn, mode, max_age=max_age)

        def apply_method(self, method, varargs=(), kwargs=None):
            if kwargs is None:
                kwargs = {}
            with self.db_lock:
                return GdbmDBHandle.apply_method(
                    self, method, varargs=varargs, kwargs=kwargs
                )

    # This won't work because the gdbm object needs to be in shared memory of the
    # spawned processes.
    # class ProcessGdbmDBHandle(ThreadedGdbmDBHandle):
    # def __init__(self, fn, mode, max_age=None, bound=None):
    # ThreadedGdbmDBHandle.__init__(self, fn, mode, max_age=max_age,
    #                                       bound=bound)
    #         self.db_lock = multiprocessing.Lock()

    if not _has_gdbm:
        handle = DBHandle(
            single_threaded=None, multi_threaded=None, multi_processing=None, prefork=None
        )
    else:
        handle = DBHandle(
            single_threaded=GdbmDBHandle,
            multi_threaded=ThreadedGdbmDBHandle,
            multi_processing=None,
            prefork=None,
        )