# Registry.py  14/05/2016  D.J.Whale
#
# A simple registry of connected devices.
#
# NOTE: This is an initial, non persisted implementation only
from lifecycle import *
import time
try:
    import Devices # python 2
except ImportError:
    from . import Devices # python 3
directory = {}
@deprecated
def allkeys(d):
    result = ""
    for k in d:
        if len(result) != 0:
            result += ','
        result += str(k)
    return result
@deprecated
def update(message):
    """Update the local directory with information about this device"""
    now      = time.time()
    header   = message["header"]
    sensorId = header["sensorid"]
    if not (sensorId in directory):
        # new device discovered
        desc = Devices.getDescription(header["mfrid"], header["productid"])
        print("ADD device:%s %s" % (hex(sensorId), desc))
        directory[sensorId] = {"header": message["header"]}
        #trace(allkeys(directory))
    directory[sensorId]["time"] = now
    #TODO would be good to keep recs, but need to iterate through all and key by paramid,
    #not as a list index, else merging will be hard.
@deprecated
def size():
    return len(directory)
@deprecated
def get_sensorids():
    return directory.keys()
@deprecated
def get_info(sensor_id):
    return directory[sensor_id]
#----- NEW DEVICE REGISTRY ----------------------------------------------------
# Done as a class, so we can have multiple registries if we want.
# TODO: file format? platform dependent database format, like dbm but there is
# a platform dependent one - but need the licence to be MIT so we can
# just embed it here to have zero dependencies.
# TODO: serialisation format for the individual device meta record? json?
class RegistryStore(): # This is data storage, so it it just the 'RegistRY'??
    """A mock in-memory only store, for testing and debugging"""
    def __init__(self, filename):
        self.filename = filename #TODO: Intentionally not used elsewhere
        self.store = {}
    #@log_method
    def __setitem__(self, key, value):
        self.store[key] = value
    #@log_method
    def __getitem__(self, key):
        return self.store[key]
    #@log_method
    def __delitem__(self, key):
        del self.store[key]
    #@log_method
    def keys(self):
        return self.store.keys()
    def size(self):
        return len(self.store)
class DeviceRegistry(): # this is actions, so is this the 'RegistRAR'??
    """A persistent registry for device class instance configurations"""
    def __init__(self, filename):
        self.store = RegistryStore(filename) # A dummy store, for testing
    @unimplemented
    def load(self):
        pass
        # load registry from disk/parse it
    @unimplemented
    def save(self):
        pass
        # persist registry to disk/write back new entries
    def add(self, device, name):
        """Add a device class instance to the registry, with a friendly name"""
        self.store[name] = device
    def get(self, name): # -> Device
        """Get the description for a device class from the store, and construct a class instance"""
        c = self.store[name]
        #TODO: Construct a new device class that is configured as per this data
        #for now, just return the RAM class instance as it is mocked
        #TODO: work out what type of device it is (fsk, ook)
        #TODO: decide which router to use for incoming messages
        #TODO: configure the message router so it will route to the device class instance for us
        #TODO: at what point is the payload decoded to get the address out?
        # OpenThings.decode() or TwoBit.decode() - where do those protocol handlers go in the pipeline?
        return c
    def delete(self, name):
        """Delete the named class instance"""
        del self.store[name]
        #TODO: delete from the RAM route too.
    def auto_create(self, context):
        """auto-create variables in the provided context, for all persisted registry entries"""
        if context == None:
            raise ValueError("Must provide a context to hold new variables")
        for name in self.store.keys():
            c = self.get(name) #TODO: should return an instantiated class
            # This creates a variable inside the context of this name, points to class instance
            setattr(context, name, c)
    def list(self):
        """List the registry in a vaguely printable format, mostly for debug"""
        for k in self.store.keys():
            print("DEVICE %s" % k)
            print("  %s" % self.store[k])
    def size(self):
        """How many entries are there in the registry?"""
        return self.store.size()
    def devices(self):
        """Get a list of all device classes in the registry"""
        #TODO: Temporary method until we read up about iterable, so we can say
        # for devices in energenie.registry
        dl = []
        for k in self.store.keys():
            d = self.store[k]
            dl.append(d)
        return dl
