# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2019. Built in San Francisco.
#
# This software is distributed under commercial non-GPL license for personal, educational,
# corporate or any other use. The software as a whole or any parts of that are prohibited
# for distribution and/or use without obtaining license from 3D Control Systems, Inc.
#
# If you do not have the license to use this software, please delete all software files
# immediately and contact sales to obtain the license: sales@3dprinteros.com.
# If you are unsure about the licensing please contact directly our sales: sales@3dprinteros.com

import time
import logging
import threading
import requests
from PySide6 import QtCore

import config
import log

NM = None
try:
    import NetworkManager as NM
    from dbus.mainloop.glib import DBusGMainLoop
    DBusGMainLoop(set_as_default=True)
except ImportError:
    logging.getLogger("network_model").warning("Error importing python-networkmanager: no module")
except:
    logging.getLogger("network_model").warning("Error importing python-networkmanager: cant connect to networkmanager. Is it up?")

#NOTE use "except:\n" when trying to call anything from NetworkManager - it has almost unpredictable exceptions

class NetworkModel(QtCore.QObject):

    UPDATE_LOOP_TIME = 1
    AFTER_SCAN_SLEEP = 12
    NETWORK_CHECK_TIMEOUT = 2
    CONNECTION_WAIT_TIMEOUT = 5
    NETWORK_CHECK_PATH = '/noauth/get_timestamp'
    NA_STR = "N/A"

    on_connectConnected = QtCore.Signal()
    on_networkConnectChanged = QtCore.Signal()
    on_connectError = QtCore.Signal()
    on_wifiListChanged = QtCore.Signal()
    on_wifiSignal = QtCore.Signal()
    on_localIp = QtCore.Signal()
    on_macAddr = QtCore.Signal()
    on_connectionIsWired = QtCore.Signal()

    @staticmethod
    def wifi_strength_percent_to_level(strength_percent):
        return round(strength_percent / 25)

    def __init__(self, core_model):
        super().__init__()
        self.logger = logging.getLogger(__name__)
        self.logger.setLevel(logging.INFO)
        if not config.get_settings()['manage_networking']['enabled']:
            self.NM = None
        self._coreModel = core_model
        self._app = core_model._app
        if NM:
            self._networkManager = NM.NetworkManager
        self._connectionIsWired = self._getConnectionTypeIsWired()
        self.on_connectionIsWired.emit()
        self._wifiStationList = []
        self._wifiScanLock = threading.RLock()
        self._initWifiInterface()
        self._firstNetworkCheck = True
        self._networkConnected = True
        self._wifiSignal = 0
        self._localIp = self.NA_STR
        self._macAddr = self.NA_STR
        self._connectError = ""
        self._connectThread = None
        host = config.get_settings().get('URL', '')
        if config.get_settings().get('protocol', {}).get('encrypted', True):
            self._networkCheckUrl = "https://" + host + self.NETWORK_CHECK_PATH
        else:
            self._networkCheckUrl = "http://" + host + self.NETWORK_CHECK_PATH
        self._networkCheckThread = threading.Thread(target=self._networkCheckLoop)
        host = config.get_settings().get('URL', '')
        if config.get_settings().get('protocol', {}).get('encrypted', True):
            self._networkCheckUrl = "https://" + host + self.NETWORK_CHECK_PATH
        else:
            self._networkCheckUrl = "http://" + host + self.NETWORK_CHECK_PATH
        self._networkCheckThread.start()

    def _getConnectionTypeIsWired(self):
        if NM:
            try:
                return 'ethernet' in self._networkManager.PrimaryConnectionType
            except:
                pass
        return True

    def _checkNetworkUsingNM(self):
        if NM:
            try:
                return self._networkManager.State in (NM.NM_STATE_CONNECTED_GLOBAL, NM.NM_STATE_CONNECTED_SITE, NM.NM_STATE_CONNECTING)
            except:
                pass
        return False

    def _checkNetworkUsingRequest(self):
        try:
            return requests.get(self._networkCheckUrl, timeout=self.NETWORK_CHECK_TIMEOUT).ok
        except:
            return False

    def _checkNetwork(self):
        if NM:
            return self._checkNetworkUsingNM()
        else:
            return self._checkNetworkUsingRequest()

    def _checkSignalLevel(self):
        signal_level = 0
        try:
            if self._wifiInterface:
                if self._wifiInterface.ActiveAccessPoint:
                    signal_level = self.wifi_strength_percent_to_level(self._wifiInterface.ActiveAccessPoint.Strength)
        except:
            self.logger.debug("Error getting WiFi signal")
        #self.logger.debug('Wifi signal:' + str(signal_level))
        return signal_level

    def _getMacAddr(self):
        macaddr = self.NA_STR
        try:
            if NM:
                for connection in self._networkManager.ActiveConnections:
                    if connection.Type in ('802-3-ethernet', '802-11-wireless'):
                        if connection.Default:
                            break
                else:
                    connection = self._networkManager.ActiveConnections[0]
                interface_name = connection.Connection.GetSettings()['connection']['interface-name']
                devices = self._networkManager.GetDevices()
                for device in devices:
                    if device.Interface == 'lo':
                        continue
                    if device.Interface == interface_name:
                        macaddr = device.HwAddress
                        break
                    if not macaddr:
                        macaddr = self.NA_STR
        except:
            self.logger.warning("Error getting MAC address")
        finally:
            return macaddr

    def _getLocalIp(self):
        ip = self.NA_STR 
        if NM:
            try:
                for connection in self._networkManager.ActiveConnections:
                    if connection.Type in ('802-3-ethernet', '802-11-wireless'):
                        if connection.Default:
                            break
                else:
                    connection = self._networkManager.ActiveConnections[0]
                ip = connection.Ip4Config.Addresses[0][0]
                if not ip:
                    ip = self.NA_STR
            except:
                self.logger.debug("Error getting local IP")
        return ip

    @log.log_exception
    def _networkCheckLoop(self):
        while not self._app.stop_flag:
            if self._checkNetwork():
                if not self._networkConnected or self._firstNetworkCheck:
                    self._firstNetworkCheck = False
                    self._networkConnected = True
                    self.on_networkConnectChanged.emit()
                    self.logger.warning("Network connection established")
                local_ip = self._getLocalIp()
                if self._localIp != local_ip:
                    self._localIp = local_ip
                    self._macAddr = self._getMacAddr()
            elif self._networkConnected or self._firstNetworkCheck:
                self._firstNetworkCheck = False
                self._networkConnected = False
                self.on_networkConnectChanged.emit()
                self.logger.warning("Network connection lost")
                self._localIp = self.NA_STR
                self._macAddr = self.NA_STR
            new_signal_level = self._checkSignalLevel()
            wired = self._getConnectionTypeIsWired()
            if wired != self.connectionIsWired:
                self._connectionIsWired = wired
                self.on_connectionIsWired.emit()
            if new_signal_level != self._wifiSignal: #TODO This is only temporary to still emit even on wired. Ref req.
                # self.logger.debug("Signal level emit: " + str(new_signal_level))
                self._wifiSignal = new_signal_level
                self.on_wifiSignal.emit()
                self.on_wifiListChanged.emit()
            time.sleep(self.UPDATE_LOOP_TIME)

    def _initWifiInterface(self):
        if NM:
            for device in self._networkManager.GetAllDevices():
                if device.DeviceType == NM.NM_DEVICE_TYPE_WIFI:
                    self.logger.info("Got WiFi device: " + str(device.Interface))
                    self._wifiInterface = device
                    break

    def _connectWifi(self, ssid, password):
        self.logger.debug("Connecting to ssid: " + str(ssid))
        self._connectError == "" # clean prev error
        with self._wifiScanLock:
            wifiNMAPObjsDict = self.scanWifi()
            access_point_object = wifiNMAPObjsDict.get(ssid, None)
            if not access_point_object:
                message = "No access point with such ssid: " + str(ssid)
                self.logger.exception(message)
                self._connectError = message
                self.on_connectError.emit()
                return False
            try:
                wifiInterfaceName = str(self._wifiInterface.Interface)
                for connection in NM.Connection.all():
                    settings = connection.GetSettings()
                    isWireless = settings.get('connection', {}).get('interface-name') == wifiInterfaceName
                    if isWireless:
                        self.logger.info("Deleting connection " + str(connection))
                        connection.Delete()
            except:
                self.logger.exception("Cant delete connection")
            settings = {'802-11-wireless-security': {'psk': password}}
            try:
                self._networkManager.AddAndActivateConnection(settings, self._wifiInterface, access_point_object)
            except:
                message = "Error connecting to WiFi"
                self.logger.exception(message)
                self._connectError = message
                self.on_connectError.emit()
                return False
            while not self._app.stop_flag:
                time.sleep(1)
                state = self._networkManager.State
                if state == NM.NM_STATE_CONNECTING:
                    continue
                if state == NM.NM_STATE_DISCONNECTED:
                    self._connectError = "Error connecting to WiFi. Wrong password?"
                    self.on_connectError.emit()
                    return False
                if state in (NM.NM_STATE_CONNECTED_GLOBAL, NM.NM_STATE_CONNECTED_SITE):
                    self.on_connectConnected.emit()
                    return True

    def _disconnectWifi(self):
        self.logger.info("Disconnecting from WiFi")
        try:
            active_connection = None
            if self._wifiInterface:
                try:
                    active_connection = self._wifiInterface.ActiveConnection
                except:
                    self.logger.error("Cant get active connection")
                    active_connection = False
            if active_connection:
                self._wifiInterface.Disconnect()
                self.logger.info("WiFi disconnect success")
            else:
                self.logger.info("No connection to disconnect from. Skipping.")
        except Exception as e:
            self.logger.warning("Error while disconnecting WiFi:" + str(e))
            self.scanWifi()

    @QtCore.Slot(str)
    def forgetNetwork(self, ssid):
        self.logger.error("Error: forgetNetwork is not implemented yet")

    @QtCore.Slot()
    def scanWifi(self):
        self.logger.debug("Scanning...")
        with self._wifiScanLock:
            if not self._wifiInterface:
                self._initWifiInterface()
            connected_ssid = None
            try:
                connected_access_point = self._wifiInterface.ActiveAccessPoint
            except:
                self.logger.warning("Error getting current access point")
            else:
                if connected_access_point:
                    connected_ssid = connected_access_point.Ssid
            try:
                all_access_points = self._wifiInterface.GetAllAccessPoints()
            except:
                self.logger.error("Error getting access points")
                return {}
            if len(all_access_points) < 2:
                self.logger.warning("Warning: suspiciously short WiFi access points list. Wrong mode?")
            wifiStationList = []
            wifiNMAPObjsDict = {}
            for access_point in all_access_points:
                try:
                    ssid = access_point.Ssid
                    signalLevel = self.wifi_strength_percent_to_level(access_point.Strength)
                    encrypted = not access_point.Flags & NM.NM_802_11_AP_FLAGS_NONE
                    #infrastusture_mode = bool(access_point.Mode & NM.NM_802_11_MODE_INFRA)
                    #adhog_mode = bool(access_point.Mode & NM.NM_802_11_MODE_ADHOC)
                except:
                    self.logger.exception("Access point vanished")
                else:
                    self.logger.info("WiFi scan found: " + ssid)
                    access_point_dict = {"ssid": ssid,
                                    "secured": encrypted,
                                    "signalLevel": signalLevel,
                                    #"infrastusture_mode": infrastusture_mode,
                                    #"adhog_mode": adhog_mode,
                                    "connected": ssid == connected_ssid}
                    wifiStationList.append(access_point_dict)
                    wifiNMAPObjsDict[ssid] = access_point
            self._wifiStationList = wifiStationList
            self.on_wifiListChanged.emit()
            #self.logger.debug(self._wifiStationList)
            return wifiNMAPObjsDict

    @QtCore.Slot(str, str)
    def connectWifi(self, ssid, passwd = ""):
        self.logger.debug(f"Connecting Wi-Fi %s:%s" % (ssid, passwd))
        if self._connectThread and self._connectThread.is_alive():
            self.logger.error("another connection is already in attempted")
        else:
            self._connectThread = threading.Thread(target=self._connectWifi, args=(ssid, passwd))
            self._connectThread.start()

    @QtCore.Slot()
    def disconnectWifi(self):
        self._disconnectWifi()

    @QtCore.Property(int, notify=on_wifiSignal)
    def wifiSignal(self):
        return self._wifiSignal

    @QtCore.Property(str, notify=on_connectError)
    def connectError(self):
        return self._connectError

    @QtCore.Property('QVariantList', notify=on_wifiListChanged)
    def wifiStationList(self):
        return self._wifiStationList

    @QtCore.Property(bool, notify=on_networkConnectChanged)
    def networkConnected(self):
        return self._networkConnected

    @QtCore.Property(bool, notify=on_connectionIsWired)
    def connectionIsWired(self):
        return self._connectionIsWired

    @QtCore.Property(str, notify=on_localIp)
    def localIp(self):
        return self._localIp

    @QtCore.Property(str, notify=on_macAddr)
    def macAddr(self):
        return self._macAddr
