import logging
from KL.attribute_ids import AttributeIds
from KL.types import NodeId, QualifiedName, NodeIdType
from KL.constants import ObjectIds
from KL.application_protocol import AddNodesItem, NodeClass, ReferenceDescription
from KL.hand_protocol import ObjectAttributes, VariableAttributes, MethodAttributes, DataTypeAttributes
import common.utils


logger = logging.getLogger(__name__)


def instantiate(parent, node_type, nodeid=None, bname=None, dname=None, idx=0, instantiate_optional=True):
    rdesc = _rdesc_from_node(parent, node_type)
    rdesc.TypeDefinition = node_type.nodeid
    from common.node import Node
    if nodeid is None:
        nodeid = NodeId(namespaceidx=idx)  # will trigger automatic node generation in namespace idx
    if bname is None:
        bname = rdesc.BrowseName
    elif isinstance(bname, str):
        bname = QualifiedName.from_string(bname)

    nodeids = _instantiate_node(
        parent.server,
        Node(parent.server, rdesc.NodeId),
        parent.nodeid,
        rdesc,
        nodeid,
        bname,
        dname=dname,
        instantiate_optional=instantiate_optional)
    return [Node(parent.server, nid) for nid in nodeids]



def _instantiate_node(server,
                      node_type,
                      parentid,
                      rdesc,
                      nodeid,
                      bname,
                      dname=None,
                      recursive=True,
                      instantiate_optional=True):
    """
    instantiate a node type under parent
    """
    addnode = AddNodesItem()
    addnode.RequestedNewNodeId = nodeid
    addnode.BrowseName = bname
    addnode.ParentNodeId = parentid
    addnode.ReferenceTypeId = rdesc.ReferenceTypeId
    addnode.TypeDefinition = rdesc.TypeDefinition

    if rdesc.NodeClass in (NodeClass.Object, NodeClass.ObjectType):
        addnode.NodeClass = NodeClass.Object
        _read_and_copy_attrs(node_type, ObjectAttributes(), addnode)

    elif rdesc.NodeClass in (NodeClass.Variable, NodeClass.VariableType):
        addnode.NodeClass = NodeClass.Variable
        _read_and_copy_attrs(node_type, VariableAttributes(), addnode)
    elif rdesc.NodeClass in (NodeClass.Method, ):
        addnode.NodeClass = NodeClass.Method
        _read_and_copy_attrs(node_type, MethodAttributes(), addnode)
    elif rdesc.NodeClass in (NodeClass.DataType, ):
        addnode.NodeClass = NodeClass.DataType
        _read_and_copy_attrs(node_type, DataTypeAttributes(), addnode)
    else:
        logger.error("Instantiate: Node class not supported: %s", rdesc.NodeClass)
        raise RuntimeError("Instantiate: Node class not supported")
        return
    if dname is not None:
        addnode.NodeAttributes.DisplayName = dname

    res = server.add_nodes([addnode])[0]
    res.StatusCode.check()
    added_nodes = [res.AddedNodeId]

    if recursive:
        from common.node import Node
        parents = utils.get_node_supertypes(node_type, includeitself=True)
        node = Node(server, res.AddedNodeId)
        for parent in parents:
            descs = parent.get_children_descriptions(includesubtypes=False)
            for c_rdesc in descs:
                # skip items that already exists, prefer the 'lowest' one in object hierarchy
                if not utils.is_child_present(node, c_rdesc.BrowseName):

                    c_node_type = Node(server, c_rdesc.NodeId)
                    refs = c_node_type.get_referenced_nodes(refs=ObjectIds.HasModellingRule)
                    if not refs:
                        # spec says to ignore nodes without modelling rules
                        logger.info("Instantiate: Skip node without modelling rule %s as part of %s", c_rdesc.BrowseName, addnode.BrowseName)
                        continue
                        # exclude nodes with optional ModellingRule if requested
                    if not instantiate_optional and refs[0].nodeid in (NodeId(ObjectIds.ModellingRule_Optional), NodeId(ObjectIds.ModellingRule_OptionalPlaceholder)):
                        logger.info("Instantiate: Skip optional node %s as part of %s", c_rdesc.BrowseName, addnode.BrowseName)
                        continue

                    # if root node being instantiated has a String NodeId, create the children with a String NodeId
                    if res.AddedNodeId.NodeIdType is NodeIdType.String:
                        inst_nodeid = res.AddedNodeId.Identifier + "." + c_rdesc.BrowseName.Name
                        nodeids = _instantiate_node(
                            server,
                            c_node_type,
                            res.AddedNodeId,
                            c_rdesc,
                            nodeid=NodeId(identifier=inst_nodeid, namespaceidx=res.AddedNodeId.NamespaceIndex),
                            bname=c_rdesc.BrowseName,
                            instantiate_optional=instantiate_optional)
                    else:
                        nodeids = _instantiate_node(
                            server,
                            c_node_type,
                            res.AddedNodeId,
                            c_rdesc,
                            nodeid=NodeId(namespaceidx=res.AddedNodeId.NamespaceIndex),
                            bname=c_rdesc.BrowseName,
                            instantiate_optional=instantiate_optional)
                    added_nodes.extend(nodeids)

    return added_nodes


def _rdesc_from_node(parent, node):
    results = node.get_attributes([AttributeIds.NodeClass, AttributeIds.BrowseName, AttributeIds.DisplayName])
    nclass, qname, dname = [res.Value.Value for res in results]

    rdesc = ReferenceDescription()
    rdesc.NodeId = node.nodeid
    rdesc.BrowseName = qname
    rdesc.DisplayName = dname
    rdesc.NodeClass = nclass
    if parent.get_type_definition() == NodeId(ObjectIds.FolderType):
        rdesc.ReferenceTypeId = NodeId(ObjectIds.Organizes)
    else:
        rdesc.ReferenceTypeId = NodeId(ObjectIds.HasComponent)
    typedef = node.get_type_definition()
    if typedef:
        rdesc.TypeDefinition = typedef
    return rdesc


def _read_and_copy_attrs(node_type, struct, addnode):
    names = [name for name in struct.__dict__.keys() if not name.startswith("_") and name not in ("BodyLength", "TypeId", "SpecifiedAttributes", "Encoding", "IsAbstract", "EventNotifier")]
    attrs = [getattr(AttributeIds, name) for name in names]            
    for name in names:
        results = node_type.get_attributes(attrs)
    for idx, name in enumerate(names):
        if results[idx].StatusCode.is_good():
            if name == "Value":
                setattr(struct, name, results[idx].Value)
            else:
                setattr(struct, name, results[idx].Value.Value)
        else:
            logger.warning("Instantiate: while copying attributes from node type {0!s}, attribute {1!s}, statuscode is {2!s}".format(node_type, name, results[idx].StatusCode))            
    addnode.NodeAttributes = struct