import keyword
import logging
import re

from lxml import etree

from zeep.exceptions import XMLParseError
from zeep.loader import absolute_location, load_external
from zeep.utils import as_qname, qname_attr
from zeep.xsd import elements as xsd_elements
from zeep.xsd import types as xsd_types
from zeep.xsd.const import xsd_ns
from zeep.xsd.types.unresolved import UnresolvedCustomType, UnresolvedType

logger = logging.getLogger(__name__)


class tags(object):
    pass


for name in [
    'schema', 'import', 'include',
    'annotation', 'element', 'simpleType', 'complexType',
    'simpleContent', 'complexContent',
    'sequence', 'group', 'choice', 'all', 'list', 'union',
    'attribute', 'any', 'anyAttribute', 'attributeGroup',
    'restriction', 'extension', 'notation',

]:
    attr = name if name not in keyword.kwlist else name + '_'
    setattr(tags, attr, xsd_ns(name))


class SchemaVisitor(object):
    """Visitor which processes XSD files and registers global elements and
    types in the given schema.

    :param schema:
    :type schema: zeep.xsd.schema.Schema
    :param document:
    :type document: zeep.xsd.schema.SchemaDocument

    """

    def __init__(self, schema, document):
        self.document = document
        self.schema = schema
        self._includes = set()

    def register_element(self, qname, instance):
        self.document.register_element(qname, instance)

    def register_attribute(self, name, instance):
        self.document.register_attribute(name, instance)

    def register_type(self, qname, instance):
        self.document.register_type(qname, instance)

    def register_group(self, qname, instance):
        self.document.register_group(qname, instance)

    def register_attribute_group(self, qname, instance):
        self.document.register_attribute_group(qname, instance)

    def register_import(self, namespace, document):
        self.document.register_import(namespace, document)

    def process(self, node, parent):
        visit_func = self.visitors.get(node.tag)
        if not visit_func:
            raise ValueError("No visitor defined for %r" % node.tag)
        result = visit_func(self, node, parent)
        return result

    def process_ref_attribute(self, node, array_type=None):
        ref = qname_attr(node, 'ref')
        if ref:
            ref = self._create_qname(ref)

            # Some wsdl's reference to xs:schema, we ignore that for now. It
            # might be better in the future to process the actual schema file
            # so that it is handled correctly
            if ref.namespace == 'http://www.w3.org/2001/XMLSchema':
                return
            return xsd_elements.RefAttribute(
                node.tag, ref, self.schema, array_type=array_type)

    def process_reference(self, node, **kwargs):
        ref = qname_attr(node, 'ref')
        if not ref:
            return

        if node.tag == tags.element:
            cls = xsd_elements.RefElement
        elif node.tag == tags.attribute:
            cls = xsd_elements.RefAttribute
        elif node.tag == tags.group:
            cls = xsd_elements.RefGroup
        elif node.tag == tags.attributeGroup:
            cls = xsd_elements.RefAttributeGroup
        return cls(node.tag, ref, self.schema, **kwargs)

    def visit_schema(self, node):
        """Visit the xsd:schema element and process all the child elements

        Definition::

            <schema
              attributeFormDefault = (qualified | unqualified): unqualified
              blockDefault = (#all | List of (extension | restriction | substitution) : ''
              elementFormDefault = (qualified | unqualified): unqualified
              finalDefault = (#all | List of (extension | restriction | list | union): ''
              id = ID
              targetNamespace = anyURI
              version = token
              xml:lang = language
              {any attributes with non-schema Namespace}...>
            Content: (
                (include | import | redefine | annotation)*,
                (((simpleType | complexType | group | attributeGroup) |
                  element | attribute | notation),
                 annotation*)*)
            </schema>

        :param node: The XML node
        :type node: lxml.etree._Element

        """
        assert node is not None

        self.document._target_namespace = node.get('targetNamespace')
        self.document._element_form = node.get('elementFormDefault', 'unqualified')
        self.document._attribute_form = node.get('attributeFormDefault', 'unqualified')

        for child in node:
            self.process(child, parent=node)

    def visit_import(self, node, parent):
        """

        Definition::

            <import
              id = ID
              namespace = anyURI
              schemaLocation = anyURI
              {any attributes with non-schema Namespace}...>
            Content: (annotation?)
            </import>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        schema_node = None
        namespace = node.get('namespace')
        location = node.get('schemaLocation')
        if location:
            location = absolute_location(location, self.document._base_url)

        if not namespace and not self.document._target_namespace:
            raise XMLParseError(
                "The attribute 'namespace' must be existent if the "
                "importing schema has no target namespace.",
                filename=self._document.location,
                sourceline=node.sourceline)

        # Check if the schema is already imported before based on the
        # namespace. Schema's without namespace are registered as 'None'
        document = self.schema._get_schema_document(namespace, location)
        if document:
            logger.debug("Returning existing schema: %r", location)
            self.register_import(namespace, document)
            return document

        # Hardcode the mapping between the xml namespace and the xsd for now.
        # This seems to fix issues with exchange wsdl's, see #220
        if not location and namespace == 'http://www.w3.org/XML/1998/namespace':
            location = 'https://www.w3.org/2001/xml.xsd'

        # Silently ignore import statements which we can't resolve via the
        # namespace and doesn't have a schemaLocation attribute.
        if not location:
            logger.debug(
                "Ignoring import statement for namespace %r " +
                "(missing schemaLocation)", namespace)
            return

        # Load the XML
        schema_node = load_external(
            location,
            self.schema._transport,
            strict=self.schema.strict)

        # Check if the xsd:import namespace matches the targetNamespace. If
        # the xsd:import statement didn't specify a namespace then make sure
        # that the targetNamespace wasn't declared by another schema yet.
        schema_tns = schema_node.get('targetNamespace')
        if namespace and schema_tns and namespace != schema_tns:
            raise XMLParseError((
                "The namespace defined on the xsd:import doesn't match the "
                "imported targetNamespace located at %r "
                ) % (location),
                filename=self.document._location,
                sourceline=node.sourceline)

        schema = self.schema.create_new_document(schema_node, location)
        self.register_import(namespace, schema)
        return schema

    def visit_include(self, node, parent):
        """

        Definition::

            <include
              id = ID
              schemaLocation = anyURI
              {any attributes with non-schema Namespace}...>
            Content: (annotation?)
            </include>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        if not node.get('schemaLocation'):
            raise NotImplementedError("schemaLocation is required")
        location = node.get('schemaLocation')

        if location in self._includes:
            return

        schema_node = load_external(
            location, self.schema._transport,
            base_url=self.document._base_url,
            strict=self.schema.strict)
        self._includes.add(location)

        # When the included document has no default namespace defined but the
        # parent document does have this then we should (atleast for #360)
        # transfer the default namespace to the included schema. We can't
        # update the nsmap of elements in lxml so we create a new schema with
        # the correct nsmap and move all the content there.
        if not schema_node.nsmap.get(None) and node.nsmap.get(None):
            nsmap = {None: node.nsmap[None]}
            nsmap.update(schema_node.nsmap)
            new = etree.Element(schema_node.tag, nsmap=nsmap)
            for child in schema_node:
                new.append(child)
            for key, value in schema_node.attrib.items():
                new.set(key, value)
            schema_node = new

        # Use the element/attribute form defaults from the schema while
        # processing the nodes.
        element_form_default = self.document._element_form
        attribute_form_default = self.document._attribute_form
        base_url = self.document._base_url

        self.document._element_form = schema_node.get('elementFormDefault', 'unqualified')
        self.document._attribute_form = schema_node.get('attributeFormDefault', 'unqualified')
        self.document._base_url = absolute_location(location, self.document._base_url)

        # Iterate directly over the children.
        for child in schema_node:
            self.process(child, parent=schema_node)

        self.document._element_form = element_form_default
        self.document._attribute_form = attribute_form_default
        self.document._base_url = base_url

    def visit_element(self, node, parent):
        """

        Definition::

            <element
              abstract = Boolean : false
              block = (#all | List of (extension | restriction | substitution))
              default = string
              final = (#all | List of (extension | restriction))
              fixed = string
              form = (qualified | unqualified)
              id = ID
              maxOccurs = (nonNegativeInteger | unbounded) : 1
              minOccurs = nonNegativeInteger : 1
              name = NCName
              nillable = Boolean : false
              ref = QName
              substitutionGroup = QName
              type = QName
              {any attributes with non-schema Namespace}...>
            Content: (annotation?, (
                      (simpleType | complexType)?, (unique | key | keyref)*))
            </element>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        is_global = parent.tag == tags.schema

        # minOccurs / maxOccurs are not allowed on global elements
        if not is_global:
            min_occurs, max_occurs = _process_occurs_attrs(node)
        else:
            max_occurs = 1
            min_occurs = 1

        # If the element has a ref attribute then all other attributes cannot
        # be present. Short circuit that here.
        # Ref is prohibited on global elements (parent = schema)
        if not is_global:
            result = self.process_reference(
                node, min_occurs=min_occurs, max_occurs=max_occurs)
            if result:
                return result

        element_form = node.get('form', self.document._element_form)
        if element_form == 'qualified' or is_global:
            qname = qname_attr(node, 'name', self.document._target_namespace)
        else:
            qname = etree.QName(node.get('name'))

        children = node.getchildren()
        xsd_type = None
        if children:
            value = None

            for child in children:
                if child.tag == tags.annotation:
                    continue

                elif child.tag in (tags.simpleType, tags.complexType):
                    assert not value

                    xsd_type = self.process(child, node)

        if not xsd_type:
            node_type = qname_attr(node, 'type')
            if node_type:
                xsd_type = self._get_type(node_type.text)
            else:
                xsd_type = xsd_types.AnyType()

        # Naive workaround to mark fields which are part of a choice element
        # as optional
        if parent.tag == tags.choice:
            min_occurs = 0

        nillable = node.get('nillable') == 'true'
        default = node.get('default')
        element = xsd_elements.Element(
            name=qname, type_=xsd_type,
            min_occurs=min_occurs, max_occurs=max_occurs, nillable=nillable,
            default=default, is_global=is_global)

        # Only register global elements
        if is_global:
            self.register_element(qname, element)
        return element

    def visit_attribute(self, node, parent):
        """Declares an attribute.

        Definition::

            <attribute
              default = string
              fixed = string
              form = (qualified | unqualified)
              id = ID
              name = NCName
              ref = QName
              type = QName
              use = (optional | prohibited | required): optional
              {any attributes with non-schema Namespace...}>
            Content: (annotation?, (simpleType?))
            </attribute>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        is_global = parent.tag == tags.schema

        # Check of wsdl:arayType
        array_type = node.get('{http://schemas.xmlsoap.org/wsdl/}arrayType')
        if array_type:
            match = re.match('([^\[]+)', array_type)
            if match:
                array_type = match.groups()[0]
                qname = as_qname(array_type, node.nsmap)
                array_type = UnresolvedType(qname, self.schema)

        # If the elment has a ref attribute then all other attributes cannot
        # be present. Short circuit that here.
        # Ref is prohibited on global elements (parent = schema)
        if not is_global:
            result = self.process_ref_attribute(node, array_type=array_type)
            if result:
                return result

        attribute_form = node.get('form', self.document._attribute_form)
        if attribute_form == 'qualified' or is_global:
            name = qname_attr(node, 'name', self.document._target_namespace)
        else:
            name = etree.QName(node.get('name'))

        annotation, items = self._pop_annotation(node.getchildren())
        if items:
            xsd_type = self.visit_simple_type(items[0], node)
        else:
            node_type = qname_attr(node, 'type')
            if node_type:
                xsd_type = self._get_type(node_type)
            else:
                xsd_type = xsd_types.AnyType()

        # TODO: We ignore 'prohobited' for now
        required = node.get('use') == 'required'
        default = node.get('default')

        attr = xsd_elements.Attribute(
            name, type_=xsd_type, default=default, required=required)

        # Only register global elements
        if is_global:
            self.register_attribute(name, attr)
        return attr

    def visit_simple_type(self, node, parent):
        """
        Definition::

            <simpleType
              final = (#all | (list | union | restriction))
              id = ID
              name = NCName
              {any attributes with non-schema Namespace}...>
            Content: (annotation?, (restriction | list | union))
            </simpleType>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """

        if parent.tag == tags.schema:
            name = node.get('name')
            is_global = True
        else:
            name = parent.get('name', 'Anonymous')
            is_global = False
        base_type = '{http://www.w3.org/2001/XMLSchema}string'
        qname = as_qname(name, node.nsmap, self.document._target_namespace)

        annotation, items = self._pop_annotation(node.getchildren())
        child = items[0]
        if child.tag == tags.restriction:
            base_type = self.visit_restriction_simple_type(child, node)
            xsd_type = UnresolvedCustomType(qname, base_type, self.schema)

        elif child.tag == tags.list:
            xsd_type = self.visit_list(child, node)

        elif child.tag == tags.union:
            xsd_type = self.visit_union(child, node)
        else:
            raise AssertionError("Unexpected child: %r" % child.tag)

        assert xsd_type is not None
        if is_global:
            self.register_type(qname, xsd_type)
        return xsd_type

    def visit_complex_type(self, node, parent):
        """
        Definition::

            <complexType
              abstract = Boolean : false
              block = (#all | List of (extension | restriction))
              final = (#all | List of (extension | restriction))
              id = ID
              mixed = Boolean : false
              name = NCName
              {any attributes with non-schema Namespace...}>
            Content: (annotation?, (simpleContent | complexContent |
                      ((group | all | choice | sequence)?,
                      ((attribute | attributeGroup)*, anyAttribute?))))
            </complexType>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        children = []
        base_type = '{http://www.w3.org/2001/XMLSchema}anyType'

        # If the complexType's parent is an element then this type is
        # anonymous and should have no name defined. Otherwise it's global
        if parent.tag == tags.schema:
            name = node.get('name')
            is_global = True
        else:
            name = parent.get('name')
            is_global = False

        qname = as_qname(name, node.nsmap, self.document._target_namespace)
        cls_attributes = {
            '__module__': 'zeep.xsd.dynamic_types',
            '_xsd_name': qname,
        }
        xsd_cls = type(name, (xsd_types.ComplexType,), cls_attributes)
        xsd_type = None

        # Process content
        annotation, children = self._pop_annotation(node.getchildren())
        first_tag = children[0].tag if children else None

        if first_tag == tags.simpleContent:
            base_type, attributes = self.visit_simple_content(children[0], node)

            xsd_type = xsd_cls(
                attributes=attributes, extension=base_type, qname=qname,
                is_global=is_global)

        elif first_tag == tags.complexContent:
            kwargs = self.visit_complex_content(children[0], node)
            xsd_type = xsd_cls(qname=qname, is_global=is_global, **kwargs)

        elif first_tag:
            element = None

            if first_tag in (tags.group, tags.all, tags.choice, tags.sequence):
                child = children.pop(0)
                element = self.process(child, node)

            attributes = self._process_attributes(node, children)
            xsd_type = xsd_cls(
                element=element, attributes=attributes, qname=qname,
                is_global=is_global)
        else:
            xsd_type = xsd_cls(qname=qname, is_global=is_global)

        if is_global:
            self.register_type(qname, xsd_type)
        return xsd_type

    def visit_complex_content(self, node, parent):
        """The complexContent element defines extensions or restrictions on a
        complex type that contains mixed content or elements only.

        Definition::

            <complexContent
              id = ID
              mixed = Boolean
              {any attributes with non-schema Namespace}...>
            Content: (annotation?,  (restriction | extension))
            </complexContent>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """

        child = node.getchildren()[-1]

        if child.tag == tags.restriction:
            base, element, attributes = self.visit_restriction_complex_content(
                child, node)
            return {
                'attributes': attributes,
                'element': element,
                'restriction': base,
            }
        elif child.tag == tags.extension:
            base, element, attributes = self.visit_extension_complex_content(
                child, node)
            return {
                'attributes': attributes,
                'element': element,
                'extension': base,
            }

    def visit_simple_content(self, node, parent):
        """Contains extensions or restrictions on a complexType element with
        character data or a simpleType element as content and contains no
        elements.

        Definition::

            <simpleContent
              id = ID
              {any attributes with non-schema Namespace}...>
            Content: (annotation?, (restriction | extension))
            </simpleContent>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """

        child = node.getchildren()[-1]

        if child.tag == tags.restriction:
            return self.visit_restriction_simple_content(child, node)
        elif child.tag == tags.extension:
            return self.visit_extension_simple_content(child, node)
        raise AssertionError("Expected restriction or extension")

    def visit_restriction_simple_type(self, node, parent):
        """
        Definition::

            <restriction
              base = QName
              id = ID
              {any attributes with non-schema Namespace}...>
            Content: (annotation?,
                (simpleType?, (
                    minExclusive | minInclusive | maxExclusive | maxInclusive |
                    totalDigits |fractionDigits | length | minLength |
                    maxLength | enumeration | whiteSpace | pattern)*))
            </restriction>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        base_name = qname_attr(node, 'base')
        if base_name:
            return self._get_type(base_name)

        annotation, children = self._pop_annotation(node.getchildren())
        if children[0].tag == tags.simpleType:
            return self.visit_simple_type(children[0], node)

    def visit_restriction_simple_content(self, node, parent):
        """
        Definition::

            <restriction
              base = QName
              id = ID
              {any attributes with non-schema Namespace}...>
            Content: (annotation?,
                (simpleType?, (
                    minExclusive | minInclusive | maxExclusive | maxInclusive |
                    totalDigits |fractionDigits | length | minLength |
                    maxLength | enumeration | whiteSpace | pattern)*
                )?, ((attribute | attributeGroup)*, anyAttribute?))
            </restriction>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        base_name = qname_attr(node, 'base')
        base_type = self._get_type(base_name)
        return base_type, []

    def visit_restriction_complex_content(self, node, parent):
        """

        Definition::

            <restriction
              base = QName
              id = ID
              {any attributes with non-schema Namespace}...>
            Content: (annotation?, (group | all | choice | sequence)?,
                    ((attribute | attributeGroup)*, anyAttribute?))
            </restriction>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        base_name = qname_attr(node, 'base')
        base_type = self._get_type(base_name)
        annotation, children = self._pop_annotation(node.getchildren())

        element = None
        attributes = []

        if children:
            child = children[0]
            if child.tag in (tags.group, tags.all, tags.choice, tags.sequence):
                children.pop(0)
                element = self.process(child, node)
            attributes = self._process_attributes(node, children)
        return base_type, element, attributes

    def visit_extension_complex_content(self, node, parent):
        """

        Definition::

            <extension
              base = QName
              id = ID
              {any attributes with non-schema Namespace}...>
            Content: (annotation?, (
                        (group | all | choice | sequence)?,
                        ((attribute | attributeGroup)*, anyAttribute?)))
            </extension>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        base_name = qname_attr(node, 'base')
        base_type = self._get_type(base_name)
        annotation, children = self._pop_annotation(node.getchildren())

        element = None
        attributes = []

        if children:
            child = children[0]
            if child.tag in (tags.group, tags.all, tags.choice, tags.sequence):
                children.pop(0)
                element = self.process(child, node)
            attributes = self._process_attributes(node, children)

        return base_type, element, attributes

    def visit_extension_simple_content(self, node, parent):
        """

        Definition::

            <extension
              base = QName
              id = ID
              {any attributes with non-schema Namespace}...>
            Content: (annotation?, ((attribute | attributeGroup)*, anyAttribute?))
            </extension>
        """
        base_name = qname_attr(node, 'base')
        base_type = self._get_type(base_name)
        annotation, children = self._pop_annotation(node.getchildren())
        attributes = self._process_attributes(node, children)

        return base_type, attributes

    def visit_annotation(self, node, parent):
        """Defines an annotation.

        Definition::

            <annotation
              id = ID
              {any attributes with non-schema Namespace}...>
            Content: (appinfo | documentation)*
            </annotation>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        return

    def visit_any(self, node, parent):
        """

        Definition::

            <any
              id = ID
              maxOccurs = (nonNegativeInteger | unbounded) : 1
              minOccurs = nonNegativeInteger : 1
              namespace = "(##any | ##other) |
                List of (anyURI | (##targetNamespace |  ##local))) : ##any
              processContents = (lax | skip | strict) : strict
              {any attributes with non-schema Namespace...}>
            Content: (annotation?)
            </any>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        min_occurs, max_occurs = _process_occurs_attrs(node)
        process_contents = node.get('processContents', 'strict')
        return xsd_elements.Any(
            max_occurs=max_occurs, min_occurs=min_occurs,
            process_contents=process_contents)

    def visit_sequence(self, node, parent):
        """
        Definition::

            <sequence
              id = ID
              maxOccurs = (nonNegativeInteger | unbounded) : 1
              minOccurs = nonNegativeInteger : 1
              {any attributes with non-schema Namespace}...>
            Content: (annotation?,
                      (element | group | choice | sequence | any)*)
            </sequence>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """

        sub_types = [
            tags.annotation, tags.any, tags.choice, tags.element,
            tags.group, tags.sequence
        ]
        min_occurs, max_occurs = _process_occurs_attrs(node)
        result = xsd_elements.Sequence(
            min_occurs=min_occurs, max_occurs=max_occurs)

        annotation, items = self._pop_annotation(node.getchildren())
        for child in items:
            assert child.tag in sub_types, child
            item = self.process(child, node)
            assert item is not None
            result.append(item)

        assert None not in result
        return result

    def visit_all(self, node, parent):
        """Allows the elements in the group to appear (or not appear) in any
        order in the containing element.

        Definition::

            <all
              id = ID
              maxOccurs= 1: 1
              minOccurs= (0 | 1): 1
              {any attributes with non-schema Namespace...}>
            Content: (annotation?, element*)
            </all>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """

        sub_types = [
            tags.annotation, tags.element
        ]
        result = xsd_elements.All()

        for child in node.iterchildren():
            assert child.tag in sub_types, child
            item = self.process(child, node)
            result.append(item)

        assert None not in result
        return result

    def visit_group(self, node, parent):
        """Groups a set of element declarations so that they can be
        incorporated as a group into complex type definitions.

        Definition::

            <group
              name= NCName
              id = ID
              maxOccurs = (nonNegativeInteger | unbounded) : 1
              minOccurs = nonNegativeInteger : 1
              name = NCName
              ref = QName
              {any attributes with non-schema Namespace}...>
            Content: (annotation?, (all | choice | sequence))
            </group>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        min_occurs, max_occurs = _process_occurs_attrs(node)

        result = self.process_reference(
            node, min_occurs=min_occurs, max_occurs=max_occurs)
        if result:
            return result

        qname = qname_attr(node, 'name', self.document._target_namespace)

        # There should be only max nodes, first node (annotation) is irrelevant
        annotation, children = self._pop_annotation(node.getchildren())
        child = children[0]

        item = self.process(child, parent)
        elm = xsd_elements.Group(name=qname, child=item)

        if parent.tag == tags.schema:
            self.register_group(qname, elm)
        return elm

    def visit_list(self, node, parent):
        """
        Definition::

            <list
              id = ID
              itemType = QName
              {any attributes with non-schema Namespace}...>
            Content: (annotation?, (simpleType?))
            </list>

        The use of the simpleType element child and the itemType attribute is
        mutually exclusive.

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element


        """
        item_type = qname_attr(node, 'itemType')
        if item_type:
            sub_type = self._get_type(item_type.text)
        else:
            subnodes = node.getchildren()
            child = subnodes[-1]  # skip annotation
            sub_type = self.visit_simple_type(child, node)
        return xsd_types.ListType(sub_type)

    def visit_choice(self, node, parent):
        """
        Definition::

            <choice
              id = ID
              maxOccurs= (nonNegativeInteger | unbounded) : 1
              minOccurs= nonNegativeInteger : 1
              {any attributes with non-schema Namespace}...>
            Content: (annotation?, (element | group | choice | sequence | any)*)
            </choice>
        """
        min_occurs, max_occurs = _process_occurs_attrs(node)

        children = node.getchildren()
        annotation, children = self._pop_annotation(children)

        choices = []
        for child in children:
            elm = self.process(child, node)
            choices.append(elm)
        return xsd_elements.Choice(
            choices, min_occurs=min_occurs, max_occurs=max_occurs)

    def visit_union(self, node, parent):
        """Defines a collection of multiple simpleType definitions.

        Definition::

            <union
              id = ID
              memberTypes = List of QNames
              {any attributes with non-schema Namespace}...>
            Content: (annotation?, (simpleType*))
            </union>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        # TODO
        members = node.get('memberTypes')
        types = []
        if members:
            for member in members.split():
                qname = as_qname(member, node.nsmap)
                xsd_type = self._get_type(qname)
                types.append(xsd_type)
        else:
            annotation, types = self._pop_annotation(node.getchildren())
            types = [self.visit_simple_type(t, node) for t in types]
        return xsd_types.UnionType(types)

    def visit_unique(self, node, parent):
        """Specifies that an attribute or element value (or a combination of
        attribute or element values) must be unique within the specified scope.
        The value must be unique or nil.

        Definition::

            <unique
              id = ID
              name = NCName
              {any attributes with non-schema Namespace}...>
            Content: (annotation?, (selector, field+))
            </unique>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        # TODO
        pass

    def visit_attribute_group(self, node, parent):
        """
        Definition::

            <attributeGroup
              id = ID
              name = NCName
              ref = QName
              {any attributes with non-schema Namespace...}>
            Content: (annotation?),
                     ((attribute | attributeGroup)*, anyAttribute?))
            </attributeGroup>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        ref = self.process_reference(node)
        if ref:
            return ref

        qname = qname_attr(node, 'name', self.document._target_namespace)
        annotation, children = self._pop_annotation(node.getchildren())

        attributes = self._process_attributes(node, children)
        attribute_group = xsd_elements.AttributeGroup(qname, attributes)
        self.register_attribute_group(qname, attribute_group)

    def visit_any_attribute(self, node, parent):
        """
        Definition::

            <anyAttribute
              id = ID
              namespace = ((##any | ##other) |
                List of (anyURI | (##targetNamespace | ##local))) : ##any
              processContents = (lax | skip | strict): strict
              {any attributes with non-schema Namespace...}>
            Content: (annotation?)
            </anyAttribute>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        process_contents = node.get('processContents', 'strict')
        return xsd_elements.AnyAttribute(process_contents=process_contents)

    def visit_notation(self, node, parent):
        """Contains the definition of a notation to describe the format of
        non-XML data within an XML document. An XML Schema notation declaration
        is a reconstruction of XML 1.0 NOTATION declarations.

        Definition::

            <notation
              id = ID
              name = NCName
              public = Public identifier per ISO 8879
              system = anyURI
              {any attributes with non-schema Namespace}...>
            Content: (annotation?)
            </notation>

        :param node: The XML node
        :type node: lxml.etree._Element
        :param parent: The parent XML node
        :type parent: lxml.etree._Element

        """
        pass

    def _get_type(self, name):
        assert name is not None
        name = self._create_qname(name)
        return UnresolvedType(name, self.schema)

    def _create_qname(self, name):
        if not isinstance(name, etree.QName):
            name = etree.QName(name)

        # Handle reserved namespace
        if name.namespace == 'xml':
            name = etree.QName(
                'http://www.w3.org/XML/1998/namespace', name.localname)

        # Various xsd builders assume that some schema's are available by
        # default (actually this is mostly just the soap-enc ns). So live with
        # that fact and handle it by auto-importing the schema if it is
        # referenced.
        if (
            name.namespace == 'http://schemas.xmlsoap.org/soap/encoding/' and
            not self.document.is_imported(name.namespace)
        ):
            import_node = etree.Element(
                tags.import_,
                namespace=name.namespace, schemaLocation=name.namespace)
            self.visit_import(import_node, None)

        return name

    def _pop_annotation(self, items):
        if not len(items):
            return None, []

        if items[0].tag == tags.annotation:
            annotation = self.visit_annotation(items[0], None)
            return annotation, items[1:]
        return None, items

    def _process_attributes(self, node, items):
        attributes = []
        for child in items:
            if child.tag in (tags.attribute, tags.attributeGroup, tags.anyAttribute):
                attribute = self.process(child, node)
                attributes.append(attribute)
            else:
                raise XMLParseError(
                    "Unexpected tag `%s`" % (child.tag),
                    filename=self.document._location,
                    sourceline=node.sourceline)
        return attributes

    visitors = {
        tags.any: visit_any,
        tags.element: visit_element,
        tags.choice: visit_choice,
        tags.simpleType: visit_simple_type,
        tags.anyAttribute: visit_any_attribute,
        tags.complexType: visit_complex_type,
        tags.simpleContent: None,
        tags.complexContent: None,
        tags.sequence: visit_sequence,
        tags.all: visit_all,
        tags.group: visit_group,
        tags.attribute: visit_attribute,
        tags.import_: visit_import,
        tags.include: visit_include,
        tags.annotation: visit_annotation,
        tags.attributeGroup: visit_attribute_group,
        tags.notation: visit_notation,
    }


def _process_occurs_attrs(node):
    """Process the min/max occurrence indicators"""
    max_occurs = node.get('maxOccurs', '1')
    min_occurs = int(node.get('minOccurs', '1'))
    if max_occurs == 'unbounded':
        max_occurs = 'unbounded'
    else:
        max_occurs = int(max_occurs)

    return min_occurs, max_occurs
