"""

Devices controlled my the ISY are represented as "nodes" on the ISY device and with Node Objects in the API

There are three types of Node Object:

    * IsyNode - Node Object
        Represent lights, switches, motion sensors
    * IsyScene - Scene Object
        Represents Scenes contains Nodes that comprise a "Scene"
    * IsyNodeFolder - Can hold Scene's or Nodes
        a organizational obj for Scene's and Nodes

Only IsyNode Objects maintain "state"

    What states are maintined depend on the physical node device itself
    but they can include
        - on, off of dim level
        - temperature
        - wattage

    Nodes can have "members" or subnodes

IsyScene Objects can take commands but do not maintin a queryable state

    A Scene is predefined state for one or more nodes
    scenes can only be comprised of nodes which are call "members"

    only nodes can be members of a scene

IsyNodeFolders are just for organizing

    Nodes, Scenes and Folders can be members of a Folder



"""

__author__ = 'Peter Shipley <peter.shipley@gmail.com>'
__copyright__ = "Copyright (C) 2015 Peter Shipley"
__license__ = "BSD"

import hashlib

from ISY.IsyUtilClass import IsySubClass, val2bool
from ISY.IsyExceptionClass import *
# from IsyClass import *
# from IsyNodeClass import *
# from IsyProgramClass import *
# from IsyVarClass import *

__all__ = ['IsyNode', 'IsyNodeFolder', 'IsyScene']


# library_using_super

class _IsyNodeBase(IsySubClass):

    #_objtype = (0, "unknown")
    _objtype = "unknown"

    def on(self, val=255) :
        """ Send On command to a node

            args:
                optional value for on level

        """
        self._on(val, "DON")

    def faston(self, val=255) :
        """ Send Fast On command to a node

            args:
                optional value for on level

        """
        self._on(val, "DFON")

    def _on(self, val, cmd) :
        if not str(val).isdigit :
            raise IsyTypeError("On Command : Bad Value : node=%s val=%s" %
                    self._mydict["address"], str(val))

        if "property" in self._mydict :
            if "ST" in  self._mydict["property"] :
                self._mydict["property"]["ST"]["value"] = val
                self._mydict["property"]["ST"]["formatted"] = "{:.0%}".format(val/255)

        self.isy._node_send(self._mydict["address"], "cmd", cmd, val)


    def off(self) :
        """ Send Off command to a node

            args: None

        """
        self._off("DOF")


    def fastoff(self) :
        """ Send Fast Off command to a node

            args: None

        """
        self._off("DFOF")


    def _off(self, cmd="DOF") :
        self.isy._node_send(self._mydict["address"], "cmd", cmd)
        if "property" in self._mydict :
            # self._mydict["property"]["time"] = 0
            if "ST" in  self._mydict["property"] :
                self._mydict["property"]["ST"]["value"] = 0
                self._mydict["property"]["ST"]["formatted"] = "Off"

    def beep(self) :
        self.isy._node_send(self._mydict["address"], "cmd", "BEEP")


    def get_spoken(self):
        """ get notes property 'spoken' """
        return self._get_prop("spoken")
    spoken = property(get_spoken)

    def get_path(self):
        return self.isy._node_get_path(self._mydict['address'], self._objtype)
    path = property(get_path)

    def members_list(self) :
        pass

    def member_iter(self, flag=0):
        return self.members_list()

    def member_list(self):
        if 'members' in self._mydict :
            # print("mydict['members'] : ", type(self._mydict['members']) )
            if type(self._mydict['members']) == 'dict' :
                return self._mydict['members'].keys()
            # if type(self._mydict['members']) == 'list' :
            return self._mydict['members'][:]
        return [ ]

    def is_dimable(self) :
        if 'type' in self._mydict :
            a = self._mydict["type"].split('.')
            if a[0] == "1" :
                return True
        return False
    dimable = property(is_dimable)


    def get_callback(self) :
        return self.isy.callback_get(self._mydict["address"])
    def set_callback(self, func, *args) :
        if func is None :
            return self.isy.callback_del(self._mydict["address"])
        else :
            return self.isy.callback_set(self._mydict["address"], func, args)
    callback = property(get_callback, set_callback)


    def is_member(self, obj) :
        if "members" in self._mydict :
            if isinstance(obj, str)  :
                return obj in self._mydict["members"]
            elif isinstance(obj, _IsyNodeBase)  :
                return obj._get_prop("address") in self._mydict["members"]
        return False

    def member_add(self, node, flag=0) :
        r = self.isy.soapcomm("SetParent",
                node=node._get_prop("address"), nodeType=node.nodeType(),
                parent=self._mydict["address"], parentType=self.nodeType())

    def _rename(self, cmd, newname) :
        if self.debug & 0x01 :
            print("rename : ", self.__class__.__name__, " : ", newname)
        #if not isinstance(newname, str) or len(newname) == 0 :
        #    print "newname : ", newname
        #    raise IsyTypeError("rename : name value not str")
        r = self.isy.soapcomm(cmd,
                        id=self._mydict["address"], name=newname )

        return r

    # check if scene _contains_ node
    def __contains__(self, other):
            return self.is_member(other)


    # check if obj _contains_  attib
