from opcua import ua
from enum import Enum
import logging

import xml.etree.ElementTree as Et
import re


logger = logging.getLogger(__name__)
# Indicates which type should be OPC build in types
_ua_build_in_types = [ua_type for ua_type in ua.VariantType.__members__ if ua_type != 'ExtensionObject']


def _repl_func(m):
    """
    taken from
     https://stackoverflow.com/questions/1549641/how-to-capitalize-the-first-letter-of-each-word-in-a-string-python
     """
    return m.group(1) + m.group(2).upper()


def _to_camel_case(name):
    """
    Create python class name from an arbitrary string to CamelCase string
    e.g.                 actionlib/TestAction -> ActionlibTestAction
         turtle_actionlib/ShapeActionFeedback -> TurtleActionlibShapeActionFeedback
    """
    name = re.sub(r'[^a-zA-Z0-9]+', ' ', name)
    name = re.sub('(^|\s)(\S)', _repl_func, name)
    name = name.replace(' ', '')
    return name


class OPCTypeDictionaryBuilder:

    def __init__(self, idx_name):
        """
        :param idx_name: name of the name space
        types in dict is created as opc:xxx, otherwise as tns:xxx
        """
        head_attributes = {'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:tns': idx_name,
                           'DefaultByteOrder': 'LittleEndian', 'xmlns:opc': 'http://opcfoundation.org/BinarySchema/',
                           'xmlns:ua': 'http://opcfoundation.org/UA/', 'TargetNamespace': idx_name}

        self.etree = Et.ElementTree(Et.Element('opc:TypeDictionary', head_attributes))

        name_space = Et.SubElement(self.etree.getroot(), 'opc:Import')
        name_space.attrib['Namespace'] = 'http://opcfoundation.org/UA/'

        self._structs_dict = {}
        self._build_in_list = _ua_build_in_types
    
    def _process_type(self, data_type):
        if data_type in self._build_in_list:
            data_type = 'opc:' + data_type
        else:
            #data_type = 'tns:' + _to_camel_case(data_type)
            data_type = 'tns:' + data_type
        return data_type

    def _add_field(self, variable_name, data_type, struct_name):
        data_type = self._process_type(data_type)
        field = Et.SubElement(self._structs_dict[struct_name], 'opc:Field')
        field.attrib['Name'] = variable_name
        field.attrib['TypeName'] = data_type

    def _add_array_field(self, variable_name, data_type, struct_name):
        data_type = self._process_type(data_type)
        array_len = 'NoOf' + variable_name
        field = Et.SubElement(self._structs_dict[struct_name], 'opc:Field')
        field.attrib['Name'] = array_len
        field.attrib['TypeName'] = 'opc:Int32'
        field = Et.SubElement(self._structs_dict[struct_name], 'opc:Field')
        field.attrib['Name'] = variable_name
        field.attrib['TypeName'] = data_type
        field.attrib['LengthField'] = array_len

    def add_field(self, variable_name, data_type, struct_name, is_array=False):
        if isinstance(data_type, Enum):
            data_type = data_type.name
        if is_array:
            self._add_array_field(variable_name, data_type, struct_name)
        else:
            self._add_field(variable_name, data_type, struct_name)

    def append_struct(self, name):
        appended_struct = Et.SubElement(self.etree.getroot(), 'opc:StructuredType')
        appended_struct.attrib['BaseType'] = 'ua:ExtensionObject'
        #appended_struct.attrib['Name'] = _to_camel_case(name)
        appended_struct.attrib['Name'] = name
        self._structs_dict[name] = appended_struct
        return appended_struct

    def get_dict_value(self):
        self.indent(self.etree.getroot())
        # For debugging
        # Et.dump(self.etree.getroot())
        return Et.tostring(self.etree.getroot(), encoding='utf-8')

    def indent(self, elem, level=0):
        i = '\n' + level * '  '
        if len(elem):
            if not elem.text or not elem.text.strip():
                elem.text = i + '  '
            if not elem.tail or not elem.tail.strip():
                elem.tail = i
            for elem in elem:
                self.indent(elem, level + 1)
            if not elem.tail or not elem.tail.strip():
                elem.tail = i
        else:
            if level and (not elem.tail or not elem.tail.strip()):
                elem.tail = i


