#
# Copyright (C) 2006-2019  Nexedi SA
#
# 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, see <http://www.gnu.org/licenses/>.

import os, errno, socket, struct, sys, threading, time
from collections import defaultdict
from contextlib import contextmanager
from copy import copy
from functools import wraps
from neo.lib import logging, util
from neo.lib.interfaces import abstract, requires
from neo.lib.protocol import CellStates, NonReadableCell, MAX_TID, ZERO_TID
from . import DatabaseFailure

READABLE = CellStates.UP_TO_DATE, CellStates.FEEDING

def fallback(func):
    setattr(Fallback, func.__name__, func)
    return abstract(func)

def splitOIDField(tid, oids):
    if len(oids) % 8:
        raise DatabaseFailure('invalid oids length for tid %s: %s'
            % (tid, len(oids)))
    return [oids[i:i+8] for i in xrange(0, len(oids), 8)]

class CreationUndone(Exception):
    pass

class Fallback(object):
    pass

class DatabaseManager(object):
    """This class only describes an interface for database managers."""

    ENGINES = ()
    UNSAFE = False

    __lock = None
    LOCK = "neostorage"
    LOCKED = "error: database is locked"

    _deferred = 0
    _repairing = None

    def __init__(self, database, engine=None, wait=None):
        """
            Initialize the object.
        """
        if engine:
            if engine not in self.ENGINES:
                raise ValueError("Unsupported engine: %r not in %r"
                                 % (engine, self.ENGINES))
            self._engine = engine
        # XXX: Maybe the default should be to retry indefinitely.
        #      But for unit tests, we really want to never retry.
        self._wait = wait or 0
        self._parse(database)
        self._init_attrs = tuple(self.__dict__)
        self._connect()

    def __getstate__(self):
        state = {x: getattr(self, x) for x in self._init_attrs}
        assert state # otherwise, __setstate__ is not called
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)
        # For the moment, no need to duplicate secondary connections.
        #self._init_attrs = tuple(self.__dict__)
        # Secondary connections don't lock.
        self.LOCK = None
        self._connect()

    @contextmanager
    def _duplicate(self):
        db = copy(self)
        try:
            yield db
        finally:
            db.close()

    _cached_attr_list = (
        '_readable_set', '_getPartition', '_getReadablePartition')

    def __getattr__(self, attr):
        if attr in self._cached_attr_list:
            self._updateReadable()
        return self.__getattribute__(attr)

    def __enter__(self):
        assert not self.LOCK, "not a secondary connection"
        # XXX: All config caching should be done in this class,
        #      rather than in backend classes.
        self._config.clear()
        try:
            for attr in self._cached_attr_list:
                delattr(self, attr)
        except AttributeError:
            pass

    def __exit__(self, t, v, tb):
        if v is None:
            # Deferring commits make no sense for secondary connections.
            assert not self._deferred
            self._commit()

    @abstract
    def _parse(self, database):
        """Called during instantiation, to process database parameter."""

    @abstract
    def _connect(self):
        """Connect to the database"""

    def autoReconnect(self, f):
        """
        Placeholder for backends that may lose connection to the underlying
        database: although a primary connection is reestablished transparently
        when possible, secondary connections use transactions and they must
        restart from the beginning.
        For other backends, there's no expected transient failure so the
        default implementation is to execute the given task exactly once.
        """
        f()

    def lock(self, db_path):
        if self.LOCK:
            assert self.__lock is None, self.__lock
            # For platforms that don't support anonymous sockets,
            # we can either use zc.lockfile or an empty SQLite db
            # (with BEGIN EXCLUSIVE).
            try:
                stat = os.stat(db_path)
            except OSError as e:
                if e.errno != errno.ENOENT:
                    raise
                return # in-memory or temporary database
            s = self.__lock = socket.socket(socket.AF_UNIX)
            try:
                s.bind('\0%s:%s:%s' % (self.LOCK, stat.st_dev, stat.st_ino))
            except socket.error as e:
                if e.errno != errno.EADDRINUSE:
                    raise
                sys.exit(self.LOCKED)

    def _getDevPath(self):
        """
        """

    @requires(_getDevPath)
    def getTopologyPath(self):
        # On Windows, st_dev only exists since Python 3.4
        return socket.gethostname(), str(os.stat(self._getDevPath()).st_dev)

    @abstract
    def erase(self):
        """"""

    def restore(self, dump): # for tests
        self.erase()
        self._restore(dump)

    def _setup(self, dedup=False):
        """To be overridden by the backend to set up a database

        It must recover self._uncommitted_data from temporary object table.
        _uncommitted_data is already instantiated and must be updated with
        refcounts to data of write-locked objects, except in case of undo,
        where the refcount is increased later, when the object is read-locked.
        Keys are data ids and values are number of references.
        """

    @requires(_setup)
    def setup(self, reset=False, dedup=False):
        """Set up a database, discarding existing data first if reset is True
        """
        if reset:
            self.erase()
        self._uncommitted_data = defaultdict(int)
        self._setup(dedup)

    @abstract
    def nonempt
y(self, table):
        """Check whether table is empty or return None if it does not exist"""

    def _checkNoUnfinishedTransactions(self, *hint):
        if self.nonempty('ttrans') or self.nonempty('tobj'):
            raise DatabaseFailure(
                "The database can not be upgraded because you have unfinished"
                " transactions.
 Use an older version of NEO to verify them.")

    def migrate(self, *args, **kw):
        version = int(self.getConfiguration("version") or 0)
        if self.VERSION < version:
            raise DatabaseFailure("The database can not be downgraded.")
        while version < self.VERSION:
            version += 1
            getattr(self, '_migrate%s' % version)(*args, **kw)
            self.setConfiguration("version", version)

    def doOperation(self, app):
        pass

    def _close(self):
        """Backend-specific code to close the database"""

    @requires(_close)
    def close(self):
        self._deferredCommit()
        self._close()
        if self.__lock:
            self.__lock.close()
            del self.__lock

    def _commit(self):
        """Backend-specific code to commit the pending changes"""

    @requires(_commit)
    def commit(self):
        logging.debug('committing...')
        self._commit()
        # Instead of cancelling a timeout that would be set to defer a commit,
        # we simply use to a boolean so that _deferredCommit() does nothing.
        # IOW, epoll may wait wake up for nothing but that should be rare,
        # because most immediate commits are usually quickly followed by
        # deferred commits.
        self._deferred = 0

    def deferCommit(self):
        self._deferred = 1
        return self._deferredCommit

    def _defer