# encoding: utf-8

from owslib.etree import etree
from owslib import crs, util
from owslib.util import testXMLValue, testXMLAttribute, nspath_eval, xmltag_split, dict_union, extract_xml_list
from owslib.namespaces import Namespaces


def get_namespaces():
    n = Namespaces()
    namespaces = n.get_namespaces(["sml", "gml", "xlink"])
    namespaces["ism"] = "urn:us:gov:ic:ism:v2"
    return namespaces


namespaces = get_namespaces()


def nsp(path):
    return nspath_eval(path, namespaces)


class SensorML(object):
    def __init__(self, element):
        if isinstance(element, str) or isinstance(element, bytes):
            self._root = etree.fromstring(element)
        else:
            self._root = element

        if hasattr(self._root, 'getroot'):
            self._root = self._root.getroot()

        self.members = [Member(x) for x in self._root.findall(nsp('sml:member'))]


class Member(object):
    def __new__(cls, element):
        t = element[-1].tag.split("}")[-1]
        if t == "System":
            return System(element.find(nsp("sml:System")))
        elif t == "ProcessChain":
            return ProcessChain(element.find(nsp("sml:ProcessChain")))
        elif t == "ProcessModel":
            return ProcessModel(element.find(nsp("sml:ProcessModel")))
        elif t == "Component":
            return Component(element.find(nsp("sml:Component")))


class PropertyGroup(object):
    def __init__(self, element):
        # Both capabilities and characteristics contain a single swe:DataRecord element
        self.capabilities = {}
        for cap in element.findall(nsp('sml:capabilities')):
            name = testXMLAttribute(cap, "name")
            if name is not None:
                self.capabilities[name] = cap[0]

        self.characteristics = {}
        for cha in element.findall(nsp('sml:characteristics')):
            name = testXMLAttribute(cha, "name")
            if name is not None:
                self.characteristics[name] = cha[0]

    def get_capabilities_by_name(self, name):
        """
            Return list of element by name, case insensitive
        """
        return [self.capabilities[capab] for capab in list(self.capabilities.keys()) if capab.lower() == name.lower()]

    def get_characteristics_by_name(self, name):
        """
            Return list of element objects by name, case insensitive
        """
        return [self.characteristics[charac]
                for charac in list(self.characteristics.keys()) if charac.lower() == name.lower()]


class ConstraintGroup(object):
    def __init__(self, element):
        # ism:SecurityAttributesOptionsGroup
        self.security = element.findall(nsp("sml:securityConstraint/sml:Security/ism:SecurityAttributesOptionGroup"))
        # gml:TimeInstant or gml:TimePeriod element
        self.validTime = element.find(nsp("sml:validTime"))
        self.rights = [Right(x) for x in element.findall(nsp("sml:legalConstraint/sml:Rights"))]


class Documentation(object):
    def __init__(self, element):
        self.arcrole = testXMLAttribute(element, nsp("xlink:arcrole"))
        self.url = testXMLAttribute(element, nsp("xlink:href"))
        self.documents = [Document(d) for d in element.findall(nsp("sml:Document"))]


class Document(object):
    def __init__(self, element):
        self.id = testXMLAttribute(element, nsp("gml:id"))
        self.version = testXMLValue(element.find(nsp("sml:version")))
        self.description = testXMLValue(element.find(nsp("gml:description")))
        self.date = testXMLValue(element.find(nsp("sml:date")))
        try:
            self.contact = Contact(element.find(nsp("sml:contact")))
        except AttributeError:
            self.contact = None
        self.format = testXMLValue(element.find(nsp('sml:format')))
        self.url = testXMLAttribute(element.find(nsp('sml:onlineResource')), nsp('xlink:href'))


class Right(object):
    def __init__(self, element):
        self.id = testXMLAttribute(element, nsp('gml:id'))
        self.privacyAct = testXMLAttribute(element, nsp('sml:privacyAct'))
        self.intellectualPropertyRights = testXMLAttribute(element, nsp('sml:intellectualPropertyRights'))
        self.copyRights = testXMLAttribute(element, nsp('sml:copyRights'))
        self.documentation = [Documentation(x) for x in element.findall(nsp("sml:documentation"))]


class ReferenceGroup(object):
    def __init__(self, element):
        self.contacts = {}
        for contact in element.findall(nsp('sml:contact')):
            cont = Contact(contact)
            self.contacts[cont.role] = cont

        self.documentation = [Documentation(x) for x in element.findall(nsp("sml:documentation"))]

    def get_contacts_by_role(self, role):
        """
            Return a Contact by role, case insensitive
        """
        return [self.contacts[contact] for contact in list(self.contacts.keys()) if contact.lower() == role.lower()]