def _reference_generator(source_id, target_id, reference_type, is_forward=True):
    ref = ua.AddReferencesItem()
    ref.IsForward = is_forward
    ref.ReferenceTypeId = reference_type
    ref.SourceNodeId = source_id
    ref.TargetNodeClass = ua.NodeClass.DataType
    ref.TargetNodeId = target_id
    return ref


class DataTypeDictionaryBuilder:

    def __init__(self, server, idx, ns_urn, dict_name, dict_node_id=None):
        self._server = server
        self._session_server = server.get_node(ua.ObjectIds.RootFolder).server
        self._idx = idx
        if dict_node_id is None:
            self.dict_id = self._add_dictionary(dict_name)
        else:
            self.dict_id = dict_node_id 
        self._type_dictionary = OPCTypeDictionaryBuilder(ns_urn)

    def _add_dictionary(self, name):
        try:
            node = self._server.nodes.opc_binary.get_child("{}:{}".format(self._idx, name))
        except ua.uaerrors.BadNoMatch:
            node = ua.AddNodesItem()
            node.RequestedNewNodeId = ua.NodeId(0, self._idx)
            node.BrowseName = ua.QualifiedName(name, self._idx)
            node.NodeClass = ua.NodeClass.Variable
            node.ParentNodeId = ua.NodeId(ua.ObjectIds.OPCBinarySchema_TypeSystem, 0)
            node.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasComponent, 0)
            node.TypeDefinition = ua.NodeId(ua.ObjectIds.DataTypeDictionaryType, 0)
            attrs = ua.VariableAttributes()
            attrs.DisplayName = ua.LocalizedText(name)
            attrs.DataType = ua.NodeId(ua.ObjectIds.ByteString)
            # Value should be set after all data types created by calling set_dict_byte_string
            attrs.Value = ua.Variant(None, ua.VariantType.Null)
            attrs.ValueRank = -1
            node.NodeAttributes = attrs
            res = self._session_server.add_nodes([node])
            return res[0].AddedNodeId
        logger.warning("Making %s object for node %s which already exist, its data will be overriden", self, node)
        #FIXME: we have an issue 
        return node.nodeid

    def _link_nodes(self, linked_obj_node_id, data_type_node_id, description_node_id):
        """link the three node by their node ids according to UA standard"""
        refs = [
                # add reverse reference to BaseDataType -> Structure
                _reference_generator(data_type_node_id, ua.NodeId(ua.ObjectIds.Structure, 0),
                                     ua.NodeId(ua.ObjectIds.HasSubtype, 0), False),
                # add reverse reference to created data type
                _reference_generator(linked_obj_node_id, data_type_node_id,
                                     ua.NodeId(ua.ObjectIds.HasEncoding, 0), False),
                # add HasDescription link to dictionary description
                _reference_generator(linked_obj_node_id, description_node_id,
                                     ua.NodeId(ua.ObjectIds.HasDescription, 0)),
                # add reverse HasDescription link
                _reference_generator(description_node_id, linked_obj_node_id,
                                     ua.NodeId(ua.ObjectIds.HasDescription, 0), False),
                # add link to the type definition node
                _reference_generator(linked_obj_node_id, ua.NodeId(ua.ObjectIds.DataTypeEncodingType, 0),
                                     ua.NodeId(ua.ObjectIds.HasTypeDefinition, 0)),
                # add has type definition link
                _reference_generator(description_node_id, ua.NodeId(ua.ObjectIds.DataTypeDescriptionType, 0),
                                     ua.NodeId(ua.ObjectIds.HasTypeDefinition, 0)),
                # add forward link of dict to description item
                _reference_generator(self.dict_id, description_node_id,
                                     ua.NodeId(ua.ObjectIds.HasComponent, 0)),
                # add reverse link to dictionary
                _reference_generator(description_node_id, self.dict_id,
                                     ua.NodeId(ua.ObjectIds.HasComponent, 0), False)]
        self._session_server.add_references(refs)

    def _create_data_type(self, type_name, nodeid=None, init=True):
        #name = _to_camel_case(type_name)
        name = type_name

        if nodeid is None:
            # create data type node
            dt_node = ua.AddNodesItem()
            dt_node.RequestedNewNodeId = ua.NodeId(0, self._idx)
            dt_node.BrowseName = ua.QualifiedName(name, self._idx)
            dt_node.NodeClass = ua.NodeClass.DataType
            dt_node.ParentNodeId = ua.NodeId(ua.ObjectIds.Structure, 0)
            dt_node.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasSubtype, 0)
            dt_attributes = ua.DataTypeAttributes()
            dt_attributes.DisplayName = ua.LocalizedText(type_name)
            dt_node.NodeAttributes = dt_attributes

            res = self._session_server.add_nodes([dt_node])
            data_type_node_id = res[0].AddedNodeId
        else:
            data_type_node_id = nodeid

        added = [data_type_node_id]

        if init:
            # create description node
            desc_node = ua.AddNodesItem()
            desc_node.RequestedNewNodeId = ua.NodeId(0, self._idx)
            desc_node.BrowseName = ua.QualifiedName(name, self._idx)
            desc_node.NodeClass = ua.NodeClass.Variable
            desc_node.ParentNodeId = self.dict_id
            desc_node.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasComponent, 0)
            desc_node.TypeDefinition = ua.NodeId(ua.ObjectIds.DataTypeDescriptionType, 0)
            desc_attributes = ua.VariableAttributes()
            desc_attributes.DisplayName = ua.LocalizedText(type_name)
            desc_attributes.DataType = ua.NodeId(ua.ObjectIds.String)
            desc_attributes.Value = ua.Variant(name, ua.VariantType.String)
            desc_attributes.ValueRank = -1
            desc_node.NodeAttributes = desc_attributes

            res = self._session_server.add_nodes([desc_node])
            description_node_id = res[0].AddedNodeId
            added.append(description_node_id)

            # create object node which the loaded python class should link to
            obj_node = ua.AddNodesItem()
            obj_node.RequestedNewNodeId = ua.NodeId(0, self._idx)
            obj_node.BrowseName = ua.QualifiedName('Default Binary', 0)
            obj_node.NodeClass = ua.NodeClass.Object
            obj_node.ParentNodeId = data_type_node_id
            obj_node.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasEncoding, 0)
            obj_node.TypeDefinition = ua.NodeId(ua.ObjectIds.DataTypeEncodingType, 0)
            obj_attributes = ua.ObjectAttributes()
            obj_attributes.DisplayName = ua.LocalizedText('Default Binary')
            obj_attributes.EventNotifier = 0
            obj_node.NodeAttributes = obj_attributes

            res = self._session_server.add_nodes([obj_node])
            bind_obj_node_id = res[0].AddedNodeId
            added.append(bind_obj_node_id)

            self._link_nodes(bind_obj_node_id, data_type_node_id, description_node_id)

        self._type_dictionary.append_struct(type_name)
        return StructNode(self, data_type_node_id, type_name, added)

    def create_data_type(self, type_name, nodeid=None, init=True):
        return self._create_data_type(type_name, nodeid, init)

    def add_field(self, variable_name, data_type, struct_name, is_array=False):
        self._type_dictionary.add_field(variable_name, data_type, struct_name, is_array)

    def set_dict_byte_string(self):
        dict_node = self._server.get_node(self.dict_id)
        value = self._type_dictionary.get_dict_value()
        dict_node.set_value(value, ua.VariantType.ByteString)


class StructNode:

    def __init__(self, type_dict, data_type, name, node_ids):
        self._type_dict = type_dict
        self.data_type = data_type
        self.name = name
        self.node_ids = node_ids 

    def add_field(self, field_name, data_type, is_array=False):
        # nested structure could directly use simple structure as field
        if isinstance(data_type, StructNode):
            data_type = data_type.name
        self._type_dict.add_field(field_name, data_type, self.name, is_array)


def get_ua_class(ua_class_name):
    #return getattr(ua, _to_camel_case(ua_class_name))
    return getattr(ua, ua_class_name)
