###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Tavendo GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

from __future__ import absolute_import

import six

__all__ = (
    'ComponentConfig',
    'HelloReturn',
    'Accept',
    'Deny',
    'Challenge',
    'HelloDetails',
    'SessionDetails',
    'CloseDetails',
    'SubscribeOptions',
    'EventDetails',
    'PublishOptions',
    'RegisterOptions',
    'CallDetails',
    'CallOptions',
    'CallResult',
)


class ComponentConfig(object):
    """
    WAMP application component configuration. An instance of this class is
    provided to the constructor of :class:`autobahn.wamp.protocol.ApplicationSession`.
    """

    def __init__(self, realm=None, extra=None):
        """
        :param realm: The realm the session should join.
        :type realm: unicode

        :param extra: Optional user-supplied object with extra
            configuration. This can be any object you like, and is
            accessible in your `ApplicationSession` subclass via
            `self.config.extra`. `dict` is a good default choice.
        """
        realm = realm or u'default'
        # FIXME
        if six.PY2 and type(realm) == str:
            realm = six.u(realm)
        if type(realm) != six.text_type:
            raise RuntimeError('"realm" must be of type Unicode - was {0}'.format(type(realm)))
        self.realm = realm
        self.extra = extra

    def __str__(self):
        return "ComponentConfig(realm = {0}, extra = {1})".format(self.realm, self.extra)


class HelloReturn(object):
    """
    Base class for ``HELLO`` return information.
    """


class Accept(HelloReturn):
    """
    Information to accept a ``HELLO``.
    """

    def __init__(self, authid=None, authrole=None, authmethod=None, authprovider=None):
        """

        :param authid: The authentication ID the client is assigned, e.g. ``"joe"`` or ``"joe@example.com"``.
        :type authid: unicode
        :param authrole: The authentication role the client is assigned, e.g. ``"anonymous"``, ``"user"`` or ``"com.myapp.user"``.
        :type authrole: unicode
        :param authmethod: The authentication method that was used to authenticate the client, e.g. ``"cookie"`` or ``"wampcra"``.
        :type authmethod: unicode
        :param authprovider: The authentication provider that was used to authenticate the client, e.g. ``"mozilla-persona"``.
        :type authprovider: unicode
        """
        if six.PY2:
            if type(authid) == str:
                authid = six.u(authid)
            if type(authrole) == str:
                authrole = six.u(authrole)
            if type(authmethod) == str:
                authmethod = six.u(authmethod)
            if type(authprovider) == str:
                authprovider = six.u(authprovider)

        assert(authid is None or type(authid) == six.text_type)
        assert(authrole is None or type(authrole) == six.text_type)
        assert(authmethod is None or type(authmethod) == six.text_type)
        assert(authprovider is None or type(authprovider) == six.text_type)

        self.authid = authid
        self.authrole = authrole
        self.authmethod = authmethod
        self.authprovider = authprovider

    def __str__(self):
        return "Accept(authid = {0}, authrole = {1}, authmethod = {2}, authprovider = {3})".format(self.authid, self.authrole, self.authmethod, self.authprovider)


class Deny(HelloReturn):
    """
    Information to deny a ``HELLO``.
    """

    def __init__(self, reason=u"wamp.error.not_authorized", message=None):
        """

        :param reason: The reason of denying the authentication (an URI, e.g. ``wamp.error.not_authorized``)
        :type reason: unicode
        :param message: A human readable message (for logging purposes).
        :type message: unicode
        """
        if six.PY2:
            if type(reason) == str:
                reason = six.u(reason)
            if type(message) == str:
                message = six.u(message)

        assert(type(reason) == six.text_type)
        assert(message is None or type(message) == six.text_type)

        self.reason = reason
        self.message = message

    def __str__(self):
        return "Deny(reason = {0}, message = '{1}')".format(self.reason, self.message)