#    def __contains__(self, other):
#       if isinstance(other, str)  :
#           return other in self._getlist
#       else :
#           return False


#    class MemberDicte(dict):
#
#       def __getitem__(self, key):
#           val = dict.__getitem__(self, key)
#           print 'GET', key
#           return val
#
#       def __setitem__(self, key, val):
#           print 'SET', key, val
#           dict.__setitem__(self, key, val)
#
#       def __delitem__(self, key):
#           print 'DEL', key
#           dict.__delitem__(self, key)
#
#       def __repr__(self):
#           dictrepr = dict.__repr__(self)
#           return '%s(%s)' % (type(self).__name__, dictrepr)
#
#       def get(self, key, default_val):
#           print 'GET', key, default_val
#           dict.get(self, key, default_val)
#
#       def update(self, *args, **kwargs):
#           print 'update', args, kwargs
#           for k, v in dict(*args, **kwargs).iteritems():
#               self[k] = v



#
# convers a node Id  to a int
# eg: "9 4A 5F 2" => 00001001010010100101111100000010 => 155868930
#
def node_id_to_int(h) :
    a = h.split(' ')
    return  ( int(a[0], 16) << 24 ) | ( int(a[1], 16) << 16 ) | \
                    ( int(a[2], 16) << 8 ) | int(a[3], 16)



# def rate
# def onlevel
class IsyNode(_IsyNodeBase):
    """ Node Class for ISY

        Attributes :
            status / ST
            ramprate / RR
            onlevel / OL

        Readonly Attributes :
            address
            formatted
            enabled
            pnode
            type
            name
            ELK_ID
            flag

        funtions:
            get_rr:
            set_rr:

    Bugs: Results are undefined for Node class objects that
            represent a deleteed node

    """
    _getlist = ['address', 'enabled', 'formatted',
            'ELK_ID',
            'parent', 'parent-type',
            'name', 'pnode', 'flag', 'wattage',
            'isLoad', 'location', 'description', 'spoken',
            'OL', 'RR', 'ST', 'type']
    _setlist = ['RR', 'OL', 'status', 'ramprate', 'onlevel', 'enable']
    _propalias = {'status': 'ST', 'value': 'ST', 'val': 'ST',
            'id': 'address', 'addr': 'address',
            'ramprate': 'RR', 'onlevel': 'OL',
            "node-flag": "flag"}
    #_boollist = [ "enabled" ]

    def __init__(self, isy, ndict) :

        # self._objtype = (1, "node")
        self._objtype = "node"

        self._nodeprops = None

        super(self.__class__, self).__init__(isy, ndict)

#        if not self.isy.eventupdates :
#            #update only nodes
#            if "node-flag" in self._mydict :
#                self.update()

        self._hash = hashlib.sha256(self._mydict["address"])

        if self.debug & 0x01 :
            print("Init Node : \"" + self._mydict["address"] + \
                "\" : \"" + self._mydict["name"] + "\"")
            # self.isy._printdict(self.__dict__)


    # Special case from BaseClass due to ST/RR/OL props
    def _get_prop(self, prop):

        # print "IN get_prop ", prop

        if prop == "formatted" :
            prop = "ST"
            value = "formatted"
        else :
            value = "value"

        if prop in self._propalias :
            prop = self._propalias[prop]

        if not prop in self._getlist :
#           if prop in ['parent', 'parent-type'] :
#               return None
            raise IsyPropertyError("no property Attribute {!s}".format(prop))

        # check if we have a property

        if prop in ['isLoad', 'location', 'description', 'spoken'] :
            if self._nodeprops is None :
                self._nodenotes = self.isy.node_get_notes(self._mydict["address"])
            if self._nodenotes is None :
                return None
            if prop in self._nodenotes :
                return self._nodenotes[prop]
            else :
                # return None
                return ""


        if prop in ['ST', 'OL', 'RR'] :
            # Scene's do not have property values

            if prop in self._mydict["property"] :
                # print self._mydict["property"]
                # print "prop value", prop, value
                return self._mydict["property"][prop][value]
            else :
                return None

#            if self._mydict["property"]["time"] == 0 :
#                    self.update()
#            elif self.isy.cachetime :
#                if time.gmtime() < (self.cachetime + self._mydict["property"]["time"]) :
#                    self.update()

        else :

