
__author__    = "Andre Merzky, Ole Weidner"
__copyright__ = "Copyright 2012-2013, The SAGA Project"
__license__   = "MIT"

import copy

import radical.utils            as ru
import radical.utils.signatures as rus

from . import exceptions               as se

from .engine import engine
from . import context
from . import base


# ------------------------------------------------------------------------------
#
class _ContextList (list) :
    """
    The `saga.Session` class has a 'contexts' member which is a mutable list of
    `saga.Context` instances.  Adding contexts to that list is semantically
    equivalent to calling `saga.Session.add_context (ctx)`, which (a) creates
    a deep copy of the context, and (b) initializes the context (i.e. calls
    `ctx._initialize (self)`, with `self` being the session instance).  We thus
    create our own provate `_ContextList` class which inherits from the native
    Python `list` class, and overload the `append()` call with said semantics.
    """

    # --------------------------------------------------------------------------
    #
    def __init__ (self, session=None, *args, **kwargs) :

        self._session = session

        if  session :
            self._logger  = session._logger
        else :
            self._logger  = ru.Logger('radical.saga')

        base_list = super  (_ContextList, self)
        base_list.__init__ (*args, **kwargs)

    # --------------------------------------------------------------------------
    #
    def append (self, ctx, session=None) :

        ctx_clone = self._initialise_context(ctx, session)

        # context initialized ok, add it to the list of known contexts
        super (_ContextList, self).append (ctx_clone)

    # --------------------------------------------------------------------------
    #
    def insert(self, index, ctx, session=None) :

        ctx_clone = self._initialise_context(ctx, session)

        # context initialized ok, add it to the list of known contexts
        super (_ContextList, self).insert (0, ctx_clone)


    # --------------------------------------------------------------------------
    # Initialise a context to be added to the list of known contexts
    # Returns a cloned, initialised context that can be added to the context
    # list.
    def _initialise_context(self, ctx, session=None):

        if  not isinstance (ctx, context.Context) :
            raise TypeError("item to add is not a saga.Context instance")

        # create a deep copy of the context (this keeps _adaptor etc)
        ctx_clone = context.Context  (ctx.type)
        ctx._attributes_deep_copy (ctx_clone)

        if  not session :
            session = self._session
            logger  = self._logger
        else :
            logger  = session._logger


        # try to initialize that context, i.e. evaluate its attributes and
        # infer additional runtime information as needed
      # logger.debug ("adding  context : %s" % (ctx_clone))

        if  not session :
            logger.warning ("cannot initialize context - no session: %s" \
                       % (ctx_clone))
        else :
            try :
                ctx_clone._initialize (session)
            except se.SagaException as e:
                msg = "Cannot add context, initialization failed (%s)"  %  str(e)
                raise se.BadParameter (msg) from e

        return ctx_clone

    # --------------------------------------------------------------------------
    #
    def __deepcopy__(self, memo):

        ret = _ContextList()

        for c in self:
            ret.append(c)

        return ret