class Challenge(HelloReturn):
    """
    Information to challenge the client upon ``HELLO``.
    """

    def __init__(self, method, extra=None):
        """

        :param method: The authentication method for the challenge (e.g. ``"wampcra"``).
        :type method: unicode

        :param extra: Any extra information for the authentication challenge. This is
           specific to the authentication method.
        :type extra: dict
        """
        if six.PY2:
            if type(method) == str:
                method = six.u(method)

        self.method = method
        self.extra = extra or {}

    def __str__(self):
        return "Challenge(method = {0}, extra = {1})".format(self.method, self.extra)


class HelloDetails(object):
    """
    Provides details of a WAMP session while still attaching.
    """

    def __init__(self, roles=None, authmethods=None, authid=None, pending_session=None):
        """

        :param roles: The WAMP roles and features supported by the attaching client.
        :type roles: dict
        :param authmethods: The authentication methods the client is willing to perform.
        :type authmethods: list
        :param authid: The authentication ID the client wants to authenticate as. Required for WAMP-CRA.
        :type authid: str
        :param pending_session: The session ID the session will get once successfully attached.
        :type pending_session: int
        """
        self.roles = roles
        self.authmethods = authmethods
        self.authid = authid
        self.pending_session = pending_session

    def __str__(self):
        return "HelloDetails(roles = {0}, authmethods = {1}, authid = {2}, pending_session = {3})".format(self.roles, self.authmethods, self.authid, self.pending_session)


class SessionDetails(object):
    """
    Provides details for a WAMP session upon open.

    .. seealso:: :func:`autobahn.wamp.interfaces.ISession.onJoin`
    """

    def __init__(self, realm, session, authid=None, authrole=None, authmethod=None, authprovider=None):
        """
        Ctor.

        :param realm: The realm this WAMP session is attached to.
        :type realm: unicode
        :param session: WAMP session ID of this session.
        :type session: int
        """
        self.realm = realm
        self.session = session
        self.authid = authid
        self.authrole = authrole
        self.authmethod = authmethod
        self.authprovider = authprovider

    def __str__(self):
        return "SessionDetails(realm = {0}, session = {1}, authid = {2}, authrole = {3}, authmethod = {4})".format(self.realm, self.session, self.authid, self.authrole, self.authmethod)


class CloseDetails(object):
    """
    Provides details for a WAMP session upon open.

    .. seealso:: :func:`autobahn.wamp.interfaces.ISession.onLeave`
    """
    REASON_DEFAULT = u"wamp.close.normal"
    REASON_TRANSPORT_LOST = u"wamp.close.transport_lost"

    def __init__(self, reason=None, message=None):
        """

        :param reason: The close reason (an URI, e.g. ``wamp.close.normal``)
        :type reason: unicode
        :param message: Closing log message.
        :type message: unicode
        """
        self.reason = reason
        self.message = message

    def __str__(self):
        return "CloseDetails(reason = {0}, message = '{1}'')".format(self.reason, self.message)


class SubscribeOptions(object):
    """
    Used to provide options for subscribing in
    :func:`autobahn.wamp.interfaces.ISubscriber.subscribe`.
    """

    def __init__(self, match=None, details_arg=None):
        """
        :param match: The topic matching method to be used for the subscription.
        :type match: unicode
        :param details_arg: When invoking the handler, provide event details
          in this keyword argument to the callable.
        :type details_arg: str
        """
        assert(match is None or (type(match) == six.text_type and match in [u'exact', u'prefix', u'wildcard']))
        assert(details_arg is None or type(details_arg) == str)

        self.match = match
        self.details_arg = details_arg

    def message_attr(self):
        # options dict as sent within WAMP message
        return {
            'match': self.match
        }

    def __str__(self):
        return "SubscribeOptions(match = {0}, details_arg = {1})".format(self.match, self.details_arg)


class EventDetails(object):
    """
    Provides details on an event when calling an event handler
    previously registered.
    """
    def __init__(self, publication, publisher=None, topic=None):
        """
        Ctor.

        :param publication: The publication ID of the event (always present).
        :type publication: int
        :param publisher: The WAMP session ID of the original publisher of this event.
        :type publisher: int
        :param topic: For pattern-based subscriptions, the actual topic URI being published to.
        :type topic1: unicode or None
        """
        self.publication = publication
        self.publisher = publisher
        self.topic = topic

    def __str__(self):
        return "EventDetails(publication = {0}, publisher = {1}, topic = {2})".format(self.publication, self.publisher, self.topic)


