#!/usr/bin/env python

# ###########################################################################
#
# This file is part of Taurus
#
# http://taurus-scada.org
#
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
#
# Taurus is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Taurus 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Taurus.  If not, see <http://www.gnu.org/licenses/>.
#
# ###########################################################################

"""This module contains the taurus base manager class
"""

import os
import atexit
import pkg_resources

from .util.singleton import Singleton
from .util.log import Logger, taurus4_deprecation
from .util.threadpool import ThreadPool
from .taurusbasetypes import (
    OperationMode,
    ManagerState,
    TaurusSerializationMode,
)
from .taurusauthority import TaurusAuthority
from .taurusdevice import TaurusDevice
from .taurusattribute import TaurusAttribute
from .taurusexception import TaurusException
from .taurusfactory import TaurusFactory
from taurus import core, tauruscustomsettings, getSchemeFromName


__all__ = ["TaurusManager"]

__docformat__ = "restructuredtext"


class TaurusManager(Singleton, Logger):
    """A :class:`taurus.core.util.singleton.Singleton` class designed to
    provide Taurus management.

    Example::

        >>> import taurus.core.taurusmanager
        >>> manager = taurus.core.taurusmanager.TaurusManager()
        >>> print(manager == taurus.core.taurusmanager.TaurusManager())
        True
    """

    PLUGIN_KEY = "__taurus_plugin__"

    DefaultSerializationMode = TaurusSerializationMode.Concurrent
    default_scheme = getattr(tauruscustomsettings, "DEFAULT_SCHEME", "tango")

    def __init__(self):
        """Initialization. Nothing to be done here for now."""
        pass

    def init(self, *args, **kwargs):
        """Singleton instance initialization.
        For internal usage only. Do **NOT** call this method directly
        """
        self._state = ManagerState.UNINITIALIZED
        self.call__init__(Logger)
        self.reInit()
        atexit.register(self.cleanUp)

    def reInit(self):
        """Reinitialization"""
        if self._state == ManagerState.INITED:
            return

        self.trace("reInit()")
        this_path = os.path.abspath(__file__)
        self._this_path = os.path.dirname(this_path)
        self._serialization_mode = self.DefaultSerializationMode

        self._thread_pool = ThreadPool(
            name="TaurusTP", parent=self, Psize=5, Qsize=1000
        )

        self._sthread_pool = ThreadPool(
            name="TaurusTSP", parent=self, Psize=1, Qsize=0
        )
        self._plugins = None

        self._initial_default_scheme = self.default_scheme

        self._state = ManagerState.INITED

    def cleanUp(self):
        """Cleanup"""
        if self._state == ManagerState.CLEANED:
            return
        self.trace("cleanUp()")

        if self._plugins is None:
            return
        self.trace("[TaurusManager] cleanUp")
        self._plugins = None

        self._thread_pool.join()
        self._thread_pool = None
        self._sthread_pool.join()
        self._sthread_pool = None

        self._state = ManagerState.CLEANED

    def addJob(self, job, callback=None, *args, **kw):
        """Deprecated. Wrapper of enqueueJob. See enqueueJob documentation."""
        self.deprecated(dep="addJob", alt="enqueueJob", rel="4.3.2")
        self.enqueueJob(job, callback=callback, job_args=args, job_kwargs=kw)

    def enqueueJob(
        self,
        job,
        callback=None,
        job_args=(),
        job_kwargs=None,
        serialization_mode=None,
    ):
        """Enqueue a job (callable) to the queue. The new job will be
        processed by a separate thread

        :param job: a callable object
        :type job: callable
        :param callback: called after the job has been processed
        :type callback: callable
        :param job_args: positional arguments passed to the job
        :type job_args: sequence
        :param job_kwargs: keyword arguments passed to the job
        :type job_kwargs: dict
        :param serialization_mode: serialization mode
        :type serialization_mode: TaurusSerializationMode
        """
        if job_kwargs is None:
            job_kwargs = {}

        if serialization_mode is None:
            serialization_mode = self._serialization_mode

        if serialization_mode == TaurusSerializationMode.Concurrent:
            if not hasattr(self, "_thread_pool") or self._thread_pool is None:
                self.info("Job cannot be processed.")
                self.debug(
                    "The requested job cannot be processed. "
                    + "Make sure this manager is initialized"
                )
                return
            self._thread_pool.add(job, callback, *job_args, **job_kwargs)
        elif serialization_mode == TaurusSerializationMode.Serial:
            if (
                not hasattr(self, "_sthread_pool")
                or self._sthread_pool is None
            ):
                self.info("Job cannot be processed.")
                self.debug(
                    "The requested job cannot be processed. "
                    + "Make sure this manager is initialized"
                )
                return

            self._sthread_pool.add(job, callback, *job_args, **job_kwargs)
        else:
            raise TaurusException(
                "{} serialization mode not supported".format(
                    serialization_mode
                )
            )

    def setSerializationMode(self, mode):
        """Sets the serialization mode for the system.

        :param mode: the new serialization mode
        :type mode: TaurusSerializationMode
        """
        self._serialization_mode = mode

    def getSerializationMode(self):
        """Gives the serialization operation mode.

        :return: the current serialization mode
        :rtype: TaurusSerializationMode
        """
        return self._serialization_mode

    def setOperationMode(self, mode):
        """Deprecated. Sets the operation mode for the system.

        :param mode: the new operation mode
        :type mode: OperationMode
        """
        dep = "setOperationMode"
        rel = "Taurus4"
        dbg_msg = "Don't use this method"
        msg = "%s is deprecated (from %s). %s" % (dep, rel, dbg_msg)
        self.deprecated(msg)

    def getOperationMode(self):
        """Deprecated. Gives the current operation mode.

        :return: the current operation mode
        :rtype: OperationMode
        """
        dep = "getOperationMode"
        rel = "Taurus4"
        dbg_msg = "Don't use this method"
        msg = "%s is deprecated (from %s). %s" % (dep, rel, dbg_msg)
        self.deprecated(msg)
        return OperationMode.ONLINE

    def getDefaultFactory(self):
        """Gives the default factory.

        :return: the default taurus factory
        :rtype: taurus.core.taurusfactory.TaurusFactory
        """
        return self.getPlugins().get(self.default_scheme, None)

    def getPlugins(self):
        """Gives the information about the existing plugins

        :return: the list of plugins
        :rtype: dict<str, class taurus.core.taurusfactory.TaurusFactory>
        """
        if self._plugins is None:
            self._plugins = self._build_plugins()
        return self._plugins

    def getFactory(self, scheme=None):
        """Gives the factory class object supporting the given scheme

        :param scheme: the scheme. If None the default scheme is used
        :type scheme: str or None
        :return: the factory class object for the given scheme or None if a
            proper factory is not found
        :rtype: taurus.core.taurusfactory.TaurusFactory or None
        """
        if scheme is None:
            return self.getDefaultFactory()
        return self.getPlugins().get(scheme)

    def getObject(self, cls, name):
        """Gives the object for the given class with the given name

        :param cls: object class
        :type cls: taurus.core.taurusmodel.TaurusModel
        :param name: the object name
        :type name: str
        :return: a taurus model object
        :rtype: taurus.core.taurusmodel.TaurusModel or None
        """
        factory = self._get_factory(name)
        if factory is None:
            return
        return factory.getObject(cls, name)

    def findObject(self, absolute_name):
        """Finds the object with the given name

        :param absolute_name: the object name
        :type absolute_name: str
        :return: the taurus model object or None if no suitable name found
        :rtype: taurus.core.taurusmodel.TaurusModel or None
        """
        factory = self._get_factory(absolute_name)
        if factory is None:
            return
        return factory.findObject(absolute_name)

    def findObjectClass(self, absolute_name):
        """Finds the object class for the given object name

        :param absolute_name: the object name
        :type absolute_name: str
        :return: the taurus model class object or None if no suitable name
            found
        :rtype: class taurus.core.taurusmodel.TaurusModel or None
        """
        factory = self._get_factory(absolute_name)
        if factory is None:
            return
        return factory.findObjectClass(absolute_name)

    def getAuthority(self, name):
        """Returns a database object for the given name

        :param name: database name
        :type name: str
        :return: the authority for the given name
        :rtype: taurus.core.taurusauthority.TaurusAuthority
        """
        return self.getObject(TaurusAuthority, name)

    def getDatabase(self, name):
        """Deprecated. Use getAuthority instead"""
        self.warning("getDatabase is deprecated. Use getAuthority instead")
        return self.getAuthority(self, name)

    def getDevice(self, name):
        """Returns a device object for the given name

        :param name: device name
        :type name: str
        :return: the device for the given name
        :rtype: taurus.core.taurusdevice.TaurusDevice
        """
        return self.getObject(TaurusDevice, name)

    def getAttribute(self, name):
        """Returns a attribute object for the given name

        :param name: attribute name
        :type name: str
        :return: the attribute for the given name
        :rtype: taurus.core.taurusattribute.TaurusAttribute
        """
        return self.getObject(TaurusAttribute, name)

    @taurus4_deprecation(alt="getAttribute")
    def getConfiguration(self, name):
        """Returns a configuration object for the given name

        :param name: configuration name
        :type name: str
        :return: the configuration for the given name
        :rtype: taurus.core.taurusconfiguration.TaurusConfiguration
        """
        return self.getAttribute(name)

    def _get_factory(self, name):
        scheme = self.getScheme(name)
        if scheme is None:
            return
        try:
            return self.getPlugins()[scheme]()
        except Exception:
            raise TaurusException('Invalid scheme "%s"' % scheme)

    def getScheme(self, name):
        """Returns the scheme name for a given model name

        :param name: model name
        :type name: str
        :return: scheme name
        :rtype: str
        """
        return getSchemeFromName(name, implicit=True)

    def _get_schema(self, name):
        raise DeprecationWarning(
            "_get_schema is deprecated. Use getScheme instead"
        )

    def _build_plugins(self):
        plugin_classes = self._get_plugin_classes()
        plugins = {}
        for plugin_class in plugin_classes:
            schemes = list(plugin_class.schemes)
            for scheme in schemes:
                if scheme in plugins:
                    k = plugins[scheme]
                    self.warning(
                        "Conflicting plugins: %s and %s both implement "
                        "scheme %s. Will keep using %s"
                        % (
                            k.__name__,
                            plugin_class.__name__,
                            scheme,
                            k.__name__,
                        )
                    )
                else:
                    plugins[scheme] = plugin_class
        return plugins

    def buildPlugins(self):
        """Returns the current valid plugins

        :return: plugins
        :rtype: dic
        """
        return self._build_plugins()

    def _get_plugin_classes(self):

        elems = os.listdir(self._this_path)
        dirs = []
        for elem in elems:
            if elem.startswith(".") or elem.startswith("_"):
                continue
            elem = os.path.join(self._this_path, elem)
            if not os.path.isdir(elem):
                continue
            plugin_file = os.path.join(elem, self.PLUGIN_KEY)
            if not os.path.exists(plugin_file):
                continue
            if not os.path.exists(os.path.join(elem, "__init__.py")):
                continue
            dirs.append(elem)

        plugins = []

        full_module_names = [
            "taurus.core.%s" % d.split(os.path.sep)[-1] for d in dirs
        ]

        full_module_names.extend(
            getattr(tauruscustomsettings, "EXTRA_SCHEME_MODULES", [])
        )

        full_module_names.extend(getattr(core, "PLUGIN_SCHEME_MODULES", []))

        # ---------------------------------------------------------------------
        # Note: this is an experimental feature introduced in v 4.5.0a
        # It may be removed or changed in future releases

        # Discover the taurus.core.schemes plugins
        schemes_ep = pkg_resources.iter_entry_points("taurus.core.schemes")
        full_module_names.extend([p.name for p in schemes_ep])
        # ---------------------------------------------------------------------

        for full_module_name in full_module_names:
            try:
                m = __import__(full_module_name, fromlist=["*"], level=0)
            except Exception:
                self.debug("Failed to inspect %s" % (full_module_name))
                continue
            for s in m.__dict__.values():
                plugin = None
                try:
                    if issubclass(s, TaurusFactory) and issubclass(
                        s, Singleton
                    ):
                        if hasattr(s, "schemes"):
                            schemes = getattr(s, "schemes")
                            if len(schemes):
                                plugin = s
                            else:
                                scheme = self._find_scheme(s)
                                if scheme is not None:
                                    s.schemes = (scheme,)
                                    plugin = s
                except Exception:
                    pass
                if plugin is not None:
                    self.debug("Found plugin %s" % plugin.__name__)
                    plugins.append(plugin)
        return plugins

    def _find_scheme(self, factory_class):
        class_name = factory_class.__name__
        for i in range(1, len(class_name)):
            if class_name[i].isupper():
                return class_name[:i].lower()

    def applyPendingOperations(self, ops):
        """Executes the given operations

        :param ops: the sequence of operations
        :type ops: sequence<taurus.core.taurusoperation.TaurusOperation>
        """
        for o in ops:
            o.execute()

    def changeDefaultPollingPeriod(self, period):
        plugin_classes = self._get_plugin_classes()
        for plugin_class in plugin_classes:
            scheme = plugin_class.schemes[0]
            self.getFactory(scheme)().changeDefaultPollingPeriod(period)

    def __str__name__(self, name):
        return "{0}({1})".format(self.__class__.__name__, name)

    def __str__(self):
        return self.__str__name__("")

    def __repr__(self):
        return self.__str__name__("")


if __name__ == "__main__":
    manager = TaurusManager()
    print(manager.getPlugins())