#            if prop in self._mydict :
#               if prop in self._boollist :
#                   return(val2bool(self._mydict[prop]))
#               else :
#                   return self._mydict[prop]
#            else :
#                return None

            return super(self.__class__, self)._get_prop(prop)

    def _set_prop(self, prop, new_value):
        """  generic property set """
        # print "IN set_prop ", prop, new_value
        if self.debug & 0x04 :
            print("_set_prop ", prop, " : ", new_value)

        if prop in self._propalias :
            prop = self._propalias[prop]

        if not prop in self._setlist :
            if prop == "ST" :
                self.on(new_value)
                return
            else :
                raise IsyPropertyError("_set_prop : " \
                    "Invalid property Attribute " + prop)

        if prop == 'enable' :
            self._mydict[prop] = bool(new_value)
            self.isy.node_enable(self._mydict["address"], bool(new_value))

        elif prop in ['OL', 'RR'] :
            if not str(new_value).isdigit :
                raise IsyTypeError("Set Property : Bad Value : node=%s prop=%s val=%s" %
                            self._mydict["address"], prop, str(new_value))


            self.isy._node_send(self._mydict["address"], "set", prop, str(new_value))

            # self._mydict["property"]["time"] = 0

            if prop in self._mydict["property"] :
                # if isinstance(new_value, (int, float))  :  # already checked with isdigit
                self._mydict["property"][prop]["value"] = new_value

        # we need to tie this to some action
        elif prop in self._mydict :
            # self._mydict[prop] = new_value
            pass
        else :
            #print "_set_prop AttributeError"
            raise AttributeError("no Attribute " + prop)


    def _gettype(self):
        """  Type of Node (readonly) """
        return "node"

    # enable node
    def get_enable(self):
        """ get enable/disable status a node """
        return self._get_prop("enable")

    def set_enable(self, new_bool):
        """ Set enable status a node

            args:
                enable bool
        """
        return self._set_prop("enable", new_bool)

    enable = property(get_enable, set_enable, None, "enable/disable a node")

    def get_wattage(self):
        """ get wattage """
        return self._get_prop("wattage")

    def set_wattage(self, watts):
        """ set wattage property """
        return self.isy.node_set_powerinfo(self._mydict["address"], wattage=watts)
    wattage = property(get_wattage, set_wattage)




    # ramprate property
    # obj mathod for getting/setting a Node's value
    # sets how fast a light fades on.
    def get_rr(self):
        """ Get/Set RampRate property of Node """
        return self._get_prop("RR")

    def set_rr(self, new_value):
        """ Get/Set RampRate property of Node """
        return self._set_prop("RR", new_value)

    ramprate = property(get_rr, set_rr)

    # On Level property
    # obj mathod for getting/setting a Node's value
    # where in most cases light is how bright the light is
    # when turned on
    def get_ol(self):
        """ Get/Set On Level property of Node """
        return self._get_prop("OL")

    def set_ol(self, new_value):
        """ Get/Set On Level property of Node """
        return self._set_prop("OL", new_value)
    onlevel = property(get_ol, set_ol)


#    def get_fm(self):
#       """ property On Level Value of Node """
#        return self._get_prop("formatted")
#    formatted = property(get_fm)

    # status property
    # obj mathod for getting/setting a Node's value
    # where in most cases light is how bright the light is
    def get_status(self):
        """ Get/Set Status property of Node """
        return self._get_prop("ST")
    def set_status(self, new_value):
        """ Get/Set Status property of Node """
        return self.on(new_value)
    status = property(get_status, set_status)


    def dim(self) :
        """
            decrease brightness of a device by ~3%
        """
        self.isy._node_send(self._mydict["address"], "cmd", "DIM")

    def brighten(self) :
        """
            increase brightness of a device by ~3%
        """
        self.isy._node_send(self._mydict["address"], "cmd", "BRT")

    #
    # readonly to node attribute
    #



    def rename(self, newname) :
        return  self._rename("RenameNode", newname)


    #
    #
    #
    def update(self) :
        """ force object to manualy update it's propertys """
        xurl = "/rest/nodes/" + self._mydict["address"]
        if self.debug & 0x01 :
            print("_updatenode pre _getXML")
        _nodestat = self.isy._getXMLetree(xurl)
        # del self._mydict["property"]["ST"]
        for prop in _nodestat.iter('property'):
            tprop = dict()
            for k, v in list(prop.items()) :
                tprop[k] = v
            if "id" in tprop :
                self._mydict["property"][tprop["id"]] = tprop
        # self._mydict["property"]["time"] = time.gmtime()

    # experimental
    def __bool__(self) :
        #print "__nonzero__ call", self._mydict["property"]["ST"]["value"], \
        #        " :: ", int(self._mydict["property"]["ST"]["value"])
        return(bool(self._mydict["property"]["ST"]["value"]) > 0)

    # use the node address as the hash value
    def __hash__(self) :
        return(self._hash)