class PublishOptions(object):
    """
    Used to provide options for subscribing in
    :func:`autobahn.wamp.interfaces.IPublisher.publish`.
    """

    def __init__(self,
                 acknowledge=None,
                 exclude_me=None,
                 exclude=None,
                 eligible=None,
                 disclose_me=None):
        """

        :param acknowledge: If ``True``, acknowledge the publication with a success or
           error response.
        :type acknowledge: bool
        :param exclude_me: If ``True``, exclude the publisher from receiving the event, even
           if he is subscribed (and eligible).
        :type exclude_me: bool
        :param exclude: List of WAMP session IDs to exclude from receiving this event.
        :type exclude: list of int
        :param eligible: List of WAMP session IDs eligible to receive this event.
        :type eligible: list of int
        :param disclose_me: If ``True``, request to disclose the publisher of this event
           to subscribers.
        :type disclose_me: bool
        """
        # filter out None entries from exclude list, so it's easier for callers
        if type(exclude) == list:
            exclude = [x for x in exclude if x is not None]
        assert(acknowledge is None or type(acknowledge) == bool)
        assert(exclude_me is None or type(exclude_me) == bool)
        assert(exclude is None or (type(exclude) == list and all(type(x) in six.integer_types for x in exclude)))
        assert(eligible is None or (type(eligible) == list and all(type(x) in six.integer_types for x in eligible)))
        assert(disclose_me is None or type(disclose_me) == bool)

        self.acknowledge = acknowledge
        self.exclude_me = exclude_me
        self.exclude = exclude
        self.eligible = eligible
        self.disclose_me = disclose_me

    def message_attr(self):
        # options dict as sent within WAMP message
        return {
            u'acknowledge': self.acknowledge,
            u'exclude_me': self.exclude_me,
            u'exclude': self.exclude,
            u'eligible': self.eligible,
            u'disclose_me': self.disclose_me
        }

    def __str__(self):
        return "PublishOptions(acknowledge = {0}, exclude_me = {1}, exclude = {2}, eligible = {3}, disclose_me = {4})".format(self.acknowledge, self.exclude_me, self.exclude, self.eligible, self.disclose_me)


class RegisterOptions(object):
    """
    Used to provide options for registering in
    :func:`autobahn.wamp.interfaces.ICallee.register`.
    """

    def __init__(self, match=None, invoke=None, details_arg=None):
        """

        :param details_arg: When invoking the endpoint, provide call details
           in this keyword argument to the callable.
        :type details_arg: str
        """
        assert(match is None or (type(match) == six.text_type and match in [u'exact', u'prefix', u'wildcard']))
        assert(invoke is None or (type(invoke) == six.text_type and invoke in [u'single', u'first', u'last', u'roundrobin', u'random']))
        assert(details_arg is None or type(details_arg) == str)

        self.match = match
        self.invoke = invoke
        self.details_arg = details_arg

    def message_attr(self):
        # options dict as sent within WAMP message
        return {
            u'match': self.match,
            u'invoke': self.invoke
        }

    def __str__(self):
        return "RegisterOptions(match = {0}, invoke = {1}, details_arg = {2})".format(self.match, self.invoke, self.details_arg)


class CallDetails(object):
    """
    Provides details on a call when an endpoint previously
    registered is being called and opted to receive call details.
    """

    def __init__(self, progress=None, caller=None, procedure=None):
        """
        Ctor.

        :param progress: A callable that will receive progressive call results.
        :type progress: callable
        :param caller: The WAMP session ID of the caller, if the latter is disclosed.
        :type caller: int
        :param procedure: For pattern-based registrations, the actual procedure URI being called.
        :type procedure: unicode or None
        """
        self.progress = progress
        self.caller = caller
        self.procedure = procedure

    def __str__(self):
        return "CallDetails(progress = {0}, caller = {1}, procedure = {2})".format(self.progress, self.caller, self.procedure)


