import netw
from micropython import const
import vts
from unit_info import Unit
from ucollections import OrderedDict
import unit_info

# -------------------- CONSTANTS -------------------- #
# BLUETOOTH
BT_MODE_DISC_OFF                = const(1)
BT_MODE_DISC_LIMITED            = const(2)
BT_MODE_DISC_ON                 = const(3)

BT_MODE_CONN_OFF                = const(1)
BT_MODE_CONN_ON                 = const(2)

BT_MODE_PAIR_OFF                = const(1)
BT_MODE_PAIR_ON                 = const(2)

BT_MODE_SEC_DISABLED            = const(1)
BT_MODE_SEC_DEFAULT             = const(2)
BT_MODE_SEC_DISPLAY_PASSKEY     = const(3)
BT_MODE_SEC_CONFIRM             = const(4)
BT_MODE_SEC_ENTER_PASSKEY       = const(5)

# WIFI
AUTH_OPEN     = 1
AUTH_WPA_WPA2 = 2
AUTH_LEAP     = 3
AUTH_PEAP     = 4

IP_STATIC = 1
IP_DHCP   = 2

EVENT_BT_BOND_SUCCESS       = const(1)
EVENT_BT_BOND_FAIL          = const(2)
EVENT_BT_CONNECT_SUCCESS    = const(3)
EVENT_BT_CONNECT_FAIL       = const(4)
EVENT_BT_DISCONNECT         = const(5)

EVENT_WIFI_CONNECT_SUCCESS  = const(1)
EVENT_WIFI_CONNECT_FAIL     = const(2)
EVENT_WIFI_DISCONNECT       = const(3)
EVENT_HTTP_CONNECT_SUCCESS  = const(4)
EVENT_HTTP_CONNECT_FAIL     = const(5)
EVENT_HTTP_DISCONNECT       = const(6)
EVENT_MQTT_CONNECT_SUCCESS  = const(7)
EVENT_MQTT_CONNECT_FAIL     = const(8)
EVENT_MQTT_DISCONNECT       = const(9)

__bond_incorrect_reasons = {
    1:'due to timeout.', 
    2:'because authentication or pairing failed. Often due to incorrect passkey.', 
    3:'because the protection against Man-In-The-Middle attack could not be guaranteed; the generated link key was too weak.',
    4:'because one of the peers does not support FIPS mode.'
}

__network_status_ids = {
    0   : 'Hardware_Address', 
    1   : 'Network_Status',
    2   : 'Interface_Type',
    101 : 'IPv4_Address',
    102 : 'Subnet_Mask',
    103 : 'Gateway',
    104 : 'Primary_DNS',
    105 : 'Seconday_DNS',
    201 : 'IPv6_link_local_address',
    210 : '210_IPv6_Address',
    211 : '211_IPv6_Address',
    212 : '212_IPv6_Address',
}

__WiFi_connection_fail_reasons = {
    0 : 'Unknown',
    1 : 'Remote close',
    2 : 'Out of range',
    3 : 'Roaming',
    4 : 'Security problems',
    5 : 'Network disabled',
}
# --------------------------------------------------- #

# class Sys_Info():
#     while netw.get_init_status() != netw.INIT_OK:
#         vts.delay_ms(1)
#     hardware_info = netw.get_hwmodule_info()
#     model = hardware_info.model.replace('"', '')
#     model_firmware = hardware_info.version.replace('"', '')

class BondingError(Exception):
    pass

class HardwareError(Exception):
    pass

class FirmwareError(Exception):
    pass

class ConnectionError(Exception):
    pass

class CallbackError(Exception):
    pass