# ------------------------------------------------------------------------------
#
class Session (base.SimpleBase) :
    """A SAGA Session object as defined in GFD.90.

    A SAGA session has the purpose of scoping the use of security credentials
    for remote operations.  In other words, a session instance acts as
    a container for security Context instances -- SAGA objects (such as
    job.Service or filesystem.File) created in that session will then use
    exactly the security contexts from that session (and no others).

    That way, the session serves two purposes:  (1) it helps SAGA to decide
    which security mechanism should be used for what interaction, and (2) it
    helps SAGA to find security credentials which would be difficult to pick up
    automatically.

    The use of a session is as follows:


    Example::


        # define an ssh context
        c = saga.Context('ssh')
        c.user_cert = '$HOME/.ssh/special_id_rsa.pub'
        c.user_key  = '$HOME/.ssh/special_id_rsa'

        # add it to a session
        s = saga.Session
        s.add_context(c)

        # create a job service in this session -- that job service can now
        # *only* use that ssh context.
        j = saga.job.Service('ssh://remote.host.net/', s)


    The session argument to the L{job.Service} constructor is fully optional --
    if left out, SAGA will use default session, which picks up some default
    contexts as described above -- that will suffice for the majority of use
    cases.

    A session instance exposes a `context` property, which is a list of
    authentication contexts managed by this session.  As the contexts and the
    session are stateless, it is safe to modify this list as needed.
    """

    # FIXME: session deep copy not implemented

    # --------------------------------------------------------------------------
    #
    @rus.takes   ('Session',
                  rus.optional(bool))
    @rus.returns (rus.nothing)
    def __init__ (self, default=True, uid=None):
        """
        default: bool
        ret:     None
        """

        simple_base = super  (Session, self)
        simple_base.__init__ (uid=uid)

        self._logger = ru.Logger('radical.saga')

        # if the default session is expected, we point our context list to the
        # shared list of the default session singleton.  Otherwise, we create
        # a private list which is not populated.

        # a session also has a lease manager, for adaptors in this session to use.

        if  default :
            default_session     = DefaultSession (uid=self._id)
            self.contexts       = copy.deepcopy(default_session.contexts)
            self._lease_manager = default_session._lease_manager
        else :
            self.contexts       = _ContextList (session=self)

            # FIXME: at the moment, the lease manager is owned by the session.
            # Howevwer, the pty layer is the main user of the lease manager,
            # and we thus keep the lease manager options in the pty subsection.
            # So here we are, in the session, evaluating the pty config options.
            self._cfg = ru.Config(module='radical.saga.session')
            self._lease_manager = ru.LeaseManager (
                    max_pool_size=self._cfg.pty.connection_pool_size,
                    max_pool_wait=self._cfg.pty.connection_pool_wait,
                    max_obj_age  =self._cfg.pty.connection_pool_ttl
                    )


    # ----------------------------------------------------------------
    #
    @rus.takes   ('Session')
    @rus.returns (str)
    def __str__  (self):
        """String represenation."""

        return "Registered contexts: %s" % (str(self.contexts))


    # ----------------------------------------------------------------
    #
    @rus.takes      ('Session',
                     context.Context)
    @rus.returns    (rus.nothing)
    def add_context (self, ctx) :
        """
        ctx:     saga.Context
        ret:     None

        Add a security L{Context} to the session.
        It is encouraged to use the L{contexts} property instead.
        """

        return self.contexts.insert (0, ctx=ctx, session=self)


    # ----------------------------------------------------------------
    #
    @rus.takes   ('Session',
                  context.Context)
    @rus.returns (rus.nothing)
    def remove_context (self, ctx) :
        """
        ctx:     saga.Context
        ret:     None

        Remove a security L{Context} from the session.
        It is encouraged to use the L{contexts} property instead.
        """

        if ctx in self.contexts :
            self.contexts.remove (ctx)


    # ----------------------------------------------------------------
    #
    @rus.takes   ('Session')
    @rus.returns (rus.list_of (context.Context))
    def list_contexts  (self) :
        """
        ret:     list[saga.Context]

        Retrieve all L{Context} objects attached to the session.
        It is encouraged to use the L{contexts} property instead.
        """

        return self.contexts


# ------------------------------------------------------------------------------
#
class DefaultSession(Session, metaclass=ru.Singleton):

    @rus.takes   ('DefaultSession')
    @rus.returns (rus.nothing)
    def __init__ (self, uid=None):

        # the default session picks up default contexts, from all context
        # adaptors.  To implemented, we have to do some legwork: get the engine,
        # dig through the registered context adaptors, and ask each of them for
        # default contexts.

        super(DefaultSession, self).__init__(default=False, uid=uid)

        _engine = engine.Engine()

        if 'radical.saga.Context' not in _engine._adaptor_registry :
            self._logger.warning ("no context adaptors found")
            return

        for schema   in _engine._adaptor_registry['radical.saga.Context'] :
            for info in _engine._adaptor_registry['radical.saga.Context'][schema] :

                default_ctxs = []

                try :
                    default_ctxs = info['adaptor_instance']._get_default_contexts ()

                except se.SagaException as e :
                    self._logger.debug   ("adaptor %s failed to provide default" \
                                          "contexts: %s" % (info['adaptor_name'], e))
                    continue


                for default_ctx in default_ctxs :

                    try :
                        self.contexts.append (ctx=default_ctx, session=self)
                        self._logger.debug   ("default context [%-20s] : %s" \
                                         %   (info['adaptor_name'], default_ctx))

                    except se.SagaException as e :
                        self._logger.debug   ("skip default context [%-20s] : %s : %s" \
                                         %   (info['adaptor_name'], default_ctx, e))
                        continue


# ------------------------------------------------------------------------------