#    def __str__(self):
#       print "__str__ call"
#       return("my str : " + self._mydict["name"])

    def __float__(self):
        # print "__float__ call"
        return float(int(self._mydict["property"]["ST"]["value"]) / float(255))

class IsyScene(_IsyNodeBase):
    """ Node Group Class for ISY

        writeonly attributes :
            status

        readonly attributes :
            address
            name
            flag
            deviceGroup
            parent
            parent-type
            ELK_ID
    """
    _getlist = ['address', 'name', "ELK_ID", "deviceGroup",
                'flag', 'parent', 'parent-type']
    _setlist = []
    _propalias = {'id': 'address', 'addr': 'address',
                    "group-flag": "flag"}

    def __init__(self, *args):
        #self._objtype = (2, "scene")
        self._objtype = "scene"
        super(self.__class__, self).__init__(*args)

    # status property
    # obj mathod for getting/setting a  Scene's value
    # where in most cases light is how bright the light is
    def set_status(self, new_value):
        """ set status value of Scene """
        return self._set_prop("ST", new_value)

    status = property(None, set_status)


    def _getmembers(self) :
        """ List members of a scene or group """
        if "members" in self._mydict :
            return self._mydict["members"].keys()
        else :
            return None
    members = property(_getmembers)

    def member_list(self) :
        return self._getmembers()

    def is_member(self, obj) :
        if "members" in self._mydict :
            if isinstance(obj, str)  :
                return obj in self._mydict["members"]
            elif isinstance(obj, _IsyNodeBase)  :
                return obj._get_prop("address") in self._mydict["members"]
        return False

    def rename(self, newname) :
        """ rename node/scene/folder """
        return  self._rename("RenameGroup", newname)

    def member_del(self, node) :
        r = self.isy.scene_del_node(
                self._mydict["address"],
                node)
#       r = self.isy.soapcomm("RemoveFromGroup",
#               node=node._get_prop("address"),
#               group=self._mydict["address"])
        return r

    def member_add_controler(self, node, flag=16) :
        """ Add Node to scene/group as Responder """
        return self.member_add(node, flag)

    def member_add_responder(self, node, flag=32) :
        """ Add Node to scene/group Controller """
        return self.member_add(node, flag)

    def member_add(self, node, flag=16) :
        """ Add Node to scene/group """
        r = self.isy.scene_add_node(
                self._mydict["address"],
                node,
                flag=0x10)
#       r = self.isy.soapcomm("MoveNode",
#               node=node._get_prop("address"),
#               group=self._mydict["address"],
#               flag=16)
        return r

    def member_iter(self, flag=0):
        """ iter though members
            Folders iter though their contents (nodes/scenes/folders)
            Scene iter though their members     (nodes)
            Nodes iter though sub-nodes         (nodes)
        """
        if "members" in self._mydict :
            for k in list(self._mydict["members"].keys()) :
                if flag and not(flag & self._mydict["members"][k]) :
                    continue
                else :
                    yield k

    def __iter__(self):
        return self.member_iter()

    # check if scene _contains_ node
    def __contains__(self, other):
            return self.is_member(other)



class IsyNodeFolder(_IsyNodeBase):
    """ Node Folder Class for ISY

        readonly attributes :
            address
            name
            flag
    """
    _getlist = ['address', 'name', 'flag']
    _setlist = []
    _propalias = {'id': 'address', 'addr': 'address', "folder-flag": "flag"}

    def __init__(self, *args):
        #self._objtype = (3, "folder")
        self._objtype = "folder"
        super(self.__class__, self).__init__(*args)

    def member_add(self, node, flag=0) :
        """ add Node/Scene or Folder to Folder Obj

                 Args:
                    node = address, name or Node/Scene/Folder Obj

              sets Parent for node/scene/folder to current Obj Folder

            calls SOAP SetParent()
        """
        r = self.isy.soapcomm("SetParent",
                node=node._get_prop("address"), nodeType=node.nodeType(),
                parent=self._mydict["address"], parentType=self.nodeType())
        return r

    def member_del(self, node) :
        """ del Node/Scene or Folder to Folder Obj

                 Args:
                    node = address, name or Node/Scene/Folder Obj

              del node/scene/folder to current Obj Folder
              (and moves to base folder)

            calls SOAP SetParent()
        """
        r = self.isy.soapcomm("SetParent",
                node=node._get_prop("address"), nodeType=node.nodeType())
        return r

    def rename(self, newname) :
        """ renames current Obj Folder

            args :
                name = new folder name

            calls SOAP RenameFolder()
        """
        return self._rename("RenameFolder", newname)


    def __iter__(self):
        return self.member_iter()

    def __contains__(self, other):
        pass

#
# Do nothing
# (syntax check)
#
if __name__ == "__main__":
    import __main__
    print(__main__.__file__)
    print("syntax ok")
    exit(0)
