# -*- coding: utf-8 -*-
# This file is a part of the AnyBlok project
#
#    Copyright (C) 2014 Jean-Sebastien SUZANNE <jssuzanne@anybox.fr>
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file,You can
# obtain one at http://mozilla.org/MPL/2.0/.
from os.path import join, exists
from logging import getLogger
import nose

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.exc import ProgrammingError, OperationalError

from anyblok import Declarations
from .config import Configuration
from .imp import ImportManager
from .blok import BlokManager
from .logging import log
from .environment import EnvironmentManager
from .migration import Migration
from .common import format_bloks
from .authorization.query import QUERY_WITH_NO_RESULTS, PostFilteredQuery

logger = getLogger(__name__)


class RegistryManagerException(Exception):
    """ Simple Exception for Registry """


class RegistryException(Exception):
    """ Simple Exception for Registry """


class RegistryManager:
    """ Manage the global registry

    Add new entry::

        RegistryManager.declare_entry('newEntry')
        RegistryManager.init_blok('newBlok')
        EnvironmentManager.set('current_blok', 'newBlok')
        RegistryManager.add_entry_in_register(
            'newEntry', 'oneKey', cls_)
        EnvironmentManager.set('current_blok', None)

    Remove an existing entry::

        if RegistryManager.has_entry_in_register('newBlok', 'newEntry',
                                                        'oneKey'):
            RegistryManager.remove_entry_in_register(
                'newBlok', 'newEntry', 'oneKey', cls_)

    get a new registry for a database::

        registry = RegistryManager.get('my database')

    """

    loaded_bloks = {}
    declared_entries = []
    declared_cores = []
    callback_assemble_entries = {}
    callback_initialize_entries = {}
    registries = {}
    needed_bloks = []

    @classmethod
    def has_blok(cls, blok):
        """ Return True if the blok is already loaded

        :param blok: name of the blok
        :rtype: boolean
        """

        return blok in cls.loaded_bloks

    @classmethod
    def clear(cls):
        """ Clear the registry dict to force the creation of new registry """
        registries = [r for r in cls.registries.values()]
        for registry in registries:
            registry.close()

    @classmethod
    def get(cls, db_name):
        """ Return an existing Registry

        If the Registry doesn't exist then the Registry are created and added
        to registries dict

        :param db_name: the name of the database linked to this registry
        :rtype: ``Registry``
        """
        EnvironmentManager.set('db_name', db_name)
        if db_name in cls.registries:
            return cls.registries[db_name]

        registry = Registry(db_name)
        cls.registries[db_name] = registry
        return registry

    @classmethod
    def reload(cls, blok):
        """ Reload the blok

        The purpose is to reload the python module to get changes in python
        file

        :param blok: the name of the blok to reload
        """
        try:
            mod = ImportManager.get(blok)
            EnvironmentManager.set('current_blok', blok)
            mod.reload()
        finally:
            EnvironmentManager.set('current_blok', None)

        for registry in cls.registries.values():
            registry.reload()

    @classmethod
    def declare_core(cls, core):
        """ Add new core in the declared cores

        ::

            RegistryManager.declare_core('Core name')

            -----------------------------------------

            @Declarations.register(Declarations.Core)
            class ``Core name``:
                ...

        .. warning::

            The core must be declared in the application, not in the bloks
            The declaration must be done before the loading of the bloks

        :param core: core name
        """

        if core not in cls.declared_cores:
            cls.declared_cores.append(core)

    @classmethod
    def undeclare_core(cls, core):
        if core in cls.declared_cores:
            cls.declared_cores.remove(core)

    @classmethod
    def declare_entry(cls, entry, assemble_callback=None,
                      initialize_callback=None):
        """ Add new entry in the declared entries

        ::

            def assemble_callback(registry):
                ...

            def initialize_callback(registry):
                ...

            RegistryManager.declare_entry(
                'Entry name', assemble_callback=assemble_callback,
                initialize_callback=initialize_callback)

            @Declarations.register(Declarations.``Entry name``)
            class MyClass:
                ...

        .. warning::

            The entry must be declared in the application, not in the bloks
            The declaration must be done before the loading of the bloks

        :param entry: entry name
        :param assemble_callback: function callback to call to assemble
        :param initialize_callback: function callback to call to init after
            assembling
        """

        if entry not in cls.declared_entries:
            cls.declared_entries.append(entry)

            if assemble_callback:
                cls.callback_assemble_entries[entry] = assemble_callback

            if initialize_callback:
                cls.callback_initialize_entries[entry] = initialize_callback

    @classmethod
    def undeclare_entry(cls, entry):
        if entry in cls.declared_entries:
            cls.declared_entries.remove(entry)

            if entry in cls.callback_assemble_entries:
                del cls.callback_assemble_entries[entry]

            if entry in cls.callback_initialize_entries:
                del cls.callback_initialize_entries[entry]

    @classmethod
    def init_blok(cls, blokname):
        """ init one blok to be known by the RegistryManager

        All bloks loaded must be initialized because the registry will be
        created with this information

        :param blokname: name of the blok
        """
        blok = {
            'Core': {core: [] for core in cls.declared_cores},
            'properties': {},
            'removed': [],
        }
        for de in cls.declared_entries:
            blok[de] = {'registry_names': []}

        cls.loaded_bloks[blokname] = blok

    @classmethod
    def has_core_in_register(cls, blok, core):
        """ Return True if One Class exist in this blok for this core

        :param blok: name of the blok
        :param core: is the existing core name
        """
        return len(cls.loaded_bloks[blok]['Core'][core]) > 0

    @classmethod
    def add_core_in_register(cls, core, cls_):
        """ Load core in blok

        warning the global var current_blok must be filled on the good blok

        :param core: is the existing core name
        :param ``cls_``: Class of the Core to save in loaded blok target
            registry
        """

        current_blok = EnvironmentManager.get('current_blok')
        cls.loaded_bloks[current_blok]['Core'][core].append(cls_)

    @classmethod
    def remove_in_register(cls, cls_):
        """ Remove Class in blok and in entry

        :param ``cls_``: Class of the entry / key to remove in loaded blok
        """
        current_blok = EnvironmentManager.get('current_blok')
        removed = cls.loaded_bloks[current_blok]['removed']
        if cls_ not in removed:
            removed.append(cls_)

    @classmethod
    def has_entry_in_register(cls, blok, entry, key):
        """ Return True if One Class exist in this blok for this entry

        :param blok: name of the blok
        :param entry: is the existing entry name
        :param key: is the existing key in the entry
        """
        if entry not in cls.loaded_bloks[blok]:
            return False

        if key not in cls.loaded_bloks[blok][entry]:
            return False

        return len(cls.loaded_bloks[blok][entry][key]['bases']) > 0

    @classmethod
    def add_entry_in_register(cls, entry, key, cls_, **kwargs):
        """ Load entry in blok

        warning the global var current_blok must be filled on the good blok
        :param entry: is the existing entry name
        :param key: is the existing key in the entry
        :param ``cls_``: Class of the entry / key to remove in loaded blok
        """

        bases = []

        for base in cls_.__bases__:
            if hasattr(base, '__registry_name__'):
                bases.append(base)

        setattr(cls_, '__anyblok_bases__', bases)

        cb = EnvironmentManager.get('current_blok')

        if key not in cls.loaded_bloks[cb][entry]:
            cls.loaded_bloks[cb][entry][key] = {
                'bases': [],
                'properties': {},
            }

        cls.loaded_bloks[cb][entry][key]['properties'].update(kwargs)
        # Add before in registry because it is the same order than the
        # inheritance __bases__ and __mro__
        cls.loaded_bloks[cb][entry][key]['bases'].insert(0, cls_)

        if key not in cls.loaded_bloks[cb][entry]['registry_names']:
            cls.loaded_bloks[cb][entry]['registry_names'].append(key)

    @classmethod
    def get_entry_properties_in_register(cls, entry, key):
        cb = EnvironmentManager.get('current_blok')
        if key not in cls.loaded_bloks[cb][entry]:
            return {}

        return cls.loaded_bloks[cb][entry][key]['properties'].copy()

    @classmethod
    def has_blok_property(cls, property_):
        """ Return True if the property exists in blok

        :param property\_: name of the property
        """
        blok = EnvironmentManager.get('current_blok')

        if property_ in cls.loaded_bloks[blok]['properties']:
            return True

        return False

    @classmethod
    def add_or_replace_blok_property(cls, property_, value):
        """ Save the value in the properties

        :param property\_: name of the property
        :param value: the value to save, the type is not important
        """
        blok = EnvironmentManager.get('current_blok')
        cls.loaded_bloks[blok]['properties'][property_] = value

    @classmethod
    def get_blok_property(cls, property_, default=None):
        """ Return the value in the properties

        :param property\_: name of the property
        :param default: return default If not entry in the property
        """
        blok = EnvironmentManager.get('current_blok')
        return cls.loaded_bloks[blok]['properties'].get(property_, default)

    @classmethod
    def remove_blok_property(cls, property_):
        """ Remove the property if exist

        :param property\_: name of the property
        """
        blok = EnvironmentManager.get('current_blok')
        if cls.has_blok_property(property_):
            del cls.loaded_bloks[blok]['properties'][property_]

    @classmethod
    def add_needed_bloks(cls, *bloks):
        for blok in bloks:
            if blok not in cls.needed_bloks:
                cls.needed_bloks.append(blok)

    @classmethod
    def get_needed_bloks(cls):
        return cls.needed_bloks