class Network():
    def __init__(self):
        netw.set_callback(self.__resp_loop)
        self.__connections_name_channel_id = {} # connections that have been made [ connection_name : channel_id ]
        self.__connection_name = None
        self.__connections_id = {} # connections that have been made [ channel_id : object ]
        self.__peer_channel = None
        self.__peer_handle_channel_id = {} # connections that have been made [ channel_id : peer_handle ]
        self.__current_connection_in_progress = None

        self.__bluetooth_user_cb = None
        self.__wifi_user_cb = None
        self.__data_user_cb = None

        self.__AT_commands = {
            'UBTI'   : self.__process_Bluetooth_scan,
            'UWSCAN' : self.__process_WiFi_scan,
            'UUBTB'  : self.__check_bonded,
            'UBTBD'  : self.__receive_bonded_devices,
            'UDCP'   : self.__peer_channel_of_connection,
            'UWSC'   : self.__store_wifi_profiles,
            'UUNU'   : self.__check_wifi_network_up,
            'UUWLD'  : self.__wifi_connect_failed,
            'UNSTAT' : self.__get_wifi_connection_details,
            'UUDPC'  : self.__peer_connect_success,
        }

        self.bluetooth_devices_found = OrderedDict() # stored as [ bluetooth_name : bluetooth_address ]
        self.__saved_bonded_devices = []
        self.__request_bonded_devices()

        if Unit.is_pbox():
            self.set_bluetooth_name('PBOX Touch {}'.format(Unit.info['Serial Number']))
        else:
            self.set_bluetooth_name('VBOX Touch {}'.format(Unit.info['Serial Number']))
            self.wifi_networks_found = {} # stored as [ SSID : BSSID ]
            self.wifi_details = {} 
            self.wifi_profiles = {} # stored as [ profile_num : SSID ]
            self.wifi_connection_established = False
            self.network_status = {}
            self.__MQTT_Gateway_flag = False
            self.__MQTT_Gateway_connections = []
            self.MQTT_Gateways = {}
            self.__load_wifi_profiles()


    ###########################################################
    #################### CALLBACK COMMANDS ####################
    ###########################################################
    def __call_bluetooth_cb(self, event_id):
        if callable(self.__bluetooth_user_cb):
            self.__bluetooth_user_cb(event_id)

    def set_bluetooth_cb(self, cb_function):
        if callable(cb_function):
            self.__bluetooth_user_cb = cb_function
        else:
            raise TypeError('Bluetooth callback takes callable func input not {}'.format(type(cb_function)))


    def __call_wifi_cb(self, event_id):
        if callable(self.__wifi_user_cb):
            self.__wifi_user_cb(event_id)

    def set_wifi_cb(self, cb_function):
        if callable(cb_function):
            self.__wifi_user_cb = cb_function
        else:
            raise TypeError('Wifi callback takes callable func input not {}'.format(type(cb_function)))


    def __call_data_cb(self, connection_obj):
        if callable(self.__data_user_cb):
            self.__data_user_cb(connection_obj)

    def set_data_cb(self, cb_function):
        if callable(cb_function):
            self.__data_user_cb = cb_function
        else:
            raise TypeError('data callback takes callable func input not {}'.format(type(cb_function)))

    #############################################################
    #################### WIFI BASED COMMANDS ####################
    #############################################################
    def __process_WiFi_scan(self, resp_msg): # Stores the details of the WiFi networks the VBOX Touch picks up
        msg = resp_msg.split(',')
        bssid = msg[0]
        op_mode = msg[1]
        ssid = msg[2].replace('"','')
        channel = msg[3]
        rssi = msg[4]
        authentication_suites = msg[5]
        unicast_ciphers = msg[6]
        group_ciphers = msg[7]
        print('MSG: BSSID = {}, OP_MODE = {}, SSID = {}, CHANNEL = {}, RSSI = {}, AUTHENTICATION_SUITES = {}, UNICAST_CIPHERS = {}, GROUP_CIPHERS = {}'.format(bssid, op_mode, ssid, channel, rssi, authentication_suites, unicast_ciphers, group_ciphers))
        self.wifi_networks_found[ssid] = bssid
        self.wifi_details[bssid] = [op_mode, channel, rssi, authentication_suites, unicast_ciphers, group_ciphers]


    def __load_wifi_profiles(self): # Loads the wifi profile data from the chip
        for x in range(0,10):
            self.cmd('AT+UWSCA={},2'.format(x))
            self.cmd('AT+UWSC={},2'.format(x)) # gets the SSID of the loaded data


    def __store_wifi_profiles(self, msg): # receive the returned data from the above load
        msg_split = msg.split(',')
        self.wifi_profiles[int(msg_split[0])] = msg_split[2].split('\r\n')[0].replace('"','')


    def setup_wifi_profile(self, ssid, password='', profile=0, auth=AUTH_WPA_WPA2, ipmode=IP_DHCP, always_active=False): # setup the connection for WiFi, 0 is default profile location                
        if profile == self.wifi_connection_established:
            self.disconnect_from_wifi() # this profile has to be inactive before it can be altered

        self.cmd('AT+UWSC={},0,{:d}'.format(profile, always_active)) # decides if the station is active on start up.
        self.cmd('AT+UWSC={},2,"{}"'.format(profile, ssid)) # Sets the ssid of the wifi
        # self.cmd('AT+UWSC={},3,"{}"'.format(profile, self.ssid_to_bssid[ssid])) # Sets the bssid
        self.cmd('AT+UWSC={},5,{}'.format(profile, auth)) # Shows the authentication type

        if auth != AUTH_OPEN: # If it requires a password
            self.cmd('AT+UWSC={},{},"{}"'.format(profile,  
                8 if AUTH_WPA_WPA2 == auth else 9, # Gives the password type
                password)) # password

        self.cmd('AT+UWSC={},100,{}'.format(profile, ipmode)) # Sets the way to get the IP address. Static or DHCP
        self.cmd('AT+UWSCA={},1'.format(profile)) # Store the profile
        self.wifi_profiles[profile] = ssid
    
        return profile


    def wifi_profile_erase(self, profile): # Erase a WiFi profile
        if profile == self.wifi_connection_established:
            self.disconnect_from_wifi()
        self.cmd('AT+UWSCA={},0'.format(profile)) # Reset profile
        self.cmd('AT+UWSCA={},1'.format(profile)) # Store the blank profile


    def connect_to_wifi(self, profile): # Connect to WiFi using profile defined earlier
        if type(profile) == str:
            try:
                new_profile = [key for key, v in self.wifi_profiles.items() if v == profile][0]
            except IndexError:
                self.__call_wifi_cb(EVENT_WIFI_CONNECT_FAIL)
                # raise ConnectionError("SSID entered does not exist as a WiFi profile.")
        else:
            new_profile = profile

        if self.wifi_connection_established == False:
            self.cmd('AT+UWSCA={},3'.format(new_profile)) # Activate profile
            self.__connecting_to_profile = new_profile
        else:
            self.__call_wifi_cb(EVENT_WIFI_CONNECT_FAIL)
            # raise ConnectionError("Already connected to WiFi, disconnect before trying to connect to a new one")


    def __check_wifi_network_up(self, msg): # Used to confirm WiFi is connected
        if self.__connecting_to_profile != None:
            self.wifi_connection_established = self.__connecting_to_profile
            self.__connecting_to_profile = None
            self.__call_wifi_cb(EVENT_WIFI_CONNECT_SUCCESS)


    def __wifi_connect_failed(self, msg):
        reason = msg.split(',')[1]
        self.cmd('AT+UWSCA={},4'.format(self.__connecting_to_profile)) # stops the program for continuing to try and connect after a connection fail.
        self.__call_wifi_cb(EVENT_WIFI_CONNECT_FAIL)
        self.__wifi_connection_fail_reason = __WiFi_connection_fail_reasons[int(reason)]


    def get_wifi_fail_reason(self):
        return self.__wifi_connection_fail_reason


    def disconnect_from_wifi(self): # Disconnect from WiFi
        # print("DISCONNECTING FROM WIFI")
        self.cmd('AT+UWSCA={},4'.format(self.wifi_connection_established)) # Deactivate profile
        self.__call_wifi_cb(EVENT_WIFI_DISCONNECT)
        self.wifi_connection_established = False
            

    def request_wifi_connection_details(self, interface_id): # Request the current WiFi connection details (e.g IPv4, IPv6, Status)
        self.network_interface_id = interface_id
        self.cmd('AT+UNSTAT={}'.format(interface_id))


    def __get_wifi_connection_details(self, resp_msg): # Gets the details from the above request and stores them
        if self.fully_connected == True:
            return
        messages = resp_msg.split('\r\n+UNSTAT:{},'.format(self.network_interface_id))[1:] # splits all the +UNSTAT responses up
        for msg in messages: # gets each string of the messages  
            msg_lst = msg.split(',') # splits them by their type and data
            if int(msg_lst[0]) == 101: # checks if it is the IP type has been acquired as this happens last
                if msg_lst[1] != '0.0.0.0': # check that the IP has changed
                    self.fully_connected = True

            self.network_status[__network_status_ids[int(msg_lst[0])]] = msg_lst[1]

    #############################################################
    #################### HTTP BASED COMMANDS ####################
    #############################################################
    def __peer_connect_HTTP(self, domain, port, secure=True): # Creates the connection via HTTP 
        self.HTTP_domain = domain
        self.HTTP_port = port
        self.HTTP_security = secure
        self.__connection_in_progress("HTTP")
        self.cmd('AT+UDCP=tcp://{}:{}/{}'.format(domain,port,'?encr=1' if secure == True else ''))

    #############################################################
    #################### MQTT BASED COMMANDS ####################
    #############################################################
    def __peer_connect_MQTT_publish(self, broker, port, client_id, username, password, publish_topic): # Creates the connection as MQTT publish
        self.__connection_in_progress("MQTT Publish")
        self.cmd('AT+UDCP=mqtt://{}:{}/?{}user={}&passwd={}&pt={}&encr=1'.format(broker, port, client_id, username, password, publish_topic))


    def __peer_connect_MQTT_subscribe(self, broker, port, client_id, username, password, subscribe_topic): # Creates the connection as MQTT subscribe
        self.__connection_in_progress("MQTT Subscribe")
        self.cmd('AT+UDCP=mqtt://{}:{}/?{}user={}&passwd={}&st={}&encr=1'.format(broker, port, client_id, username, password, subscribe_topic))


    def __peer_connect_MQTT_gateway(self, broker, port, client_id, username, password, topic): # Creates the connection as an MQTT gateway
        self.__MQTT_Gateway_flag = True
        if not self.__MQTT_Gateway_connections:
            self.__peer_connect_MQTT_subscribe(broker, port, client_id, username, password, topic)
            self.__broker = broker
            self.__port = port
            self.__client_id = client_id
            self.__username = username
            self.__password = password
            self.__topic = topic
        else:
            self.__peer_connect_MQTT_publish(self.__broker, self.__port, self.__client_id, self.__username, self.__password, self.__topic)

    ##################################################################
    #################### BLUETOOTH BASED COMMANDS ####################
    ##################################################################
    def __process_Bluetooth_scan(self, msg): # Processes and stores the information from a bluetooth scan
        device_split = msg.split(",")
        device_bluetooth_ADDR = device_split[0][:-1]
        
        device_bluetooth_name = device_split[3].replace('"', '')
        if device_bluetooth_name.isspace() or len(device_bluetooth_name) == 0:
            device_bluetooth_name = device_bluetooth_ADDR

        self.bluetooth_devices_found[device_bluetooth_name] = device_bluetooth_ADDR


    def _get_address_from_name(self,name): # Gets the BSSID address of the device based on the SSID
        try:
            return self.bluetooth_devices_found[name]
        except KeyError:
            print("Device not found")


    def set_bluetooth_name(self, name): # Sets a new name to appear on a bluetooth connection for the VBOX Touch device
        self.cmd('AT+UBTLN='+name) # Write name
        self.cmd('AT&W') # Write name to start up database?
        self.__bluetooth_name = name

    def pairing(self, security=BT_MODE_SEC_DEFAULT, pairing=BT_MODE_PAIR_ON,
                        discoverability=BT_MODE_DISC_ON, connectability=BT_MODE_CONN_ON): # By default it will enable pairing
        #may require re-calling after a BT disconnect as accepted connections set pairing mode to disabled
        self.cmd('AT+UBTSM={}'.format(security)) #set Security Mode
        self.cmd('AT+UBTDM={}'.format(discoverability)) #set Discoverability
        self.cmd('AT+UBTCM={}'.format(connectability)) #set Connectability Mode
        self.cmd('AT+UBTPM={}'.format(pairing)) #set Pairing Mode

 
    def __request_bonded_devices(self): # Requests what devices are bonded
        self.cmd('AT+UBTBD')


    def __receive_bonded_devices(self, device): # Receives the data from the above function
        # print("BONDED DEVICES: {}".format(device))
        self.__saved_bonded_devices.append(device.split(',')[0])
        
    def get_previously_bonded_devices(self):
        return self.__saved_bonded_devices

    def bond_to_device(self, device_name): # Sends a bond request to device
        self.cmd('AT+UBTB={}'.format(self._get_address_from_name(device_name)))


    def unbond_device(self, device_name): # Sends an unbond request
        self.cmd('AT+UBTUB={}'.format(self._get_address_from_name(device_name)))


    def __check_bonded(self, resp_msg): # Checks if the bond was successful
        bond_event_status = int(resp_msg.split(',')[1])
        if bond_event_status == 0:
            print("Bonding Succesful")
            self.__call_bluetooth_cb(EVENT_BT_BOND_SUCCESS)
        else:
            self.__call_bluetooth_cb(EVENT_BT_BOND_FAIL)
            self.__bond_event_failed_reason = __bond_incorrect_reasons[bond_event_status]
    

    def get_bond_fail_reason(self):
        return self.__bond_event_failed_reason


    def __peer_connect_bluetooth(self, device_name): # Creates the connection via Bluetooth
        self.__connection_in_progress('Bluetooth')
        address = self._get_address_from_name(device_name)
        self.cmd('AT+UDCP=spp://{}p/'.format(address))

    ###################################################
    #################### UNIVERSAL ####################
    ###################################################
    def connect(self, connection_name, connection_type, *args): # Connect via a certain method
        '''
        Connection name - any data type
        Connection_type - Any of the below method names given as a string

        Connect via any of 5 methods:
        Bluetooth      - args needed: name of the device that you are trying to connect to [string]

        HTTP           - args needed: domain to connect to [string], 
                                      port of connection [int], 
                                      security of connection [bool].

        MQTT Publish   - args needed: broker to connect to [string], 
                                      port of connection [int], 
                                      your client id on the MQTT server (if applicable, often blank) [string], 
                                      username on the MQTT server [string], 
                                      password on the MQTT server [string], 
                                      the topic you want to publish to [string].

        MQTT Subscribe - args needed: broker to connect to [string], 
                                      port of connection [int], 
                                      your client id on the MQTT server (if applicable, often blank) [string], 
                                      username on the MQTT server [string], 
                                      password on the MQTT server [string], 
                                      the topic you want to subscribe to [string].

        MQTT Gateway   - args needed: broker to connect to [string], 
                                      port of connection [int], 
                                      your client id on the MQTT server (if applicable, often blank) [string], 
                                      username on the MQTT server [string], 
                                      password on the MQTT server [string], 
                                      the topic you want to connect to [string].
        '''

        if self.__current_connection_in_progress != None:
            raise ConnectionError('Connection already in progress: {}'.format(self.__current_connection_in_progress))
        
        try:
            self.__connections_name_channel_id[connection_name]
            raise NameError('Already a connection with this name')
        except KeyError:
            pass

        self.__connection_name = connection_name
        connection_type = connection_type.upper()

        if connection_type == 'BLUETOOTH':
            self.__peer_connect_bluetooth(*args) # start of the creation of bluetooth object
        elif str(self.wifi_connection_established) != 'False':
            if connection_type == 'HTTP':
                self.__peer_connect_HTTP(*args) # start of the creation of HTTP object
            elif connection_type == 'MQTT PUBLISH':
                self.__peer_connect_MQTT_publish(*args) # start of the creation of MQTT publish object
            elif connection_type == 'MQTT SUBSCRIBE':
                self.__peer_connect_MQTT_subscribe(*args) # start of the creation of MQTT subscribe object
            elif connection_type == 'MQTT GATEWAY':
                self.__peer_connect_MQTT_gateway(*args) # start of the creation of MQTT gateway object
            else:
                raise ConnectionError('Connection type does not exist')
        else:
            raise ConnectionError('WiFi not connected')


    def __connection_in_progress(self, connection_type): # Set the connection status
        connection_type = connection_type.upper()
        if connection_type == 'HTTP':
            if not Unit.is_wifi_capable():
                raise HardwareError("Network Component cannot access WiFi")

        elif 'MQTT' in connection_type:
            if not Unit.is_MQTT_capable():
                raise FirmwareError("Update Network Component Firmware")
        
        self.__current_connection_in_progress = connection_type
        # Need to disable the ability to pair from external device when a different connection is taking place
        self.pairing(pairing=BT_MODE_PAIR_OFF, discoverability=BT_MODE_DISC_OFF, connectability=BT_MODE_CONN_OFF) 

    
    def __connection_finished(self): # Finished connecting, reset vars and enable pairing from external device
        # print("CONNECTION FINISHED")
        self.__connection_name = None
        self.__peer_channel = None
        self.__current_connection_in_progress = None
        self.pairing()


    def __peer_channel_of_connection(self, resp_msg): # get the peer channel of the connection
        self.__peer_channel = resp_msg.split('\r\n')[0]


    def __peer_connect_success(self, resp_msg):
        self.__peer_channel = resp_msg[0]


    def disconnect_connection_using_name(self, connection_name): # disconnect a currently active connection using the connections name
        self.__peer_disconnect(self.__connections_name_channel_id[connection_name])


    def __peer_disconnect(self, channel_id): # send a disconnect from peer request
        try:
            self.cmd('AT+UDCPC={}'.format(self.__peer_handle_channel_id[channel_id]))
        except KeyError:
            print("Address not found")


    def __create_object_from_channel_id(self, msg): # create the object
        channel_id = msg[0]
        device_address = ''

        self.__current_connection_in_progress = 'BLUETOOTH' if self.__current_connection_in_progress == None else self.__current_connection_in_progress

        for character in msg[3:9]: 
            hex_val = hex(character).replace('0x', '')
            print('HEX VAL = {}'.format(hex_val))
            if len(hex_val) < 2:
                hex_val = '0'+hex_val
            device_address = device_address + hex_val.upper()

        self.__connections_name_channel_id[self.__connection_name] = channel_id

        self.__peer_handle_channel_id[channel_id] = self.__peer_channel

        if self.__current_connection_in_progress == 'HTTP':
            self.__connections_id[channel_id] = Connection(connection_type = self.__current_connection_in_progress, channel_ID=channel_id, security = self.HTTP_security, domain = self.HTTP_domain, port = self.HTTP_port)
            self.HTTP_security, self.HTTP_domain, self.HTTP_port = None, None, None
        else:
            self.__connections_id[channel_id] = Connection(connection_type = self.__current_connection_in_progress, channel_ID=channel_id)
            if self.__MQTT_Gateway_flag == True:
                self.__MQTT_Gateway_connections.append(self.__connections_id[channel_id])
                if len(self.__MQTT_Gateway_connections) == 2:
                    self.MQTT_Gateways[self.__connection_name] = MQTT_Gateway(self.__MQTT_Gateway_connections)
                    self.__MQTT_Gateway_connections = []
                    self.__MQTT_Gateway_flag = False
                else:
                    self.__peer_connect_MQTT_gateway(None, None, None, None, None, None)

        if self.__current_connection_in_progress == 'HTTP':
            self.__call_wifi_cb(EVENT_HTTP_CONNECT_SUCCESS)
        elif 'MQTT' in self.__current_connection_in_progress:
            self.__call_wifi_cb(EVENT_MQTT_CONNECT_SUCCESS)
        elif self.__current_connection_in_progress == 'BLUETOOTH':
            self.__call_bluetooth_cb(EVENT_BT_CONNECT_SUCCESS)

        if self.__MQTT_Gateway_flag == False:
            self.__connection_finished()


    def get_connection_object_from_name(self, name): # returns the object given the name
        return self.__connections_id[self.__connections_name_channel_id[name]]
    

    def __delete_object_from_channel_id(self, msg): # deletes the connection object and the name link from their respective lists
        channel_id = msg[0]
        try:
            # self.__peer_disconnect(channel_id)
            name_to_delete = [key for key, v in self.__connections_name_channel_id.items() if v == channel_id][0]
            self.__connections_name_channel_id.pop(name_to_delete)
            temp = self.__connections_id.pop(channel_id)
            if temp._connection_type == 'HTTP':
                self.__call_wifi_cb(EVENT_HTTP_DISCONNECT)
            elif 'MQTT' in temp._connection_type:
                self.__call_wifi_cb(EVENT_MQTT_DISCONNECT)
            elif temp._connection_type == 'BLUETOOTH':
                self.__call_bluetooth_cb(EVENT_BT_DISCONNECT)
        except KeyError:
            print("Connection object not found on channel: {}".format(channel_id))
        except IndexError:
            if self.__current_connection_in_progress == 'HTTP':
                self.__call_wifi_cb(EVENT_HTTP_CONNECT_FAIL)
            elif 'MQTT' in self.__current_connection_in_progress:
                self.__call_wifi_cb(EVENT_MQTT_CONNECT_FAIL)
            elif self.__current_connection_in_progress == 'BLUETOOTH':
                self.__call_bluetooth_cb(EVENT_BT_CONNECT_FAIL)
            
            self.__connection_finished()


    def scan(self, scan_type, time = 10000): # calls the scan depending on which type was entered
        if scan_type.upper() == "WIFI":
            self.cmd('AT+UWSCAN')
        elif scan_type.upper() == "BLUETOOTH":
            self.cmd('AT+UBTI=1,'+str(time)) # 10s scan
        else:
            raise Exception("Scan type does not exist")


    def __bind_stream(self, channel_id, MQTT_Gateway_name, client_type):
        stream_bt = self.__peer_handle_channel_id[channel_id]
        stream_MQTT = self.__peer_handle_channel_id[self.MQTT_Gateways[MQTT_Gateway_name].get_peer_handle(client_type.upper())]
        self.cmd("AT+UDBIND={},{}".format(stream_MQTT, stream_bt))


    def cmd(self, s): # calls the command to be performed
        netw.get_messages().clear()
        netw.tx_msg_edm(s, netw.UBX_EDM_CMD_AT_REQ)
        print("Sent: ", s)

    
    def __AT_default_resp(self, b): # Calls this if it doesn't know what the command is
        print("AT COMMAND DOESN'T EXIST: {}".format(b))


    def __resp_loop(self): # Receives all messages and diverts them to their relevant location 
        resp = netw.get_messages().pop()
        # print(resp)
        msg_type = resp.type
        msg_id = resp.id
        msg_len = resp.len
        msg = resp.msg
        channel_id = msg[0]

        if msg_id == netw.UBX_EDM_ID_AT:
            AT_resp_code = msg.decode('utf-8').strip().split(':')[0].replace('+','')
            if AT_resp_code == 'OK':
                print("---------- COMMAND FINISHED ----------")
            elif AT_resp_code == 'ERROR':
                print("---------- AT COMMAND FAILED ----------")
            else:
                print('AT COMMAND: {}'.format(AT_resp_code))
                self.__AT_commands.get(AT_resp_code, self.__AT_default_resp)(msg.decode('utf-8').strip().split(':')[1])

        elif msg_id == netw.UBX_EDM_ID_DATA:
            if msg_type == netw.UBX_EDM_TYPE_EVENT:
                self.__connections_id[channel_id]._process_data_received(msg)
                self.__call_data_cb(self.__connections_id[channel_id])

        elif msg_id == netw.UBX_EDM_ID_CONNECT:
            if msg_type == netw.UBX_EDM_TYPE_EVENT:
                self.__create_object_from_channel_id(msg)
        
        elif msg_id == netw.UBX_EDM_ID_DISCONNECT:
            if msg_type == netw.UBX_EDM_TYPE_EVENT:
                self.__delete_object_from_channel_id(msg)



