from KL.constants import ObjectIds
from KL.types import *
from KL.attribute_ids import AttributeIds
from datetime import datetime
from KL.application_protocol import ReadValueId, ReadParameters, BrowseDirection, NodeClass, BrowseNextParameters, \
    BrowseDescription, BrowseParameters, BrowseResultMask
from common import manage_nodes


def _to_nodeid(nodeid):
    if isinstance(nodeid, int):
        return TwoByteNodeId(nodeid)
    elif isinstance(nodeid, Node):
        return nodeid.nodeid
    elif isinstance(nodeid, NodeId):
        return nodeid
    elif type(nodeid) in (str, bytes):
        return NodeId.from_string(nodeid)
    else:
        raise Exception("Could not resolve '{0}' to a type id".format(nodeid))


class Node(object):
    """
    High level node object, to access node attribute,
    browse and populate address space.
    Node objects are usefull as-is but they do not expose the entire
    OPC-UA protocol. Feel free to look at the code of this class and call
    directly UA services methods to optimize your code
    """

    def __init__(self, server, nodeid):
        self.server = server
        self.nodeid = None
        if isinstance(nodeid, Node):
            self.nodeid = nodeid.nodeid
        elif isinstance(nodeid, NodeId):
            self.nodeid = nodeid
        elif type(nodeid) in (str, bytes):
            self.nodeid = NodeId.from_string(nodeid)
        elif isinstance(nodeid, int):
            self.nodeid = NodeId(nodeid, 0)
        else:
            raise Exception(
                "argument to node must be a NodeId object or a string defining a nodeid found {0} of type {1}".format(
                    nodeid, type(nodeid)))
        self.basenodeid = None

    def __eq__(self, other):
        if isinstance(other, Node) and self.nodeid == other.nodeid:
            return True
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def __str__(self):
        return self.nodeid.to_string()

    def __repr__(self):
        return "Node({0})".format(self.nodeid)

    def __hash__(self):
        return self.nodeid.__hash__()

    def set_value(self, value, varianttype=None):

        datavalue = None
        if isinstance(value, DataValue):
            datavalue = value
        elif isinstance(value, Variant):
            datavalue = DataValue(value)
            datavalue.SourceTimestamp = datetime.utcnow()
        else:
            datavalue = DataValue(Variant(value, varianttype))
            datavalue.SourceTimestamp = datetime.utcnow()
        self.set_attribute(AttributeIds.Value, datavalue)

    set_data_value = set_value

    def get_value(self):
        """
        Get value of a node as a python type. Only variables ( and properties) have values.
        An exception will be generated for other node types.
        WARNING: on server side, this function returns a ref to object in ua database. Do not modify it if it is a mutable
        object unless you know what you are doing
        """
        result = self.get_data_value()
        return result.Value.Value

    def set_attribute(self, attributeid, datavalue):
        """
        Set an attribute of a node
        attributeid is a member of AttributeIds
        datavalue is a DataValue object
        """
        attr = WriteValue()
        attr.NodeId = self.nodeid
        attr.AttributeId = attributeid
        attr.Value = datavalue
        params = WriteParameters()
        params.NodesToWrite = [attr]
        result = self.server.write(params)
        result[0].check()

    def get_data_value(self):
        """
        Get value of a node as a DataValue object. Only variables (and properties) have values.
        An exception will be generated for other node types.
        DataValue contain a variable value as a variant as well as server and source timestamps
        """
        return self.get_attribute(AttributeIds.Value)

    def get_attribute(self, attr):
        """
        Read one attribute of a node
        result code from server is checked and an exception is raised in case of error
        """
        rv = ReadValueId()
        rv.NodeId = self.nodeid
        rv.AttributeId = attr
        params = ReadParameters()
        params.NodesToRead.append(rv)
        result = self.server.read(params)
        result[0].StatusCode.check()
        return result[0]

    def add_object(self, nodeid, bname, objecttype=None):
        return manage_nodes.create_object(self, nodeid, bname, objecttype)

    def add_variable(self, nodeid, bname, val, varianttype=None, datatype=None):
        return manage_nodes.create_variable(self, nodeid, bname, val, varianttype, datatype)

    def get_type_definition(self):
        references = self.get_references(refs=ObjectIds.HasTypeDefinition, direction=BrowseDirection.Forward)
        if len(references) == 0:
            return None
        return references[0].NodeId

    def get_references(self, refs=ObjectIds.References, direction=BrowseDirection.Both,
                       nodeclassmask=NodeClass.Unspecified, includesubtypes=True):

        desc = BrowseDescription()
        desc.BrowseDirection = direction
        desc.ReferenceTypeId = _to_nodeid(refs)
        desc.IncludeSubtypes = includesubtypes
        desc.NodeClassMask = nodeclassmask
        desc.ResultMask = BrowseResultMask.All

        desc.NodeId = self.nodeid
        params = BrowseParameters()
        params.View.Timestamp = get_win_epoch()
        params.NodesToBrowse.append(desc)
        params.RequestedMaxReferencesPerNode = 0
        results = self.server.browse(params)

        references = self._browse_next(results)
        return references

    def _browse_next(self, results):
        references = results[0].References
        while results[0].ContinuationPoint:
            params = BrowseNextParameters()
            params.ContinuationPoints = [results[0].ContinuationPoint]
            params.ReleaseContinuationPoints = False
            results = self.server.browse_next(params)
            references.extend(results[0].References)
        return references

    def get_referenced_nodes(self, refs=ObjectIds.References, direction=BrowseDirection.Both,
                             nodeclassmask=NodeClass.Unspecified, includesubtypes=True):
        """
        returns referenced nodes based on specific filter
        Paramters are the same as for get_references

        """
        references = self.get_references(refs, direction, nodeclassmask, includesubtypes)
        nodes = []
        for desc in references:
            node = Node(self.server, desc.NodeId)
            nodes.append(node)
        return nodes

    def get_children_descriptions(self, refs=ObjectIds.HierarchicalReferences, nodeclassmask=NodeClass.Unspecified,
                                  includesubtypes=True):
        return self.get_references(refs, BrowseDirection.Forward, nodeclassmask, includesubtypes)