class GeneralInfoGroup(object):
    def __init__(self, element):
        self.keywords = extract_xml_list(element.findall(nsp('sml:keywords/sml:KeywordList/sml:keyword')))

        self.identifiers = {}
        for identifier in element.findall(nsp('sml:identification/sml:IdentifierList/sml:identifier')):
            ident = Identifier(identifier)
            self.identifiers[ident.name] = ident

        self.classifiers = {}
        for classifier in element.findall(nsp('sml:classification/sml:ClassifierList/sml:classifier')):
            classi = Classifier(classifier)
            self.classifiers[classi.name] = classi

    def get_identifiers_by_name(self, name):
        """
            Return list of Identifier objects by name, case insensitive
        """
        return [self.identifiers[identifier]
                for identifier in list(self.identifiers.keys()) if identifier.lower() == name.lower()]

    def get_classifiers_by_name(self, name):
        """
            Return list of Classifier objects by name, case insensitive
        """
        return [self.classifiers[classi] for classi in list(self.classifiers.keys()) if classi.lower() == name.lower()]


class Contact(object):
    def __init__(self, element):
        # TODO: This only supports the sml:contact/sml:ResponsibleParty elements, but there are numerous ways to store
        # contact information here.
        self.role = testXMLAttribute(element, nsp("xlink:role"))
        self.href = testXMLAttribute(element, nsp("xlink:href"))
        self.organization = testXMLValue(element.find(nsp('sml:ResponsibleParty/sml:organizationName')))
        self.phone = testXMLValue(element.find(nsp('sml:ResponsibleParty/sml:contactInfo/sml:phone/sml:voice')))
        self.address = testXMLValue(
            element.find(nsp('sml:ResponsibleParty/sml:contactInfo/sml:address/sml:deliveryPoint')))
        self.city = testXMLValue(element.find(nsp('sml:ResponsibleParty/sml:contactInfo/sml:address/sml:city')))
        self.region = testXMLValue(
            element.find(nsp('sml:ResponsibleParty/sml:contactInfo/sml:address/sml:administrativeArea')))
        self.postcode = testXMLValue(
            element.find(nsp('sml:ResponsibleParty/sml:contactInfo/sml:address/sml:postalCode')))
        self.country = testXMLValue(element.find(nsp('sml:ResponsibleParty/sml:contactInfo/sml:address/sml:country')))
        self.email = testXMLValue(
            element.find(nsp('sml:ResponsibleParty/sml:contactInfo/sml:address/sml:electronicMailAddress')))
        self.url = testXMLAttribute(
            element.find(nsp('sml:ResponsibleParty/sml:contactInfo/sml:onlineResource')), nsp("xlink:href"))


class HistoryGroup(object):
    def __init__(self, element):
        self.history = {}
        for event_member in element.findall(nsp('sml:history/sml:EventList/sml:member')):
            name = testXMLAttribute(event_member, "name")
            if self.history.get(name) is None:
                self.history[name] = []
            for e in event_member.findall(nsp("sml:Event")):
                self.history[name].append(Event(e))

    def get_history_by_name(self, name):
        """
            Return Events list by members name
        """
        return self.history.get(name.lower(), [])


class Event(ReferenceGroup, GeneralInfoGroup):
    def __init__(self, element):
        ReferenceGroup.__init__(self, element)
        GeneralInfoGroup.__init__(self, element)
        self.id = testXMLAttribute(element, nsp("gml:id"))
        self.date = testXMLValue(element.find(nsp('sml:date')))
        self.description = testXMLValue(element.find(nsp('gml:description')))


class MetadataGroup(GeneralInfoGroup, PropertyGroup, ConstraintGroup, ReferenceGroup, HistoryGroup):
    def __init__(self, element):
        GeneralInfoGroup.__init__(self, element)
        PropertyGroup.__init__(self, element)
        ConstraintGroup.__init__(self, element)
        ReferenceGroup.__init__(self, element)
        HistoryGroup.__init__(self, element)


class AbstractFeature(object):
    def __init__(self, element):
        self.name = testXMLValue(element.find(nsp("gml:name")))
        self.description = testXMLValue(element.find(nsp("gml:description")))
        self.gmlBoundedBy = testXMLValue(element.find(nsp("gml:boundedBy")))


class AbstractProcess(AbstractFeature, MetadataGroup):
    def __init__(self, element):
        AbstractFeature.__init__(self, element)
        MetadataGroup.__init__(self, element)
        # sml:IoComponentPropertyType
        self.inputs = element.findall(nsp("sml:input"))
        # sml:IoComponentPropertyType
        self.outputs = element.findall(nsp("sml:output"))
        # swe:DataComponentPropertyType
        self.parameters = element.findall(nsp("sml:parameter"))


