"""
@package pmsco.database.access
wrapper classes for access to a pmsco database

the most import class to be used is DatabaseAccess.

usage:
~~~~~~{.py}
db = DatabaseAccess()
db.connect("file.db")
with db.session():
    # database access here
    # ...
    # commit transaction
    session.commit()
    # continue in new transaction

# at the end of the context
# the session is closed and orm objects are detached from the database.
~~~~~~

@author Matthias Muntwiler, matthias.muntwiler@psi.ch

@copyright (c) 2016-21 by Paul Scherrer Institut @n
Licensed under the Apache License, Version 2.0 (the "License"); @n
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
  http://www.apache.org/licenses/LICENSE-2.0
"""

import fasteners
import logging
from pathlib import Path
import pmsco.database.orm as orm

logger = logging.getLogger(__name__)


class _DummyLock(object):
    """
    dummy lock used for in memory database.
    """
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass


class LockedSession(object):
    """
    database session context manager

    this context manager (to be used in a with statement)
    acquires a lock on the database lock file
    and provides a database session (orm.Session()).

    the session is closed (and pending transactions committed) on exit.
    if an exception occurs, pending transactions are rolled back before the session is closed.

    @note the term _session_ refers to a session in sqlalchemy.
    """
    def __init__(self, lock_file=None):
        self.lock_file = lock_file
        self._session = None
        self._lock = None

    def __enter__(self):
        self._lock = self.lock()
        self._lock.__enter__()
        self._session = orm.Session()
        return self._session

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            self._session.close()
        else:
            self._session.rollback()
            self._session.close()
        self._lock.__exit__(exc_type, exc_val, exc_tb)
        self._lock = None

    def lock(self):
        """
        create a file-lock context manager for the database.

        this is either a fasteners.InterProcessLock object on self._lock_filename
        or a _DummyLock object if the database is in memory.
        InterprocessLock allows to serialize access to the database by means of a lock file.
        this is necessary if multiple pmsco instances require access to the same database.
        _DummyLock is used with an in-memory database which does not require locking.

        the lock object can be used as context-manager in a with statement.
        """
        if self.lock_file:
            return fasteners.InterProcessLock(self.lock_file)
        else:
            return _DummyLock()


class DatabaseAccess(object):
    """
    basic database connection

    this class maintains a database connection and builds session objects.

    a _session_ corresponds to an sqlalchemy session, which defines the lifecycle of mapped objects.
    a session can open one or multiple (subsequent) transactions.

    usage:
    ~~~~~~{.py}
    db = DatabaseAccess()
    db.connect("file.db")
    with db.session():
        # database access
        session.commit()
    ~~~~~~

    the session object is a context handler.
    it commits the transaction and closes the session at the end of the context.
    if an exception occurs, it rolls back the transaction and closes the session before passing the exception.
    """
    def __init__(self):
        self.db_file = ""
        self.lock_file = ""

    def connect(self, db_file, lock_file=""):
        """
        connect to a new or existing database file.

        if the file does not exist, or if it is empty, a new database schema is created.

        @param db_file: name of a file or ":memory:" for an in-memory database.

        @param lock_file: name of a file that is used to lock the database.
            by default, the db_filename with a suffix of ".lock" is used.
            for most uses, the default should be fine.
            the argument is provided mainly for testing the locking functionality.

            this must be a file that is not used for anything else.
            the file does not need to exist.
            it's best if the file is in the same directory as the database file.
            all clients of a database must use the same lock file.

        @return: None
        """
        self.db_file = db_file

        if lock_file:
            self.lock_file = lock_file
        elif db_file == ":memory:":
            self.lock_file = ""
        else:
            self.lock_file = Path(str(db_file) + ".lock")

        orm.connect(orm.sqlite_link(self.db_file))

    def session(self):
        """
        open a database session.

        this function returns a pmsco.database.util.LockedSession object
        which is a context handler that provides an sqlalchemy session
        that is locked against concurrent access from other DatabaseAccess instances.
        see the class description for an example usage pattern.

        @return: pmsco.database.util.LockedSession() object.
        """
        return LockedSession(self.lock_file)