registry = DeviceRegistry("registry.txt")
# This will create all class instance variables in the module that imports the registry.
# So, if there is an entry called "tv" in the registry, then the app module
# will get a variable called tv that is bound to the appropriate device instance.
# You can then just say tv.turn_on() regardless of the type of device it is, as long
# as it has switching capability.
#
# usage:
#   import sys
#   from Registry import registry
#   registry.auto_create(sys.modules[__file__])
#----- DISCOVERY AND LEARNING -------------------------------------------------
#5. LEARN/DISCOVER: To be able to instigate and manage learn mode from within an app
#
#   a. To send specific commands to green button devices so they can
#      learn the pattern
#   ? broadcast specific (house_code, index) repeatedly
#   ? user assisted start/stop
#   b. To sniff for any messages from MiHome devices and capture them
#      for later analysis and turning into device objects
#   ? either as a special receive-only learn mode
#   ? or as part of normal receive operation through routing unknown device id's
#   ? need a way to take a device id and consult active directory list,
#     and route to the correct class instance - a router for incoming messages
#   This means we need an incoming message 'router' with a message pump
#   that the app can call - whenever it is in receive, does a peek and
#   if there is a message, it knows what modulaton scheme is in use
#   so can route the message with (modulation, payload)
#   c. To process MiHome join requests, and send MiHome join acks
#   ? this would be routed by address to the device class
#   This also needs the message pump
#----- MESSAGE ROUTER ---------------------------------------------------------
# a handler that is called whenever a message is received.
# routes it to the correct handling device class instance
# or instigates the unknown handler
# consults a RAM copy of part of the registry
# from mfrid,productid,sensorid -> handler
# The RAM copy is a routing table
# it must be updated whenever a factory returns a device class instance.
# Note, if you have a device class instance that is not registered,
# this means it cannot receive messages unless you pass them to it yourself.
# That's fine?
# might be one for OOK devices, a different one for FSK devices
# as they have different keying rules. OOK receive will only probably
# occur from another raspberry pi, or from a hand controller or MiHome hub.
# But it is possible to OOK receive a payload, it only has a house address
# and 4 index bits in it and no data, but those are routeable.
class Router():
    def __init__(self, name):
        self.name = name # probably FSK or OOK
        self.routes = {} # key(tuple of ids) -> value(device class instance)
        self.unknown_cb = None
        self.incoming_cb = None
    def add(self, address, instance):
        """Add this device instance to the routing table"""
        # When a message comes in for this address, it will be routed to its handle_message() method
        # address might be a string, a number, a tuple, but probably always the same for any one router
        self.routes[address] = instance
    def incoming_message(self, address, payload):
        if self.incoming_cb != None:
            self.incoming_cb(address, payload)
        if address in self.routes:
            ci = self.routes[address]
            ci.incoming_message(payload)
        else: # unknown address
            self.handle_unknown(address, payload)
    def when_incoming(self, callback):
        self.incoming_cb = callback
    def when_unknown(self, callback):
        """Register a callback for unknown messages"""
        #NOTE: this is the main hook point for auto discovery and registration
        self.unknown_cb = callback
    def handle_unknown(self, address, payload):
        if self.unknown_cb != None:
            self.unknown_cb(address, payload)
        else:
            # Default action is just a debug message, and drop the payload
            print("Unknown address: %s" % str(address))
# Might rename these, especially when we add in other protocols
# such as devices that are 868 wirefree doorbells etc.
#TODO: Name is not completely representative of function.
# This is the Energenie 433.92MHz with OpenThings
fsk_router = Router("fsk")
#OOK receive not yet written
#It will be used to be able to learn codes from Energenie legacy hand remotes
##ook_router = Router("ook")
# END