class AbstractRestrictedProcess(AbstractFeature):
    """ Removes ('restricts' in xml schema language) gml:name, gml:description,
    and sml:metadataGroup from an AbstractProcess """
    def __init__(self, element):
        AbstractFeature.__init__(self, element)
        self.name = None
        self.description = None


class AbstractPureProcess(AbstractRestrictedProcess):
    def __init__(self, element):
        AbstractRestrictedProcess.__init__(self, element)

        # sml:IoComponentPropertyType
        self.inputs = element.findall(nsp("sml:input"))
        # sml:IoComponentPropertyType
        self.outputs = element.findall(nsp("sml:output"))
        # swe:DataComponentPropertyType
        self.parameters = element.findall(nsp("sml:parameter"))


class ProcessModel(AbstractPureProcess):
    def __init__(self, element):
        AbstractPureProcess.__init__(self, element)
        self.method = ProcessMethod(element.find("method"))


class CompositePropertiesGroup(object):
    def __init__(self, element):
        # All components should be of instance AbstractProcess (sml:_Process)
        self.components = element.findall(nsp("sml:components/sml:ComponentList/sml:component"))
        # sml:Link or sml:ArrayLink element
        self.connections = element.findall(nsp("sml:connections/sml:ConnectionList/sml:connection"))


class PhysicalPropertiesGroup(object):
    def __init__(self, element):
        # gml:EngieeringCRS element
        self.spatialReferenceFrame = element.find(nsp("sml:spatialReferenceFrame/gml:EngineeringCRS"))
        # gml:TemporalCRS element
        self.temporalReferenceFrame = element.find(nsp("sml:temporalReferenceFrame/gml:TemporalCRS"))
        # gml:Envelope element
        self.smlBoundedBy = element.find(nsp("sml:boundedBy"))
        # swe:Time or sml:_Process element
        self.timePosition = element.find(nsp("sml:timePosition"))

        # It is either a sml:position OR and sml:location element here.  Process both.
        # swe:Position, swe:Vector, or sml:_Process element
        self.positions = element.findall(nsp("sml:position"))
        # gml:Point of gml:_Curve
        self.location = element.find(nsp("sml:location"))

        try:
            self.interface = Interface(element.find(nsp("sml:interface")))
        except AttributeError:
            self.interface = None


class ProcessChain(AbstractPureProcess, CompositePropertiesGroup):
    def __init__(self, element):
        AbstractPureProcess.__init__(self, element)
        CompositePropertiesGroup.__init__(self, element)


class System(AbstractProcess, PhysicalPropertiesGroup, CompositePropertiesGroup):
    def __init__(self, element):
        AbstractProcess.__init__(self, element)
        PhysicalPropertiesGroup.__init__(self, element)
        CompositePropertiesGroup.__init__(self, element)


class Component(AbstractProcess, PhysicalPropertiesGroup):
    def __init__(self, element):
        AbstractProcess.__init__(self, element)
        PhysicalPropertiesGroup.__init__(self, element)
        self.method = ProcessMethod(element.find("method"))


class Term(object):
    def __init__(self, element):
        self.codeSpace = testXMLAttribute(element.find(nsp('sml:Term/sml:codeSpace')), nsp("xlink:href"))
        self.definition = testXMLAttribute(element.find(nsp('sml:Term')), "definition")
        self.value = testXMLValue(element.find(nsp('sml:Term/sml:value')))


class Classifier(Term):
    def __init__(self, element):
        Term.__init__(self, element)
        self.name = testXMLAttribute(element, "name")


class Identifier(Term):
    def __init__(self, element):
        Term.__init__(self, element)
        self.name = testXMLAttribute(element, "name")


class ProcessMethod(MetadataGroup):
    """ Inherits from gml:AbstractGMLType """
    def __init__(self, element):
        MetadataGroup.__init__(self, element)
        self.rules = element.find(nsp("sml:rules"))
        self.ioStructure = element.find(nsp("sml:IOStructureDefinition"))
        self.algorithm = element.find(nsp("sml:algorithm"))
        self.implementations = element.findall(nsp("sml:implementation"))


class Interface(object):
    def __init__(self, element):
        self.name = testXMLAttribute(element, "name")
        self.interface_definition = InterfaceDefinition(element.find(nsp("sml:InterfaceDefinition")))


class InterfaceDefinition(object):
    def __init__(self, element):
        raise NotImplementedError("InterfaceDefinition is not implemented in OWSLib (yet)")


class Link(object):
    def __init__(self, element):
        raise NotImplementedError("Link is not implemented in OWSLib (yet)")


class ArrayLink(object):
    def __init__(self, element):
        raise NotImplementedError("ArrayLink is not implemented in OWSLib (yet)")