class Connection(): # The connection class. Used to create a connection object.
    def __init__(self, connection_type, channel_ID, **kwargs):
        self.__processed_data = [None]*10
        self.__processed_data_circle_counter = 0

        self._connection_type = connection_type
        self._channel_id = channel_ID
        self.__data_received = ''

        self.__receive_data_functions = {
            'BLUETOOTH'      : self.__process_Bluetooth,
            'MQTT SUBSCRIBE' : self.__process_MQTT,
            'HTTP'           : self.__process_HTTP,
        }

        self.__send_data_functions = {
            'BLUETOOTH'      : self.__send_Bluetooth,
            'MQTT PUBLISH'   : self.__publish_MQTT,
            'HTTP'           : self.__HTTP_GET_request,
        }

        if self._connection_type == 'HTTP':
            print("kwargs: ", kwargs)
            self.__security = kwargs['security']
            self.__domain = kwargs['domain']
            self.__port = kwargs['port']
            self.__content_type = None
            self.__content_length = None
            self.__msg_started = False

    ################################################################################
    #################### Possible data received back functions #####################
    ################################################################################
    def _process_data_received(self, msg): # manages which function to send the data received to
        self.__receive_data_functions.get(self._connection_type, self.__connection_type_incorrect)(msg)


    def __process_Bluetooth(self, msg):
        message = msg.decode('utf-8')[1:] 
        if message[0:12] == 'CONNECT_MQTT':
            connect_to = message.split('CONNECT_MQTT')[1].split(',') # list of MQTT Gateway Host_name, connection type
            network.__bind_stream(self._channel_id, connect_to[0].strip(), connect_to[1].strip())
        
        self.__processed_data[self.__processed_data_circle_counter] = msg.decode('utf-8')[1:]
        # print("BLUETOOTH MESSAGE: {}".format(self.__processed_data[self.__processed_data_circle_counter]))
        if self.__processed_data_circle_counter > 8:
            self.__processed_data_circle_counter = 0
        else:
            self.__processed_data_circle_counter +=1 
        

    def __process_MQTT(self, msg):
        self.__processed_data[self.__processed_data_circle_counter] = msg.decode('utf-8')[1:]
        print("MQTT MESSAGE: {}".format(self.__processed_data[self.__processed_data_circle_counter]))
        if self.__processed_data_circle_counter > 8:
            self.__processed_data_circle_counter = 0
        else:
            self.__processed_data_circle_counter +=1 
            

    def __process_HTTP(self, msg):
        msg_received = msg.decode('utf-8').strip()
        split_data = msg_received.split('\r\n')

        for x in range(0, len(split_data)):
            if 'Content-Type: ' in split_data[x]:
                self.__content_type = split_data[x].split(': ')[1].split(';')[0]
            elif 'Content-Length: ' in split_data[x]:
                self.__content_length = split_data[x].split(': ')[1].split(';')[0]

        split_to_message = msg_received.split('\r\n\r\n')
        if len(split_to_message) == 2:
            self.__msg_started = True
            self.__data_received = split_to_message[1]
        
        elif self.__msg_started == True:
            self.__data_received = self.__data_received + msg_received

        try:
            if int(self.__content_length) <= len(self.__data_received):
                if self.__content_type != 'application/json':
                    self.__processed_data[self.__processed_data_circle_counter] = self.__data_received
                    print("HTTP MESSAGE: {}".format(self.__processed_data[self.__processed_data_circle_counter]))
                    if self.__processed_data_circle_counter > 8:
                        self.__processed_data_circle_counter = 0
                    else:
                        self.__processed_data_circle_counter +=1 
                else:
                    self.__json_HTTP_data(self.__data_received)
                
                self.__data_received = ''
                self.__msg_started = False
        except TypeError:
            pass


    def __json_HTTP_data(self, separated_data):
        import ujson
        # Have to find how to stop json from breaking when it gets invalid data (This happens when the data got is requested wrong but from a valid place)
        self.__processed_data[self.__processed_data_circle_counter] = ujson.loads(separated_data)
        print("JSON MESSAGE: {}".format(self.__processed_data[self.__processed_data_circle_counter]))
        if self.__processed_data_circle_counter > 8:
            self.__processed_data_circle_counter = 0
        else:
            self.__processed_data_circle_counter +=1  
    

    ######################################################################
    #################### Possible data send functions ####################
    ######################################################################
    def send_data(self, *args): # manages which function to send the data received to
        text = self.__send_data_functions.get(self._connection_type, self.__connection_type_incorrect)(*args)
        message = bytearray()
        message.append(int(self._channel_id))
        message.extend(text)
        # print("Sending message out: {}".format(bytes(message).decode()))
        netw.tx_msg_edm(bytes(message).decode(), netw.UBX_EDM_CMD_DATA)


    def __send_Bluetooth(self, msg):
        return msg


    def __publish_MQTT(self, msg):
        return msg
    

    def __HTTP_GET_request(self, subdirectory = 'api/activity', connection = 'keep-alive'): # security, domain, port
        if subdirectory[0] == '/':
            subdirectory = subdirectory [1:]

        msg = ('GET {}://{}:{}/{} HTTP/1.1\r\n'
        'Connection: {}\r\n'
        'Accept-Encoding: identity\r\n'
        'TE: identity\r\n'
        'Host: {}:{}\r\n\r\n').format('https' if self.__security else 'http', self.__domain, self.__port, subdirectory, connection, self.__domain, self.__port)
        return msg

    ######################################################################
    def __connection_type_incorrect(self, msg): # calls this if it can't find the connection function (Shouldn't happen)
        raise Exception("CONNECTION TYPE CANNOT DO THAT OR DOES NOT EXIST")

    def get_processed_data(self): # returns the data it most recently received 
        return self.__processed_data

    def get_recent_data(self):
        return self.__processed_data[self.__processed_data_circle_counter-1 if self.__processed_data_circle_counter != 0 else 9]


