"""
Temporary replace xmi.py from pyEcore with our own as multi-valued attributed values are not correctly read from the
xmi file. In pyEcore 0.13 + 0.14 they used () tuple notation instead of [] for multi-valued attribute values causing it to return a
generator for those values instead of a list of values.

The xmi module introduces XMI resource and XMI parsing.
"""
from enum import unique, Enum
from functools import lru_cache
from lxml.etree import parse, QName, Element, SubElement, ElementTree
from pyecore.resources.resource import Resource
from pyecore.ecore import EClass, EStringToStringMapEntry, EAnnotation, EProxy, \
                    EDataType

XSI = 'xsi'
XSI_URL = 'http://www.w3.org/2001/XMLSchema-instance'
XMI = 'xmi'
XMI_URL = 'http://www.omg.org/XMI'


@unique
class XMIOptions(Enum):
    OPTION_USE_XMI_TYPE = 0
    SERIALIZE_DEFAULT_VALUES = 1


class XMIResource(Resource):
    def __init__(self, uri=None, use_uuid=False):
        super().__init__(uri, use_uuid)
        self._later = []
        self.prefixes = {}
        self.reverse_nsmap = {}

    def load(self, options=None):
        self.options = options or {}
        self.cache_enabled = True
        tree = parse(self.uri.create_instream())
        xmlroot = tree.getroot()
        self.prefixes.update(xmlroot.nsmap)
        self.reverse_nsmap = {v: k for k, v in self.prefixes.items()}

        self.xsitype = f'{{{self.prefixes.get(XSI)}}}type'
        self.xmiid = f'{{{self.prefixes.get(XMI)}}}id'
        self.schema_tag = f'{{{self.prefixes.get(XSI)}}}schemaLocation'

        # Decode the XMI
        if f'{{{self.prefixes.get(XMI)}}}XMI' == xmlroot.tag:
            real_roots = xmlroot
        else:
            real_roots = [xmlroot]

        def grouper(iterable):
            args = [iter(iterable)] * 2
            return zip(*args)

        self.schema_locations = {}
        schema_tag_list = xmlroot.attrib.get(self.schema_tag, '')
        for prefix, path in grouper(schema_tag_list.split()):
            if '#' not in path:
                path = path + '#'
            self.schema_locations[prefix] = EProxy(path, self)

        for root in real_roots:
            modelroot = self._init_modelroot(root)
            for child in root:
                self._decode_eobject(child, modelroot)

        if self.contents:
            self._decode_ereferences()

        self._clean_registers()
        self.uri.close_stream()

    def xsi_type_url(self):
        if self.options.get(XMIOptions.OPTION_USE_XMI_TYPE, False):
            return XMI_URL
        return XSI_URL

    @staticmethod
    def extract_namespace(tag):
        qname = QName(tag)
        return qname.namespace, qname.localname

    def _type_attribute(self, node):
        type_ = node.get(self.xsitype)
        if type_ is None:
            xmi_type_url = f'{{{self.prefixes.get(XMI)}}}type'
            type_ = node.get(xmi_type_url)
        return type_

    def _get_metaclass(self, nsURI, eclass_name):
        try:
            return self.get_metamodel(nsURI).getEClassifier(eclass_name)
        except Exception as e:
            proxy = self.schema_locations[nsURI]
            try:
                return proxy.getEClassifier(eclass_name)
            except Exception:
                raise e

    def _init_modelroot(self, xmlroot):
        nsURI, eclass_name = self.extract_namespace(xmlroot.tag)
        eclass = self._get_metaclass(nsURI, eclass_name)
        if not eclass:
            raise TypeError(f'"{eclass_name}" EClass does not exists '
                            f'(line {xmlroot.tag})')
        modelroot = eclass()
        modelroot._eresource = self
        self.use_uuid = xmlroot.get(self.xmiid) is not None
        self.contents.append(modelroot)
        erefs = []
        for key, value in xmlroot.attrib.items():
            namespace, _ = self.extract_namespace(key)
            if key == self.xmiid:
                modelroot._internal_id = value
                self.uuid_dict[value] = modelroot
            # Do stuff with this
            # elif namespace:
            #     try:
            #
            #         # metaclass = self.get_metamodel(namespace)
            #         pass
            #     except KeyError:
            #         pass
            elif not namespace:
                feature = self._find_feature(modelroot.eClass, key)
                if not feature:
                    continue
                if feature.is_attribute:
                    self._decode_eattribute_value(modelroot, feature, value)
                    if feature.iD:
                        self.uuid_dict[value] = modelroot
                else:
                    erefs.append((feature, value))
        if erefs:
            self._later.append((modelroot, erefs))
        return modelroot

    @staticmethod
    def _decode_eattribute_value(eobject, eattribute, value, from_tag=False):
        is_many = eattribute.many
        if is_many and not from_tag:
            values = value.split()
            from_string = eattribute._eType.from_string
            results = [from_string(x) for x in values]
            eobject.__getattribute__(eattribute.name).extend(results)
        elif is_many:
            value = eattribute._eType.from_string(value)
            eobject.__getattribute__(eattribute._name).append(value)
        else:
            val = eattribute._eType.from_string(value)
            eobject.__setattr__(eattribute._name, val)

    def _decode_eobject(self, current_node, parent_eobj):
        eobject_info = self._decode_node(parent_eobj, current_node)
        feat_container, eobject, eatts, erefs, from_tag = eobject_info

        # deal with eattributes and ereferences
        for eattribute, value in eatts:
            self._decode_eattribute_value(eobject, eattribute, value, from_tag)

        if erefs:
            self._later.append((eobject, erefs))

        if not feat_container:
            return

        # attach the new eobject to the parent one
        if feat_container.many:
            parent_eobj.__getattribute__(feat_container._name).append(eobject)
        else:
            parent_eobj.__setattr__(feat_container._name, eobject)

        # iterate on children
        for child in current_node:
            self._decode_eobject(child, eobject)

    def _is_none_node(self, node):
        return f'{{{XSI_URL}}}nil' in node.attrib

    def _decode_node(self, parent_eobj, node):
        _, node_tag = self.extract_namespace(node.tag)
        feature_container = self._find_feature(parent_eobj.eClass, node_tag)
        if not feature_container:
            raise ValueError(f'Feature "{node_tag}" is unknown '
                             f'for {parent_eobj.eClass.name}, '
                             f'line {node.sourceline}')
        if self._is_none_node(node):
            if feature_container.many:
                parent_eobj.__getattribute__(feature_container._name) \
                           .append(None)
            else:
                parent_eobj.__setattr__(feature_container._name, None)

            return (None, None, [], [], False)
        if node.get('href'):
            ref = node.get('href')
            proxy = EProxy(path=ref, resource=self)
            return (feature_container, proxy, [], [], False)
        if self._type_attribute(node):
            prefix, _type = self._type_attribute(node).split(':')
            if not prefix:
                raise ValueError(f'Prefix {prefix} is not registered, '
                                 f'{node.tag} line {node.sourceline}')
            epackage = self.prefix2epackage(prefix)
            etype = epackage.getEClassifier(_type)
            if not etype:
                raise ValueError(f'Type {_type} is unknown in {epackage}, '
                                 f'{node.tag} line {node.sourceline}')
        else:
            etype = feature_container._eType
            if isinstance(etype, EProxy):
                etype.force_resolve()

        # we create the instance
        if etype is EClass or etype is EClass.eClass:
            name = node.get('name')
            eobject = etype(name)
        elif (etype is EStringToStringMapEntry
              or etype is EStringToStringMapEntry.eClass) \
                and feature_container is EAnnotation.details:
            annotation_key = node.get('key')
            annotation_value = node.get('value')
            parent_eobj.details[annotation_key] = annotation_value
            if annotation_key == 'documentation':
                container = parent_eobj.eContainer()
                if hasattr(container, 'python_class'):
                    container = container.python_class
                container.__doc__ = annotation_value
            return (None, None, (), (), False)
        elif isinstance(etype, EDataType):
            value = node.text if node.text else ''
            return (None, parent_eobj, ((feature_container, value),),
                    (), True)
        else:
            # idref = node.get(f'{{{XMI_URL}}}idref')
            # if idref:
            #     return (None, parent_eobj, [],
            #             [(feature_container, idref)], True)
            eobject = etype()

        # we sort the node feature (no containments)
        eatts = []
        erefs = []
        for key, value in node.attrib.items():
            feature = self._decode_attribute(eobject, key, value, node)
            if not feature:
                continue  # we skip the unknown features
            if etype is EClass and feature._name == 'name':
                continue  # we skip the name for metamodel import
            if feature.is_attribute:
                eatts.append((feature, value))
                if feature.iD:
                    self.uuid_dict[value] = eobject
            else:
                erefs.append((feature, value))
        return (feature_container, eobject, eatts, erefs, False)

    def _decode_attribute(self, owner, key, value, node):
        namespace, att_name = self.extract_namespace(key)
        prefix = self.reverse_nsmap[namespace] if namespace else None
        # This is a special case, we are working with uuids
        if key == self.xmiid:
            owner._internal_id = value
            self.uuid_dict[value] = owner
        elif prefix in ('xsi', 'xmi') and att_name == 'type':
            # type has already been handled
            pass
        # elif namespace:
        #     pass
        elif not namespace:
            if att_name == 'href':
                return
            feature = self._find_feature(owner.eClass, att_name)
            if not feature:
                raise ValueError(f'Feature {att_name} does not exists for '
                                 f'type {owner.eClass.name} '
                                 f'({node.tag} line {node.sourceline})')
            return feature

    def _decode_ereferences(self):
        opposite = []
        for eobject, erefs in self._later:
            for ref, value in erefs:
                name = ref._name
                if name == 'eOpposite':
                    opposite.append((eobject, ref, value))
                    continue
                if ref.many:
                    values = [self.normalize(x) for x in value.split()]
                else:
                    values = [value]
                for value in values:
                    if not value:  # BA: Skip empty references
                        continue
                    resolved_value = self._resolve_nonhref(value)
                    if not resolved_value:
                        raise ValueError(f'EObject for {value} is unknown')
                    if not hasattr(resolved_value, '_inverse_rels'):
                        resolved_value = resolved_value.eClass
                    if ref.many:
                        eobject.__getattribute__(name).append(resolved_value)
                    else:
                        eobject.__setattr__(name, resolved_value)

        for eobject, ref, value in opposite:
            resolved_value = self._resolve_nonhref(value)
            if not resolved_value:
                raise ValueError(f'EObject for {value} is unknown')
            eobject.__setattr__(ref._name, resolved_value)

    @lru_cache()
    def _resolve_nonhref(self, path):
        uri, fragment = self._is_external(path)
        if uri:
            cleaned_uri = uri + '#' + fragment
            if cleaned_uri in self._resolve_mem:
                return self._resolve_mem[cleaned_uri]
            proxy = EProxy(path=cleaned_uri, resource=self)
            self._resolve_mem[cleaned_uri] = proxy
            return proxy
        if fragment in self._resolve_mem:
            return self._resolve_mem[fragment]
        return self.resolve(fragment)

    def _clean_registers(self):
        self._later.clear()
        self._find_feature.cache_clear()
        self._resolve_nonhref.cache_clear()
        self._resolve_mem.clear()
        self.cache_enabled = False

    def register_nsmap(self, prefix, uri):
        if uri in self.reverse_nsmap:
            return
        if prefix not in self.prefixes:
            self.prefixes[prefix] = uri
            self.reverse_nsmap[uri] = prefix
            return
        same_prefix = [x for x in self.prefixes.keys() if x.startswith(prefix)]
        prefix = f'{prefix}_{len(same_prefix)}'
        self.prefixes[prefix] = uri
        self.reverse_nsmap[uri] = prefix

    def register_eobject_epackage(self, eobj):
        epackage = eobj.eClass.ePackage
        prefix = epackage.nsPrefix
        nsURI = epackage.nsURI
        self.register_nsmap(prefix, nsURI)

    def save(self, output=None, options=None):
        self.options = options or {}
        output = self.open_out_stream(output)
        self.prefixes.clear()
        self.reverse_nsmap.clear()

        serialize_default = \
            self.options.get(XMIOptions.SERIALIZE_DEFAULT_VALUES,
                             False)
        nsmap = {XMI: XMI_URL}

        if len(self.contents) == 1:
            root = self.contents[0]
            self.register_eobject_epackage(root)
            tmp_xmi_root = self._go_across(root, serialize_default)
        else:
            tag = QName(XMI_URL, 'XMI')
            tmp_xmi_root = Element(tag)
            for root in self.contents:
                root_node = self._go_across(root, serialize_default)
                tmp_xmi_root.append(root_node)

        # update nsmap with prefixes register during the nodes creation
        nsmap.update(self.prefixes)
        xmi_root = Element(tmp_xmi_root.tag, nsmap=nsmap)
        xmi_root[:] = tmp_xmi_root[:]
        xmi_root.attrib.update(tmp_xmi_root.attrib)
        xmi_version = QName(XMI_URL, 'version')
        xmi_root.attrib[xmi_version] = '2.0'
        tree = ElementTree(xmi_root)
        tree.write(output,
                   pretty_print=True,
                   xml_declaration=True,
                   encoding=tree.docinfo.encoding)
        output.flush()
        self.uri.close_stream()

    def _add_explicit_type(self, node, obj):
        self.prefixes[XSI] = XSI_URL
        xsi_type = QName(self.xsi_type_url(), 'type')
        uri = obj.eClass.ePackage.nsURI
        if uri not in self.reverse_nsmap:
            # epackage = self.get_metamodel(uri)
            epackage = obj.eClass.ePackage
            self.register_nsmap(epackage.nsPrefix, uri)
        prefix = self.reverse_nsmap[uri]
        node.attrib[xsi_type] = f'{prefix}:{obj.eClass.name}'

    def _build_none_node(self, feature_name):
        sub = Element(feature_name)
        xsi_null = QName(self.xsi_type_url(), 'nil')
        sub.attrib[xsi_null] = 'true'
        return sub

    def _go_across(self, obj, serialize_default=False):
        eclass = obj.eClass
        if obj.eContainmentFeature():
            node = Element(obj.eContainmentFeature()._name)
            if obj.eContainmentFeature()._eType != eclass:
                self._add_explicit_type(node, obj)
        else:
            # obj is the root
            epackage = eclass.ePackage
            nsURI = epackage.nsURI
            tag = QName(nsURI, eclass.name) if nsURI else eclass.name
            node = Element(tag)

        if self.use_uuid:
            self._assign_uuid(obj)
            xmi_id = f'{{{XMI_URL}}}id'
            node.attrib[xmi_id] = obj._internal_id

        for feat in obj._isset:
            if feat.derived or feat.transient:
                continue
            feat_name = feat._name
            value = obj.__getattribute__(feat_name)
            if value is None:
                if serialize_default:
                    node.append(self._build_none_node(feat_name))
                continue
            if hasattr(feat._eType, 'eType') and feat._eType.eType is dict:
                for key, val in value.items():
                    entry = Element(feat_name)
                    entry.attrib['key'] = key
                    entry.attrib['value'] = val
                    node.append(entry)
            elif feat.is_attribute:
                etype = feat._eType
                if feat.many and value:
                    to_str = etype.to_string
                    has_special_char = False
                    result_list = []
                    for v in value:
                        string = None if v is None else to_str(v)
                        if not string or any(x.isspace() for x in string):
                            has_special_char = True
                        result_list.append(string)
                    if has_special_char:
                        for v in result_list:
                            if v is None:
                                node.append(self._build_none_node(feat_name))
                            else:
                                sub = SubElement(node, feat_name)
                                sub.text = v
                    else:
                        node.attrib[feat_name] = ' '.join(result_list)
                    continue
                default_value = feat.get_default_value()
                if value != default_value or serialize_default:
                    node.attrib[feat_name] = etype.to_string(value)
                continue

            elif feat.eOpposite and feat.eOpposite.containment:
                continue
            elif not feat.containment:
                if feat.many:
                    results = (self._build_path_from(x) for x in value)
                    embedded = []
                    crossref = []
                    for i, result in enumerate(results):
                        frag, cref = result
                        if cref:
                            crossref.append((i, frag))
                        else:
                            embedded.append(frag)
                    if embedded:
                        result = ' '.join(embedded)
                        node.attrib[feat_name] = result
                    for i, ref in crossref:
                        sub = SubElement(node, feat_name)
                        sub.attrib['href'] = ref
                        self._add_explicit_type(sub, value[i])
                else:
                    frag, is_crossref = self._build_path_from(value)
                    if is_crossref:
                        sub = SubElement(node, feat_name)
                        sub.attrib['href'] = frag
                        self._add_explicit_type(sub, value)
                    else:
                        node.attrib[feat_name] = frag

            else:
                children = value if feat.many else [value]
                for child in children:
                    node.append(self._go_across(child, serialize_default))
        return node