import collections
import traceback
from typing import Any, Callable, Dict, Optional, Union
import logging
import time
import json
import copy

# example device_manifest.json file that should be in any module declaring devices. keys are device type names.


# submodule values are the submodule you would have to import to get access to that device type. blank is the root.
# easy to read manually!
"""
"devices": {
  "Device":{
    "submodule": ""
    "description": "Device base clas
  }
}
"""

#Gonna overwrite these functions insude functions...
minimum = min
maximum = max

class Device():
    """represents exactly one "device".   
    should not be used to represent an interface to a large collection, use
    one instance per device.
    

    Note that this is meant to be subclassed twice.  Once by the actual driver, and again by the 
    host application, to detemine how to handle calls made by the driver.

    """


    # this name must be the same as the name of the device itself
    device_type: str="Device"
    default_config={}

    # Iterable of config keys that should be considered secret, and hidden behind asterisks and such.
    config_secrets={}

    def __init__(self, name: str, config: Dict[str, str],**kw):
        """ 

        Attributes:

            config:
                The current configuration of the device

            config_properties:
                For each key in config, there MAY be an option here that can contain any of these optional keys.

                secret:
                    Denotes that the key must be protected from shoulder surfing

                description:
                    Free text
                    
                type:
                    Values may be

                    bool:
                        'yes', 'true', or 'enable' should represent true, with yes being preferred
                    
                    local_fs_path:
                        String is a path on the same folder as the device
        Args:
            name: must be a special ch
ar free string.  
                It may contain slashes, for compatibility with hosts using that for heirarchy
       
            config: must contain a name field, and must contain a type field matching the device type name.
                All other fields must be optional, and a blank unconfigured device should be creatable.  
 
               The device should set it's own missing fields for use as a template 
                
                Options starting with temp. are reserved for device specific things that should not actually be saved.
                Options endning with __ are used to add additional fields with special meaning.  Don't use these!


                All your device-specific options should begin with device.
        """

        config=copy.deepcopy(config)

        if config.get("type",self.device_type) != self.device_type:
            raise ValueError("This config does not match this class type:"+str((config['type'], self,type)))
    

        # Raise error on bad data.
        json.dumps(config)

        self.config: Dict[str, str] = config
        self.__datapointhandlers: Dict[str, Callable] = {}
        self.datapoints = {}

        # Used to store properties about config keys
        self.config_properties: Dict[str, Dict[str:Any]]= {}

        #Functions that can be called to explicitly request a data point
        #That return the new value
        self.__datapoint_getters: Dict[str, Callable] = {}

        for i in self.default_config:
            if not i in self.config:
                self.set_config_option(i,self.default_config[i])

        self.name=name
        if 'name' in self.config:
            if not self.config['name']== name:
                raise ValueError("Nonmatching name")
            name=self.name = config['name']
        else:
            self.set_config_option('name', name)
        

    @staticmethod
    def discover_devices(config:Dict[str, str] = {}, current_device: Optional[object]=None, intent="", **kwargs) -> Dict[str, Dict]:
        """ 
        Discover a set of suggested configs that could be used to build a new device.        
        
        Not required to be implemented and may just return {}  

        ***********************
        Discovered suggestions MUST NOT have any passwords or secrets if the suggestion would cause them to be tried somewhere
        other than what the user provided them for, unless the protocol does not actually reveal the secrets to the server.
        
        You do not want to autosuggest trying the same credentials at bad.com that the user gave for example.com.

    
        The suggested UI semantics for discover commands is "Add a similar device" and "Reconfigure this device".
        
        Reconfiguration should always be available as the user might always want to take an existing device object and
        swap out the actual physical device it connects to.

        Kwargs is reserved for further hints on what kinds of devices should be discovered.

        Args:
            config: You may pass a partial config, or a completed config to find other
                similar devices. The device should reuse as much of the given config as possible and logical,
                discarding anything that wouldn't  work with the selected device.

            current_device: May be set to the current version of a device, if it is being used in a UI along the lines of
                suggesting how to further set up a partly configured device, or suggesting ways to add another similar device.

            kwargs: is reserved for further hints on what kinds of devices should be discovered.

        
            intent: may be a hint as to what kind of config you are looking for.  
                If it is "new", that means the host wants to add another
                similar device.  If it is "replace", the host wants to keep the same config 
                but point at a different physical device.  If it is
                "configure",  the host wants to look for alternate configurations available for the same exact device.

                If it is "step", the user wants to refine the existing config.

        Returns:
            A dict of device data dicts that could be used to create a new device, indexed by a descriptive name.

    
        
        UI:

      

        """

        return {}

    def set_config_option(self, key: str, value: str):
        """sets an option in self.config. used for subclassing as you may want to persist.
        __init__ will automatically set the state.  this is used by the device itself to set it's 
        own persistent values at runtime, perhaps in response to a websocket message.

        __init__ will automatically set the state when passed the config dict, you don't have to do that part.
        
        this is used by the device itself to set it's own persistent values at runtime, perhaps in response to a websocket message.

        
        the host is responsible for subclassing this and actually saving the data somehow, should that feature be needed.
        """

        if not isinstance(key,str):
            raise TypeError("Key must be str")

        value = str(value)
        if len(value)>8192:
            logging.error("Excessively long param for "+key+" starting with "+value[:128])

        # Auto strip the values to clean them up
        self.config[key] = value.strip()


    def set_config_default(self, key: str, value: str):
        """sets an option in self.config if it does not exist or is blank.        
         Calls into set_config_option, you should not need to subclass this.
        """

        if not key in self.config or not self.config[key].strip():
            self.set_config_option(key,value.strip())


    def print(self, s: str, title: str = ""):
        """used by the device to print to the hosts live device message feed, if such a thing should happen to exist"""
        logging.info(title + ': ' + str(s))

    def handle_error(self, s: str, title: str = ""):
        """like print but specifically marked as error. may get special notificatio