class Registry:
    """ Define one registry

    A registry is linked to a database, and stores the definition of the
    installed Bloks, Models, Mixins for this database::

        registry = Registry('My database')
    """

    def __init__(self, db_name):
        self.db_name = db_name
        url = Configuration.get_url(db_name=db_name)
        echo = bool(int(Configuration.get('db_echo') or False))
        self.engine = create_engine(url, echo=echo)
        self.registry_base = type("RegistryBase", tuple(), {
            'registry': self,
            'Env': EnvironmentManager})
        self.noautomigration = Configuration.get('noautomigration')
        self.ini_var()
        self.Session = None
        self.nb_query_bases = self.nb_session_bases = 0
        self.load()

    def ini_var(self):
        """ Initialize the var to load the registry """
        self.loaded_namespaces = {}
        self.declarativebase = None
        self.loaded_bloks = {}
        self.loaded_registries = {x + '_names': []
                                  for x in RegistryManager.declared_entries}
        self.loaded_cores = {core: []
                             for core in RegistryManager.declared_cores}
        self.ordered_loaded_bloks = []
        self.loaded_namespaces = {}
        self.children_namespaces = {}
        self.properties = {}
        self.removed = []
        self._precommit_hook = []

    def get(self, namespace):
        """ Return the namespace Class

        :param namespace: namespace to get from the registry str
        :rtype: namespace cls
        :exception: RegistryManagerException
        """
        if namespace not in self.loaded_namespaces:
            raise RegistryManagerException(
                "No namespace %r loaded" % namespace)

        return self.loaded_namespaces[namespace]

    def has(self, namespace):
        return True if namespace in self.loaded_namespaces else False

    def get_bloks_by_states(self, *states):
        """ Return the bloks in these states

        :param states: list of the states
        :rtype: list of blok's name
        """
        if not states:
            return []

        res = []
        query = """
            SELECT system_blok.name
            FROM system_blok
            WHERE system_blok.state in ('%s')
            ORDER BY system_blok.order""" % "', '".join(states)
        if self.Session:
            res = self.execute(query).fetchall()
        else:
            conn = None
            try:
                conn = self.engine.connect()
                res = conn.execute(query).fetchall()
            except (ProgrammingError, OperationalError):
                pass
            finally:
                if conn:
                    conn.close()

        if res:
            return [x[0] for x in res]

        return []

    def get_bloks_to_load(self):
        """ Return the bloks to load by the registry

        :rtype: list of blok's name
        """
        return self.get_bloks_by_states('installed', 'toupdate')

    def get_bloks_to_install(self, loaded):
        """ Return the bloks to install in the registry

        :rtype: list of blok's name
        """
        toinstall = self.get_bloks_by_states('toinstall')
        for blok in BlokManager.auto_install:
            if blok not in (toinstall + loaded):
                toinstall.append(blok)

        for blok in RegistryManager.get_needed_bloks():
            if blok not in (toinstall + loaded):
                toinstall.append(blok)

        return toinstall

    def check_permission(self, target, principals, permission):
        """Check that one of the principals has permisson on target.

        :param target: model instance (record) or class. Checking a permission
                       on a model class with a policy that needs to work on
                       records is considered a configuration error: the policy
                       has the right to fail.
        :param principals: list, set or tuple of strings
        :rtype: bool
        """
        return self.lookup_policy(target, permission).check(
            target, principals, permission)

    def wrap_query_permission(self, query, principals, permission, models=()):
        """Wrap query to return only authorized results

        :param principals: list, set or tuple of strings
        :param models: models on which to apply security filtering. If
                       not supplied, it will be infered from the query. The
                       length and ordering much match that of expected results.
        :returns: a query-like object, implementing the results fetching API,
                  but that can't be further filtered.

        This method calls all the relevant policies to apply pre- and
        post-filtering. Although postfiltering is discouraged in authorization
        policies for performance and expressiveness (limit, offset),
        there are cases for which it is unavoidable, or in which the tradeoff
        goes the other way.

        In normal operation, the relevant models are infered directly from
        the query.
        For join situations, and more complex queries, the caller has control
        on the models on which to exert permission checking.

        For instance, it might make sense to use a join between Model1 and
        Model2 to actually constrain Model1 (on which permission filtering
        should occur) by information contained in Model2, even if the passed
        principals should not grant access to the relevant Model2 records.
        """
        if not models:
            models = []
            for column in query.column_descriptions:
                if column['aliased']:
                    # actually, think aliases could work almost direcly
                    # it's just a matter of documenting that what the policy
                    # gets may be an alias instead of a model.
                    raise NotImplementedError(
                        "Sorry, table/model aliases aren't supported yet. "
                        "Here's the unsupported column: %r" % column)
                if not issubclass(column['type'], self.registry_base):
                    raise NotImplementedError(
                        "Sorry, only model columns are supported for now. "
                        "Here is the unsupported one: %r" % column)
                models.append(column['type'])

        postfilters = {}
        for model in models:
            policy = self.lookup_policy(model, permission)
            query = policy.filter(model, query, principals, permission)
            if query is False:  # TODO use a dedicated singleton ?
                return QUERY_WITH_NO_RESULTS
            if policy.postfilter is not None:
                postfilters[model] = lambda rec: policy.postfilter(
                    rec, principals, permission)
        return PostFilteredQuery(query, postfilters)

    def lookup_policy(self, target, permission):
        """Return the policy instance that applies to target or its model.

        :param target: model class or instance

        If a policy is declared for the precise permission, it is returned.
        Otherwise, the default policy for that model is returned.
        By ultimate default the special
        :class:`anyblok.authorization.rule.DenyAll` is returned.
        """
        model_name = target.__registry_name__
        policy = self._authz_policies.get((model_name, permission))
        if policy is not None:
            return policy

        policy = self._authz_policies.get(model_name)
        if policy is not None:
            return policy

        return self._authz_policies.get(None)

    def load_entry(self, blok, entry):
        """ load one entry type for one blok

        :param blok: name of the blok
        :param entry: declaration type to load
        """
        _entry = RegistryManager.loaded_bloks[blok][entry]
        for key in _entry['registry_names']:
            v = _entry[key]
            if key not in self.loaded_registries:
                self.loaded_registries[key] = {'properties': {}, 'bases': []}

            self.loaded_registries[key]['properties'].update(v['properties'])
            old_bases = [] + self.loaded_registries[key]['bases']
            self.loaded_registries[key]['bases'] = v['bases']
            self.loaded_registries[key]['bases'] += old_bases
            self.loaded_registries[entry + '_names'].append(key)

    def load_core(self, blok, core):
        """ load one core type for one blok

        :param blok: name of the blok
        :param core: the core name to load
        """
        bases = RegistryManager.loaded_bloks[blok]['Core'][core]
        for base in bases:
            self.loaded_cores[core].insert(0, base)

    def load_properties(self, blok):
        properties = RegistryManager.loaded_bloks[blok]['properties']
        for k, v in properties.items():
            if k not in self.properties:
                self.properties[k] = v
            elif isinstance(self.properties[k], dict) and isinstance(v, dict):
                self.properties.update(v)
            elif isinstance(self.properties[k], list) and isinstance(v, list):
                self.properties.extend(v)
            else:
                self.properties[k] = v

    def load_removed(self, blok):
        for removed in RegistryManager.loaded_bloks[blok]['removed']:
            if removed not in self.removed:
                self.removed.append(removed)

    def load_blok(self, blok, toinstall, toload):
        """ load on blok, load all the core and all the entry for one blok

        :param blok: name of the blok
        :exception: RegistryManagerException
        """
        if blok in self.ordered_loaded_bloks:
            return True

        if blok not in BlokManager.bloks:
            return False

        b = BlokManager.bloks[blok](self)
        for required in b.required + b.conditional:
            if not self.load_blok(required, toinstall, toload):
                raise RegistryManagerException(
                    "Required blok not found")

        for optional in b.optional:
            if toinstall or optional in toload:
                self.load_blok(optional, toinstall, toload)

        for core in RegistryManager.declared_cores:
            self.load_core(blok, core)

        for entry in RegistryManager.declared_entries:
            self.load_entry(blok, entry)

        self.load_properties(blok)
        self.load_removed(blok)
        self.loaded_bloks[blok] = b
        self.ordered_loaded_bloks.append(blok)
        logger.info("Blok %r loaded" % blok)
        return True

    def update_to_install_blok_dependencies_state(self, toinstall):
        dependencies_to_install = []

        def check_dependencies(blok):
            if blok in dependencies_to_install:
                return True

            if blok not in BlokManager.bloks:
                return False

            b = BlokManager.bloks[blok](self)
            for required in b.required:
                if not check_dependencies(required):
                    raise RegistryManagerException(
                        "%r: Required blok not found %r" % (blok, required))

            for optional in b.optional:
                check_dependencies(optional)

            if blok not in toinstall:
                dependencies_to_install.append(blok)

            return True

        for blok in toinstall:
            check_dependencies(blok)

        if dependencies_to_install:
            query = """
                update system_blok
                set state='toinstall'
                where name in ('%s')
                and state = 'uninstalled'""" % "', '".join(
                dependencies_to_install)
            if self.Session:
                self.execute(query)
            else:
                conn = None
                try:
                    conn = self.engine.connect()
                    conn.execute(query)
                except (ProgrammingError, OperationalError):
                    pass
                finally:
                    if conn:
                        conn.close()

            return True

        return False

    def add_in_registry(self, namespace, base):
        """ Add a class as an attribute of the registry

        :param namespace: tree path of the attribute
        :param base: class to add
        """
        namespace = namespace.split('.')[1:]

        def final_namespace(parent, child):
            if hasattr(parent, child) and getattr(parent, child):
                other_base = get_namespace(parent, child)
                other_base = other_base.children_namespaces.copy()
                for ns, cns in other_base.items():
                    setattr(base, ns, cns)

                if hasattr(parent, 'children_namespaces'):
                    if child in parent.children_namespaces:
                        parent.children_namespaces[child] = base

            elif hasattr(parent, 'children_namespaces'):
                parent.children_namespaces[child] = base

            setattr(parent, child, base)

        def get_namespace(parent, child):
            if hasattr(parent, child) and getattr(parent, child):
                return getattr(parent, child)

            tmpns = type(child, tuple(), {'children_namespaces': {}})
            if hasattr(parent, 'children_namespaces'):
                parent.children_namespaces[child] = tmpns

            setattr(parent, child, tmpns)
            return tmpns

        def update_namespaces(parent, namespaces):
            if len(namespaces) == 1:
                final_namespace(parent, namespaces[0])
            else:
                new_parent = get_namespace(parent, namespaces[0])
                update_namespaces(new_parent, namespaces[1:])

        update_namespaces(self, namespace)

    def create_session_factory(self):
        """Create the SQLA Session factory

        in function of the Core Session class ans the Core Qery class
        """
        query_bases = [] + self.loaded_cores['Query']
        query_bases += [self.registry_base]
        Query = type('Query', tuple(query_bases), {})
        Session = type('Session', tuple(self.loaded_cores['Session']), {
            'registry_query': Query})

        bind = self.connection() if self.Session else self.engine
        self.Session = scoped_session(
            sessionmaker(bind=bind, class_=Session),
            EnvironmentManager.scoped_function_for_session)
        self.nb_query_bases = len(self.loaded_cores['Query'])
        self.nb_session_bases = len(self.loaded_cores['Session'])

    def must_recreate_session_factory(self):
        """Check if the SQLA Session Factory must be destroy and recreate

        :rtype: Boolean, True if nb Core Session/Query inheritance change
        """
        nb_query_bases = len(self.loaded_cores['Query'])
        if nb_query_bases != self.nb_query_bases:
            return True

        nb_session_bases = len(self.loaded_cores['Session'])
        if nb_session_bases != self.nb_session_bases:
            return True

        return False

    @log(logger)
    def load(self):
        """ Load all the namespaces of the registry

        Create all the table, make the shema migration
        Update Blok, Model, Column rows
        """
        mustreload = False
        blok2install = None
        try:
            self.declarativebase = declarative_base(class_registry=dict(
                registry=self))
            toload = self.get_bloks_to_load()
            toinstall = self.get_bloks_to_install(toload)
            if self.update_to_install_blok_dependencies_state(toinstall):
                toinstall = self.get_bloks_to_install(toload)

            for blok in toload:
                self.load_blok(blok, False, toload)

            if toinstall:
                if self.noautomigration:
                    raise RegistryManager("Install module is forbidden with "
                                          "no auto migration mode")

                blok2install = toinstall[0]
                self.load_blok(blok2install, True, toload)

            instrumentedlist_base = [] + self.loaded_cores['InstrumentedList']
            instrumentedlist_base += [list]
            self.InstrumentedList = type(
                'InstrumentedList', tuple(instrumentedlist_base), {})

            for entry in RegistryManager.declared_entries:
                if entry in RegistryManager.callback_assemble_entries:
                    logger.info('Assemble %r entry' % entry)
                    RegistryManager.callback_assemble_entries[entry](self)

            if self.Session is None or self.must_recreate_session_factory():
                self.create_session_factory()
            else:
                self.flush()

            mustreload = self.apply_model_schema_on_table() or mustreload

        except:
            self.close()
            raise

        test_blok = blok2install and Configuration.get(
            'test_blok_at_install')
        selected_bloks = format_bloks(Configuration.get('selected_bloks'))
        in_selected_bloks = blok2install in (selected_bloks or [blok2install])
        unwanted_bloks = format_bloks(Configuration.get('unwanted_bloks'))
        not_in_unwanted_bloks = blok2install not in (unwanted_bloks or [])

        if test_blok and in_selected_bloks and not_in_unwanted_bloks:
            self.System.Blok.load_all()
            self.run_test(blok2install)
            if len(toinstall) > 1 or mustreload:
                self.reload()

        else:
            if len(toinstall) > 1 or mustreload:
                self.reload()
            else:
                self.System.Blok.load_all()

    def run_test(registry, blok2install):
        defaultTest = join(BlokManager.getPath(blok2install), 'tests')
        if exists(defaultTest):

            class ContextSuite(nose.suite.ContextSuite):

                def __init__(self, *args, **kwargs):
                    super(ContextSuite, self).__init__(*args, **kwargs)
                    if self.context is not None:
                        self.context.registry = registry

            class ContextSuiteFactory(nose.suite.ContextSuiteFactory):
                suiteClass = ContextSuite

            class TestLoader(nose.loader.TestLoader):

                def __init__(self, *args, **kwargs):
                    super(TestLoader, self).__init__(*args, **kwargs)
                    self.suiteClass = ContextSuiteFactory(config=self.config)

            nose.run(defaultTest=[defaultTest], testLoader=TestLoader(),
                     argv=['-v', '-s'])
        else:
            logger.warning("Blok %r has no %r directory" % (
                blok2install, defaultTest))

    def apply_model_schema_on_table(self):
        # replace the engine by the session.connection for bind attribute
        # because session.connection is already the connection use
        # by blok, migration and all write on the data base
        # or use engine for bind, force create_all method to create new
        # new connection, this new connection have not acknowedge of the
        # data in the session.connection, and risk of bad lock on the
        # tables
        if not self.noautomigration:
            self.declarativebase.metadata.create_all(self.connection())

        self.migration = Migration(self)
        self.migration.auto_upgrade_database()
        mustreload = False
        for entry in RegistryManager.declared_entries:
            if entry in RegistryManager.callback_initialize_entries:
                logger.info('Initialize %r entry' % entry)
                r = RegistryManager.callback_initialize_entries[entry](
                    self)
                mustreload = mustreload or r

        return mustreload

    def close_session(self):
        """ Close only the session, not the registry
        After the call of this method the registry won't be usable
        you should use close method which call this method
        """
        if self.Session:
            session = self.Session()
            session.rollback()
            session.expunge_all()
            session.close_all()

    def close(self):
        """Release the session, connection and engine"""
        self.close_session()
        self.engine.dispose()
        if self.db_name in RegistryManager.registries:
            del RegistryManager.registries[self.db_name]

    def __getattr__(self, attribute):
        # TODO safe the call of session for reload
        if self.Session:
            session = self.Session()
            if attribute == 'session':
                return session
            if hasattr(session, attribute):
                return getattr(session, attribute)

        else:
            super(Registry, self).__getattr__(attribute)

    def precommit_hook(self, registryname, method, put_at_the_if_exist):
        """ Add a method in the precommit_hook list

        a precommit hook is a method called just after the commit, it is used
        to call this method once, because a hook is saved only once

        :param registryname: namespace of the model
        :param method: method to call on the registryname
        :param put_at_the_if_exist: if true and hook allready exist then the
            hook are moved at the end
        """
        entry = (registryname, method)
        if entry in self._precommit_hook:
            if put_at_the_if_exist:
                self._precommit_hook.remove(entry)
                self._precommit_hook.append(entry)

        else:
            self._precommit_hook.append(entry)

    def commit(self, *args, **kwargs):
        """ Overload the commit method of the SqlAlchemy session """
        hooks = []
        if self._precommit_hook:
            hooks.extend(self._precommit_hook)

        for hook in hooks:
            Model = self.loaded_namespaces[hook[0]]
            method = hook[1]
            getattr(Model, method)()
            self._precommit_hook.remove(hook)

        self.session_commit(*args, **kwargs)

    def session_commit(self, *args, **kwargs):
        if self.Session:
            session = self.Session()
            session.commit(*args, **kwargs)

    def clean_model(self):
        """ Clean the registry of all the namespaces """
        for model in self.loaded_namespaces.keys():
            name = model.split('.')[1]
            if hasattr(self, name) and getattr(self, name):
                setattr(self, name, None)

    @log(logger)
    def reload(self):
        """ Reload the registry, close session, clean registry, reinit var """
        # self.close_session()
        self.clean_model()
        self.ini_var()
        self.load()

    @log(logger, withargs=True)
    def upgrade(self, install=None, update=None, uninstall=None):
        """ Upgrade the current registry

        :param install: list of the blok to install
        :param update: list of the blok to update
        :param uninstall: list of the blok to uninstall
        :exception: RegistryException
        """
        Blok = self.System.Blok
        Association = Blok.Association
        Blok2 = Blok.aliased()

        def get_bloks(blok, filter_states, filter_modes=None):
            if filter_modes is None:
                filter_modes = Association.MODES.keys()

            query = Blok.query()
            query = query.join(Association, Association.blok == Blok.name)
            query = query.join(Blok2, Association.linked_blok == Blok2.name)
            query = query.filter(Blok2.name == blok)
            query = query.filter(Blok.state.in_(filter_states))
            query = query.filter(Association.mode.in_(filter_modes))
            return query.all().name

        def apply_state(blok_name, state, in_states):
            query = Blok.query().filter(Blok.name == blok_name)
            blok = query.first()
            if blok is None:
                raise RegistryException(
                    "Blok %r not found in entry point declarations" %
                    blok_name)
            if blok.state not in in_states:
                raise RegistryException(
                    "Apply state %r is forbidden because the state %r of "
                    "blok %r is not one of %r" % (
                        state, blok.state, blok_name, in_states))
            query.update({Blok.state: state}, synchronize_session='fetch')

        def upgrade_state_bloks(state):
            if state == 'toinstall':
                def wrap(bloks):
                    for blok in bloks:
                        apply_state(blok, state, ['uninstalled', state])

            elif state == 'toupdate':
                def wrap(bloks):
                    for blok in bloks:
                        apply_state(blok, state, ['installed', state])
                        upgrade_state_bloks(state)(get_bloks(blok,
                                                             ['installed']))

            elif state == 'touninstall':
                def wrap(bloks):
                    for blok in bloks:
                        if Blok.check_if_the_conditional_are_installed(blok):
                            raise RegistryException(
                                "the blok %r can not be unistalled because "
                                "this blok is a conditional blok and all the "
                                "bloks in his conditional list are installed "
                                "You must uninstall one of them" % blok)
                        apply_state(blok, state, ['installed', state])
                        upgrade_state_bloks(state)(get_bloks(blok, [
                            'installed', 'toinstall', 'touninstall'],
                            filter_modes=['required', 'conditional']))
                        upgrade_state_bloks('toupdate')(get_bloks(blok, [
                            'installed', 'toinstall', 'touninstall'],
                            filter_modes=['optional']))

            else:
                raise Declarations.RegistryManager("Unknow state %r" % state)

            return wrap

        upgrade_state_bloks('toinstall')(install or [])
        upgrade_state_bloks('toupdate')(update or [])
        upgrade_state_bloks('touninstall')(uninstall or [])

        self.commit()
        self.reload()