class CallOptions(object):
    """
    Used to provide options for calling with :func:`autobahn.wamp.interfaces.ICaller.call`.
    """

    def __init__(self,
                 on_progress=None,
                 timeout=None,
                 disclose_me=None):
        """

        :param on_progress: A callback that will be called when the remote endpoint
           called yields interim call progress results.
        :type on_progress: callable
        :param timeout: Time in seconds after which the call should be automatically canceled.
        :type timeout: float
        :param disclose_me: Request to disclose the identity of the caller (it's WAMP session ID)
           to Callees. Note that a Dealer, depending on Dealer configuration, might
           reject the request, or might disclose the Callee's identity without
           a request to do so.
        :type disclose_me: bool
        """
        assert(on_progress is None or callable(on_progress))
        assert(timeout is None or (type(timeout) in list(six.integer_types) + [float] and timeout > 0))
        assert(disclose_me is None or type(disclose_me) == bool)

        self.on_progress = on_progress
        self.timeout = timeout
        self.disclose_me = disclose_me

    def message_attr(self):
        # options dict as sent within WAMP message
        res = {
            u'timeout': self.timeout,
            u'disclose_me': self.disclose_me
        }
        if self.on_progress:
            res['receive_progress'] = True
        return res

    def __str__(self):
        return "CallOptions(on_progress = {0}, timeout = {1}, disclose_me = {2})".format(self.on_progress, self.timeout, self.disclose_me)


class CallResult(object):
    """
    Wrapper for remote procedure call results that contain multiple positional
    return values or keyword return values.
    """

    def __init__(self, *results, **kwresults):
        """
        Constructor.

        :param results: The positional result values.
        :type results: list
        :param kwresults: The keyword result values.
        :type kwresults: dict
        """
        self.results = results
        self.kwresults = kwresults

    def __str__(self):
        return "CallResult(results = {0}, kwresults = {1})".format(self.results, self.kwresults)


class IPublication(object):
    """
    Represents a publication of an event. This is used with acknowledged publications.
    """

    def id(self):
        """
        The WAMP publication ID for this publication.
        """


class ISubscription(object):
    """
    Represents a subscription to a topic.
    """

    def id(self):
        """
        The WAMP subscription ID for this subscription.
        """

    def active(self):
        """
        Flag indicating if subscription is active.
        """

    def unsubscribe(self):
        """
        Unsubscribe this subscription that was previously created from
        :func:`autobahn.wamp.interfaces.ISubscriber.subscribe`.

        After a subscription has been unsubscribed successfully, no events
        will be routed to the event handler anymore.

        Returns an instance of :tx:`twisted.internet.defer.Deferred` (when
        running on **Twisted**) or an instance of :py:class:`asyncio.Future`
        (when running on **asyncio**).

        - If the unsubscription succeeds, the returned Deferred/Future will
          *resolve* (with no return value).

        - If the unsubscription fails, the returned Deferred/Future will *reject*
          with an instance of :class:`autobahn.wamp.exception.ApplicationError`.

        :returns: A Deferred/Future for the unsubscription
        :rtype: instance(s) of :tx:`twisted.internet.defer.Deferred` / :py:class:`asyncio.Future`
        """


class IRegistration(object):
    """
    Represents a registration of an endpoint.
    """

    def id(self):
        """
        The WAMP registration ID for this registration.
        """

    def active(self):
        """
        Flag indicating if registration is active.
        """

    def unregister(self):
        """
        Unregister this registration that was previously created from
        :func:`autobahn.wamp.interfaces.ICallee.register`.

        After a registration has been unregistered successfully, no calls
        will be routed to the endpoint anymore.

        Returns an instance of :tx:`twisted.internet.defer.Deferred` (when
        running on **Twisted**) or an instance of :py:class:`asyncio.Future`
        (when running on **asyncio**).

        - If the unregistration succeeds, the returned Deferred/Future will
          *resolve* (with no return value).

        - If the unregistration fails, the returned Deferred/Future will be rejected
          with an instance of :class:`autobahn.wamp.exception.ApplicationError`.

        :returns: A Deferred/Future for the unregistration
        :rtype: instance(s) of :tx:`twisted.internet.defer.Deferred` / :py:class:`asyncio.Future`
        """