class MQTT_Gateway():
    def __init__(self, connections):
        self.connections = connections 

    def get_peer_handle(self, connection_type):
        return self.connections[0 if connection_type == 'SUBSCRIBE' else 1]._channel_id

network = Network()
# vts.delay_ms(5000)
# network.connect_to_wifi(0)
# vts.delay_ms(10000)
# network.connect('Gateway', 'MQTT Gateway', 'your-broker.example.com', 8883, '', 'your_username', 'your_password', 'my/test/topic')
# vts.delay_ms(5000)
# network.scan('Bluetooth')
# vts.delay_ms(11000)
# network.bond_to_device('VBOX Touch 65390')
# vts.delay_ms(5000)
# network.connect('Test', 'Bluetooth', 'VBOX Touch 65390')
# vts.delay_ms(5000)

# if __name__ == '__main__':
#     new_network = Network()

    # ------------------------------- BT TEST ------------------------------- #
    # No1
    # new_network.set_bluetooth_name('VBOX Touch 1')
    # new_network.scan('Bluetooth', 10000)
    # vts.delay_ms(11000)
    # new_network.connect('VBOX Touch', 'Bluetooth', 'VBOX Touch 2')

    # No2
    # new_network.set_bluetooth_name('VBOX Touch 2')
    # new_network.scan('Bluetooth', 10000)
    # vts.delay_ms(11000)
    # new_network.connect('VBOX Touch', 'Bluetooth', 'VBOX Touch 1')


    # ------------------------------ WIFI TEST ------------------------------ #
    # new_network.setup_wifi_profile('YourWiFiSSID', 'YourWiFiPassword', profile=0)
    # vts.delay_ms(1000)
    # new_network.connect_to_wifi(0)
    
    # while str(new_network.wifi_connection_established) == "False":
    #     vts.delay_ms(10)
    
    # ------------------------------ MQTT TEST ------------------------------ #
    # ---------- SUBSCRIBE ---------- #
    # new_network.connect('Subscribe', 'MQTT Subscribe', 'your-broker.example.com', 8883, '', 'your_username', 'your_password', 'my/test/topic')

    # while new_network.current_connection_in_progress != None:
    #     vts.delay_ms(100)

    # ---------- PUBLISH ---------- #
    # new_network.connect('Publish', 'MQTT Publish', 'your-broker.example.com', 8883, '', 'your_username', 'your_password', 'my/test/topic')
    # vts.delay_ms(1000)
    # new_network.__connections_id[1].send_data('hello')
    
    # ---------- GATEWAY ---------- #
    # new_network.connect('Gateway', 'MQTT Gateway', 'your-broker.example.com', 8883, '', 'your_username', 'your_password', 'my/test/topic')

    # ------------------------------ HTTP TEST ------------------------------ #
    # new_network.connect('Example', 'HTTP', 'example.com', 443, True)
    # vts.delay_ms(5000)
    # new_network.__connections_id[0].send_data('api/activity')
    # new_network.__connections_id[0].send_data('about')

# for names, address in network.bluetooth_devices_found.items():
#     if address in network.get_previously_bonded_devices():
#         print(names)