"""
    zeep.wsdl.definitions
    ~~~~~~~~~~~~~~~~~~~~~

    A WSDL document exists out of a number of definitions. There are 6 major
    definitions, these are:

     - types
     - message
     - portType
     - binding
     - port
     - service

    This module defines the definitions which occur within a WSDL document,

"""
import warnings
from collections import OrderedDict, namedtuple

from six import python_2_unicode_compatible

from zeep.exceptions import IncompleteOperation

MessagePart = namedtuple('MessagePart', ['element', 'type'])


class AbstractMessage(object):
    """Messages consist of one or more logical parts.

    Each part is associated with a type from some type system using a
    message-typing attribute. The set of message-typing attributes is
    extensible. WSDL defines several such message-typing attributes for use
    with XSD:

        - element: Refers to an XSD element using a QName.
        - type: Refers to an XSD simpleType or complexType using a QName.

    """
    def __init__(self, name):
        self.name = name
        self.parts = OrderedDict()

    def __repr__(self):
        return '<%s(name=%r)>' % (self.__class__.__name__, self.name.text)

    def resolve(self, definitions):
        pass

    def add_part(self, name, element):
        self.parts[name] = element


class AbstractOperation(object):
    """Abstract operations are defined in the wsdl's portType elements."""

    def __init__(self, name, input_message=None, output_message=None,
                 fault_messages=None, parameter_order=None):
        """Initialize the abstract operation.

        :param name: The name of the operation
        :type name: str
        :param input_message: Message to generate the request XML
        :type input_message: AbstractMessage
        :param output_message: Message to process the response XML
        :type output_message: AbstractMessage
        :param fault_messages: Dict of messages to handle faults
        :type fault_messages: dict of str: AbstractMessage

        """
        self.name = name
        self.input_message = input_message
        self.output_message = output_message
        self.fault_messages = fault_messages
        self.parameter_order = parameter_order


class PortType(object):
    def __init__(self, name, operations):
        self.name = name
        self.operations = operations

    def __repr__(self):
        return '<%s(name=%r)>' % (
            self.__class__.__name__, self.name.text)

    def resolve(self, definitions):
        pass


@python_2_unicode_compatible
class Binding(object):
    """Base class for the various bindings (SoapBinding / HttpBinding)

    .. raw:: ascii

        Binding
           |
           +-> Operation
                   |
                   +-> ConcreteMessage
                             |
                             +-> AbstractMessage

    """
    def __init__(self, wsdl, name, port_name):
        """Binding

        :param wsdl:
        :type wsdl:
        :param name:
        :type name: string
        :param port_name:
        :type port_name: string

        """
        self.name = name
        self.port_name = port_name
        self.port_type = None
        self.wsdl = wsdl
        self._operations = {}

    def resolve(self, definitions):
        self.port_type = definitions.get('port_types', self.port_name.text)

        for name, operation in list(self._operations.items()):
            try:
                operation.resolve(definitions)
            except IncompleteOperation as exc:
                warnings.warn(str(exc))
                del self._operations[name]

    def _operation_add(self, operation):
        # XXX: operation name is not unique
        self._operations[operation.name] = operation

    def __str__(self):
        return '%s: %s' % (self.__class__.__name__, self.name.text)

    def __repr__(self):
        return '<%s(name=%r, port_type=%r)>' % (
            self.__class__.__name__, self.name.text, self.port_type)

    def get(self, key):
        try:
            return self._operations[key]
        except KeyError:
            raise ValueError("No such operation %r on %s" % (key, self.name))

    @classmethod
    def match(cls, node):
        raise NotImplementedError()

    @classmethod
    def parse(cls, definitions, xmlelement):
        raise NotImplementedError()


@python_2_unicode_compatible
class Operation(object):
    """Concrete operation

    Contains references to the concrete messages

    """
    def __init__(self, name, binding):
        self.name = name
        self.binding = binding
        self.abstract = None
        self.style = None
        self.input = None
        self.output = None
        self.faults = {}

    def resolve(self, definitions):
        try:
            self.abstract = self.binding.port_type.operations[self.name]
        except KeyError:
            raise IncompleteOperation(
                "The wsdl:operation %r was not found in the wsdl:portType %r" % (
                    self.name, self.binding.port_type.name.text))

    def __repr__(self):
        return '<%s(name=%r, style=%r)>' % (
            self.__class__.__name__, self.name, self.style)

    def __str__(self):
        if not self.input:
            return u'%s(missing input message)' % (self.name)

        retval = u'%s(%s)' % (self.name, self.input.signature())
        if self.output:
            retval += u' -> %s' % (self.output.signature(as_output=True))
        return retval

    def create(self, *args, **kwargs):
        return self.input.serialize(*args, **kwargs)

    def process_reply(self, envelope):
        raise NotImplementedError()

    @classmethod
    def parse(cls, wsdl, xmlelement, binding):
        """

        Definition::

            <wsdl:operation name="nmtoken"> *
               <-- extensibility element (2) --> *
               <wsdl:input name="nmtoken"? > ?
                   <-- extensibility element (3) -->
               </wsdl:input>
               <wsdl:output name="nmtoken"? > ?
                   <-- extensibility element (4) --> *
               </wsdl:output>
               <wsdl:fault name="nmtoken"> *
                   <-- extensibility element (5) --> *
               </wsdl:fault>
            </wsdl:operation>

        """
        raise NotImplementedError()


@python_2_unicode_compatible
class Port(object):
    """Specifies an address for a binding, thus defining a single communication
    endpoint.

    """
    def __init__(self, name, binding_name, xmlelement):
        self.name = name
        self._resolve_context = {
            'binding_name': binding_name,
            'xmlelement': xmlelement,
        }

        # Set during resolve()
        self.binding = None
        self.binding_options = None

    def __repr__(self):
        return '<%s(name=%r, binding=%r, %r)>' % (
            self.__class__.__name__, self.name, self.binding,
            self.binding_options)

    def __str__(self):
        return u'Port: %s (%s)' % (self.name, self.binding)

    def resolve(self, definitions):
        if self._resolve_context is None:
            return

        try:
            self.binding = definitions.get(
                'bindings', self._resolve_context['binding_name'].text)
        except IndexError:
            return False

        if definitions.location:
            force_https = definitions.location.startswith('https')
        else:
            force_https = False

        self.binding_options = self.binding.process_service_port(
            self._resolve_context['xmlelement'],
            force_https)
        self._resolve_context = None
        return True


@python_2_unicode_compatible
class Service(object):
    """Used to aggregate a set of related ports.

    """
    def __init__(self, name):
        self.ports = OrderedDict()
        self.name = name
        self._is_resolved = False

    def __str__(self):
        return u'Service: %s' % self.name

    def __repr__(self):
        return '<%s(name=%r, ports=%r)>' % (
            self.__class__.__name__, self.name, self.ports)

    def resolve(self, definitions):
        if self._is_resolved:
            return

        unresolved = []
        for name, port in self.ports.items():
            is_resolved = port.resolve(definitions)
            if not is_resolved:
                unresolved.append(name)

        # Remove unresolved bindings (http etc)
        for name in unresolved:
            del self.ports[name]

        self._is_resolved = True

    def add_port(self, port):
        self.ports[port.name] = port
