# -*- coding: utf-8 -*-

# Copyright (c) 2012 - 2020 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the IRC data structures and their manager.
"""


import copy

from PyQt5.QtCore import pyqtSignal, QObject, QCoreApplication

import Utilities
from Utilities.AutoSaver import AutoSaver
from Utilities.crypto import pwConvert
import Preferences


class IrcIdentity(object):
    """
    Class implementing the IRC identity object.
    """
    DefaultIdentityName = "0default"
    DefaultIdentityDisplay = QCoreApplication.translate(
        "IrcIdentity", "Default Identity")
    
    DefaultAwayMessage = QCoreApplication.translate(
        "IrcIdentity", "Gone away for now.")
    DefaultQuitMessage = QCoreApplication.translate(
        "IrcIdentity", "IRC for eric6 IDE")
    DefaultPartMessage = QCoreApplication.translate(
        "IrcIdentity", "IRC for eric6 IDE")
    
    def __init__(self, name):
        """
        Constructor
        
        @param name name of the identity (string)
        """
        super(IrcIdentity, self).__init__()
        
        self.__name = name
        self.__realName = ""
        self.__nickNames = []
        self.__serviceName = ""
        self.__password = ""
        self.__ident = Utilities.getUserName()
        
        self.__rememberPosOnAway = True
        self.__awayMessage = IrcIdentity.DefaultAwayMessage
        
        self.__quitMessage = IrcIdentity.DefaultQuitMessage
        self.__partMessage = IrcIdentity.DefaultPartMessage
    
    def save(self, settings):
        """
        Public method to save the identity data.
        
        @param settings reference to the settings object (QSettings)
        """
        # no need to save the name because that is the group key
        settings.setValue("Ident", self.__ident)
        settings.setValue("RealName", self.__realName)
        settings.setValue("NickNames", self.__nickNames)
        settings.setValue("ServiceName", self.__serviceName)
        settings.setValue("Password", self.__password)
        settings.setValue("QuitMessage", self.__quitMessage)
        settings.setValue("PartMessage", self.__partMessage)
        settings.setValue("RememberAwayPosition", self.__rememberPosOnAway)
        settings.setValue("AwayMessage", self.__awayMessage)
    
    def load(self, settings):
        """
        Public method to load the identity data.
        
        @param settings reference to the settings object (QSettings)
        """
        self.__ident = settings.value("Ident", Utilities.getUserName())
        self.__realName = settings.value("RealName", "")
        self.__nickNames = Preferences.toList(settings.value("NickNames", []))
        self.__serviceName = settings.value("ServiceName", "")
        self.__password = settings.value("Password", "")
        self.__quitMessage = settings.value(
            "QuitMessage", IrcIdentity.DefaultQuitMessage)
        self.__partMessage = settings.value(
            "PartMessage", IrcIdentity.DefaultPartMessage)
        self.__rememberPosOnAway = Preferences.toBool(
            settings.value("RememberAwayPosition", True))
        self.__awayMessage = settings.value(
            "AwayMessage", IrcIdentity.DefaultAwayMessage)
    
    def setName(self, name):
        """
        Public method to set the identity name.
        
        @param name identity name (string)
        """
        self.__name = name
    
    def getName(self):
        """
        Public method to get the identity name.
        
        @return identity name (string)
        """
        return self.__name
    
    def setIdent(self, name):
        """
        Public method to set the real identity name.
        
        @param name real identity name (string)
        """
        self.__ident = name
    
    def getIdent(self):
        """
        Public method to get the real identity name.
        
        @return real identity name (string)
        """
        return self.__ident
    
    def setRealName(self, name):
        """
        Public method to set the real name of the identity.
        
        @param name real name (string)
        """
        self.__realName = name
    
    def getRealName(self):
        """
        Public method to get the real name.
        
        @return real name (string)
        """
        return self.__realName
    
    def setNickNames(self, names):
        """
        Public method to set the nick names of the identity.
        
        @param names nick names (list of string)
        """
        self.__nickNames = names[:]
    
    def getNickNames(self):
        """
        Public method to get the nick names.
        
        @return nick names (list of string)
        """
        return self.__nickNames
    
    def setServiceName(self, name):
        """
        Public method to set the service name of the identity used for
        identification.
        
        @param name service name (string)
        """
        self.__serviceName = name
    
    def getServiceName(self):
        """
        Public method to get the service name of the identity used for
        identification.
        
        @return service name (string)
        """
        return self.__serviceName
    
    def setPassword(self, password):
        """
        Public method to set a new password.
        
        @param password password to set (string)
        """
        self.__password = pwConvert(password, encode=True)
    
    def getPassword(self):
        """
        Public method to get the password.
        
        @return password (string)
        """
        return pwConvert(self.__password, encode=False)
    
    def setQuitMessage(self, message):
        """
        Public method to set the QUIT message.
        
        @param message QUIT message (string)
        """
        if message:
            self.__quitMessage = message
        else:
            self.__quitMessage = IrcIdentity.DefaultQuitMessage
    
    def getQuitMessage(self):
        """
        Public method to get the QUIT message.
        
        @return QUIT message (string)
        """
        return self.__quitMessage
    
    def setPartMessage(self, message):
        """
        Public method to set the PART message.
        
        @param message PART message (string)
        """
        if message:
            self.__partMessage = message
        else:
            self.__partMessage = IrcIdentity.DefaultPartMessage
    
    def getPartMessage(self):
        """
        Public method to get the PART message.
        
        @return PART message (string)
        """
        return self.__partMessage
    
    def setRememberAwayPosition(self, remember):
        """
        Public method to set to remember the chat position upon AWAY.
        
        @param remember flag indicating to remember the chat position (boolean)
        """
        self.__rememberPosOnAway = remember
    
    def rememberAwayPosition(self):
        """
        Public method to get a flag indicating to remember the chat position
        upon AWAY.
        
        @return flag indicating to remember the chat position (boolean)
        """
        return self.__rememberPosOnAway
    
    def setAwayMessage(self, message):
        """
        Public method to set the AWAY message.
        
        @param message AWAY message (string)
        """
        if message:
            self.__awayMessage = message
        else:
            self.__awayMessage = IrcIdentity.DefaultAwayMessage
    
    def getAwayMessage(self):
        """
        Public method to get the AWAY message.
        
        @return AWAY message (string)
        """
        return self.__awayMessage
    
    @classmethod
    def createDefaultIdentity(cls):
        """
        Class method to create the default identity.
        
        @return default identity (IrcIdentity)
        """
        userName = Utilities.getUserName()
        realName = Utilities.getRealName()
        if not realName:
            realName = "eric IDE chat"
        identity = IrcIdentity(IrcIdentity.DefaultIdentityName)
        identity.setNickNames([userName, userName + "_", userName + "__"])
        identity.setRealName(realName)
        identity.setIdent(userName)
        return identity


class IrcServer(object):
    """
    Class implementing the IRC identity object.
    """
    DefaultPort = 6667
    DefaultSslPort = 6697
    
    def __init__(self, name):
        """
        Constructor
        
        @param name name of the server (string)
        """
        super(IrcServer, self).__init__()
        
        self.__server = name
        self.__port = IrcServer.DefaultPort
        self.__ssl = False
        self.__password = ""
    
    def save(self, settings):
        """
        Public method to save the server data.
        
        @param settings reference to the settings object (QSettings)
        """
        settings.setValue("Name", self.__server)
        settings.setValue("Port", self.__port)
        settings.setValue("SSL", self.__ssl)
        settings.setValue("Password", self.__password)
    
    def load(self, settings):
        """
        Public method to load the server data.
        
        @param settings reference to the settings object (QSettings)
        """
        self.__server = settings.value("Name", "")
        self.__port = int(settings.value("Port", IrcServer.DefaultPort))
        self.__ssl = Preferences.toBool(settings.value("SSL", False))
        self.__password = settings.value("Password", "")
    
    def getName(self):
        """
        Public method to get the server name.
        
        @return server name (string)
        """
        return self.__server
    
    def setName(self, name):
        """
        Public method to set the server name.
        
        @param name server name (string)
        """
        self.__server = name
    
    def getPort(self):
        """
        Public method to get the server port number.
        
        @return port number (integer)
        """
        return self.__port
    
    def setPort(self, port):
        """
        Public method to set the server port number.
        
        @param port server port number (integer)
        """
        self.__port = port
    
    def useSSL(self):
        """
        Public method to check for SSL usage.
        
        @return flag indicating SSL usage (boolean)
        """
        return self.__ssl
    
    def setUseSSL(self, on):
        """
        Public method to set the SSL usage.
        
        @param on flag indicating SSL usage (boolean)
        """
        self.__ssl = on
    
    def setPassword(self, password):
        """
        Public method to set a new password.
        
        @param password password to set (string)
        """
        self.__password = pwConvert(password, encode=True)
    
    def getPassword(self):
        """
        Public method to get the password.
        
        @return password (string)
        """
        return pwConvert(self.__password, encode=False)


class IrcChannel(object):
    """
    Class implementing the IRC channel object.
    """
    def __init__(self, name):
        """
        Constructor
        
        @param name name of the network (string)
        """
        super(IrcChannel, self).__init__()
        
        self.__name = name
        self.__key = ""
        self.__autoJoin = False
    
    def save(self, settings):
        """
        Public method to save the channel data.
        
        @param settings reference to the settings object (QSettings)
        """
        # no need to save the channel name because that is the group key
        settings.setValue("Key", self.__key)
        settings.setValue("AutoJoin", self.__autoJoin)
    
    def load(self, settings):
        """
        Public method to load the network data.
        
        @param settings reference to the settings object (QSettings)
        """
        self.__key = settings.value("Key", "")
        self.__autoJoin = Preferences.toBool(settings.value("AutoJoin", False))
    
    def getName(self):
        """
        Public method to get the channel name.
        
        @return channel name (string)
        """
        return self.__name
    
    def setKey(self, key):
        """
        Public method to set a new channel key.
        
        @param key channel key to set (string)
        """
        self.__key = pwConvert(key, encode=True)
    
    def getKey(self):
        """
        Public method to get the channel key.
        
        @return channel key (string)
        """
        return pwConvert(self.__key, encode=False)
    
    def autoJoin(self):
        """
        Public method to check the auto join status.
        
        @return flag indicating if the channel should be
            joined automatically (boolean)
        """
        return self.__autoJoin
    
    def setAutoJoin(self, enable):
        """
        Public method to set the auto join status of the channel.
        
        @param enable flag indicating if the channel should be
            joined automatically (boolean)
        """
        self.__autoJoin = enable


class IrcNetwork(object):
    """
    Class implementing the IRC network object.
    """
    def __init__(self, name):
        """
        Constructor
        
        @param name name of the network (string)
        """
        super(IrcNetwork, self).__init__()
        
        self.__name = name
        self.__identity = ""
        self.__server = None
        self.__channels = {}
        self.__autoConnect = False
    
    def save(self, settings):
        """
        Public method to save the network data.
        
        @param settings reference to the settings object (QSettings)
        """
        # no need to save the network name because that is the group key
        settings.setValue("Identity", self.__identity)
        settings.setValue("AutoConnect", self.__autoConnect)
        
        settings.beginGroup("Server")
        self.__server.save(settings)
        settings.endGroup()
        
        settings.beginGroup("Channels")
        for key in self.__channels:
            settings.beginGroup(key)
            self.__channels[key].save(settings)
            settings.endGroup()
        settings.endGroup()
    
    def load(self, settings):
        """
        Public method to load the network data.
        
        @param settings reference to the settings object (QSettings)
        """
        self.__identity = settings.value("Identity", "")
        self.__autoConnect = Preferences.toBool(
            settings.value("AutoConnect", False))
        
        settings.beginGroup("Server")
        self.__server = IrcServer("")
        self.__server.load(settings)
        settings.endGroup()
        
        settings.beginGroup("Channels")
        for key in settings.childGroups():
            self.__channels[key] = IrcChannel(key)
            settings.beginGroup(key)
            self.__channels[key].load(settings)
            settings.endGroup()
        settings.endGroup()
    
    def setName(self, name):
        """
        Public method to set the network name.
        
        @param name network name (string)
        """
        self.__name = name
    
    def getName(self):
        """
        Public method to get the network name.
        
        @return network name (string)
        """
        return self.__name
    
    def setIdentityName(self, name):
        """
        Public method to set the name of the identity.
        
        @param name identity name (string)
        """
        self.__identity = name
    
    def getIdentityName(self):
        """
        Public method to get the name of the identity.
        
        @return identity name (string)
        """
        return self.__identity
    
    def getServerName(self):
        """
        Public method to get the server name.
        
        @return server name (string)
        """
        if self.__server:
            return self.__server.getName()
        else:
            return ""
    
    def getServer(self):
        """
        Public method to get the server object.
        
        @return reference to the server (IrcServer)
        """
        return self.__server
    
    def setServer(self, server):
        """
        Public method to set the server.
        
        @param server server object to set (IrcServer)
        """
        self.__server = server
    
    def setChannels(self, channels):
        """
        Public method to set the list of channels.
        
        @param channels list of channels for the network (list of IrcChannel)
        """
        self.__channels = {}
        for channel in channels:
            self.__channels[channel.getName()] = channel
    
    def getChannels(self):
        """
        Public method to get the channels.
        
        @return list of channels for the network (list of IrcChannel)
        """
        return list(copy.deepcopy(self.__channels).values())
    
    def getChannelNames(self):
        """
        Public method to get the list of channels.
        
        @return list of channel names (list of string)
        """
        return list(sorted(self.__channels.keys()))
    
    def getChannel(self, channelName):
        """
        Public method to get a channel.
        
        @param channelName name of the channel to retrieve (string)
        @return reference to the channel (IrcChannel)
        """
        if channelName in self.__channels:
            return self.__channels[channelName]
        else:
            return None
    
    def setChannel(self, channel):
        """
        Public method to set a channel.
        
        @param channel channel object to set (IrcChannel)
        """
        channelName = channel.getName()
        if channelName in self.__channels:
            self.__channels[channelName] = channel
    
    def addChannel(self, channel):
        """
        Public method to add a channel.
        
        @param channel channel object to add (IrcChannel)
        """
        channelName = channel.getName()
        if channelName not in self.__channels:
            self.__channels[channelName] = channel
    
    def deleteChannel(self, channelName):
        """
        Public method to delete the given channel.
        
        @param channelName name of the channel to be deleted (string)
        """
        if channelName in self.__channels:
            del self.__channels[channelName]
    
    def setAutoConnect(self, enable):
        """
        Public method to set the auto connect flag.
        
        @param enable flag indicate to connect to the network at start-up.
        """
        self.__autoConnect = enable
    
    def autoConnect(self):
        """
        Public method to check, if the network should be connected to at
        start-up.
        
        @return flag indicating an auto connect (boolean)
        """
        return self.__autoConnect
    
    @classmethod
    def createDefaultNetwork(cls, ssl=False):
        """
        Class method to create the default network.
        
        @param ssl flag indicating to create a SSL network configuration
            (boolean)
        @return default network object (IrcNetwork)
        """
        # network
        if ssl:
            networkName = "Freenode (SSL)"
        else:
            networkName = "Freenode"
        network = IrcNetwork(networkName)
        network.setIdentityName(IrcIdentity.DefaultIdentityName)
        
        # server
        serverName = "chat.freenode.net"
        server = IrcServer(serverName)
        if ssl:
            server.setPort(IrcServer.DefaultSslPort)
            server.setUseSSL(True)
        else:
            server.setPort(IrcServer.DefaultPort)
        network.setServer(server)
        
        # channel
        channel = IrcChannel("#eric-ide")
        channel.setAutoJoin(False)
        network.addChannel(channel)
        
        # auto connect
        network.setAutoConnect(False)
        
        return network


class IrcNetworkManager(QObject):
    """
    Class implementing the IRC identity object.
    
    @signal dataChanged() emitted after some data has changed
    @signal networksChanged() emitted after a network object has changed
    @signal identitiesChanged() emitted after an identity object has changed
    """
    dataChanged = pyqtSignal()
    networksChanged = pyqtSignal()
    identitiesChanged = pyqtSignal()
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent object (QObject)
        """
        super(IrcNetworkManager, self).__init__(parent)
        
        self.__loaded = False
        self.__saveTimer = AutoSaver(self, self.save)
        
        self.__settings = Preferences.Prefs.settings
        
        self.__networks = {}
        self.__identities = {}
        
        self.dataChanged.connect(self.__saveTimer.changeOccurred)
    
    def close(self):
        """
        Public method to close the open search engines manager.
        """
        self.__saveTimer.saveIfNeccessary()
    
    def save(self):
        """
        Public slot to save the IRC data.
        """
        if not self.__loaded:
            return
        
        # save IRC data
        self.__settings.beginGroup("IRC")
        
        # identities
        self.__settings.remove("Identities")
        self.__settings.beginGroup("Identities")
        for key in self.__identities:
            self.__settings.beginGroup(key)
            self.__identities[key].save(self.__settings)
            self.__settings.endGroup()
        self.__settings.endGroup()
        
        # networks
        self.__settings.remove("Networks")
        self.__settings.beginGroup("Networks")
        for key in self.__networks:
            self.__settings.beginGroup(key)
            self.__networks[key].save(self.__settings)
            self.__settings.endGroup()
        self.__settings.endGroup()
        
        self.__settings.endGroup()
    
    def __load(self):
        """
        Private slot to load the IRC data.
        """
        if self.__loaded:
            return
        
        # load IRC data
        self.__settings.beginGroup("IRC")
        
        # identities
        self.__settings.beginGroup("Identities")
        for key in self.__settings.childGroups():
            self.__identities[key] = IrcIdentity(key)
            self.__settings.beginGroup(key)
            self.__identities[key].load(self.__settings)
            self.__settings.endGroup()
        self.__settings.endGroup()
        
        # networks
        self.__settings.beginGroup("Networks")
        for key in self.__settings.childGroups():
            self.__networks[key] = IrcNetwork(key)
            self.__settings.beginGroup(key)
            self.__networks[key].load(self.__settings)
            self.__settings.endGroup()
        self.__settings.endGroup()
        
        self.__settings.endGroup()
        
        if not self.__identities or not self.__networks:
            # data structures got corrupted; load defaults
            self.__loadDefaults()
        
        if IrcIdentity.DefaultIdentityName not in self.__identities:
            self.__loadDefaults(identityOnly=True)
        
        self.__loaded = True
    
    def __loadDefaults(self, identityOnly=False):
        """
        Private method to load default values.
        
        @param identityOnly flag indicating to just load the default
            identity (boolean)
        """
        if not identityOnly:
            self.__networks = {}
            self.__identities = {}
        
        # identity
        identity = IrcIdentity.createDefaultIdentity()
        self.__identities[identity.getName()] = identity
        
        if not identityOnly:
            network = IrcNetwork.createDefaultNetwork()
            self.__networks[network.getName()] = network
            network = IrcNetwork.createDefaultNetwork(True)
            self.__networks[network.getName()] = network
        
        self.dataChanged.emit()
    
    ##################################################################
    ## Identity related methods below
    ##################################################################
    
    def getIdentity(self, name, create=False):
        """
        Public method to get an identity object.
        
        @param name name of the identity to get (string)
        @param create flag indicating to create a new object,
            if none exists (boolean)
        @return reference to the identity (IrcIdentity)
        """
        if not name:
            return None
        
        if not self.__loaded:
            self.__load()
        
        if name in self.__identities:
            return self.__identities[name]
        elif create:
            ircId = IrcIdentity(name)
            self.__identities[name] = ircId
            
            self.dataChanged.emit()
            
            return ircId
        else:
            return None
    
    def getIdentities(self):
        """
        Public method to get a copy of all identities.
        
        @return dictionary of all identities (dict of IrcIdentity)
        """
        return copy.deepcopy(self.__identities)
    
    def setIdentities(self, identities):
        """
        Public method to set the identities.
        
        @param identities dictionary of all identities (dict of IrcIdentity)
        """
        self.__identities = copy.deepcopy(identities)
        self.identityChanged()
        
        # Check all networks, if the identity they use is still available.
        # If it isn't, change them to use the default identity.
        for network in self.__networks.values():
            if network.getIdentityName() not in self.__identities:
                network.setIdentityName(IrcIdentity.DefaultIdentityName)
    
    def getIdentityNames(self):
        """
        Public method to get the names of all identities.
        
        @return names of all identities (list of string)
        """
        return list(self.__identities.keys())
    
    def addIdentity(self, identity):
        """
        Public method to add a new identity.
        
        @param identity reference to the identity to add (IrcIdentity)
        """
        name = identity.getName()
        self.__identities[name] = identity
        self.identityChanged()
    
    def deleteIdentity(self, name):
        """
        Public method to delete the given identity.
        
        @param name name of the identity to delete (string)
        """
        if (
            name in self.__identities and
            name != IrcIdentity.DefaultIdentityName
        ):
            del self.__identities[name]
            self.identityChanged()
    
    def renameIdentity(self, oldName, newName):
        """
        Public method to rename an identity.
        
        @param oldName old name of the identity (string)
        @param newName new name of the identity (string)
        """
        if oldName in self.__identities:
            self.__identities[newName] = self.__identities[oldName]
            del self.__identities[oldName]
            
            for network in self.__networks:
                if network.getIdentityName() == oldName:
                    network.setIdentityName(newName)
            
            self.identityChanged()
    
    def identityChanged(self):
        """
        Public method to indicate a change of an identity object.
        """
        self.dataChanged.emit()
        self.identitiesChanged.emit()
    
    ##################################################################
    ## Network related methods below
    ##################################################################
    
    def getNetwork(self, name):
        """
        Public method to get a network object.
        
        @param name name of the network (string)
        @return reference to the network object (IrcNetwork)
        """
        if not self.__loaded:
            self.__load()
        
        if name in self.__networks:
            return self.__networks[name]
        else:
            return None
    
    def setNetwork(self, network, networkName=""):
        """
        Public method to set a network.
        
        @param network network object to set (IrcNetwork)
        @param networkName name the network was known for (string)
        """
        name = network.getName()
        if networkName and name != networkName:
            # the network name has changed
            self.deleteNetwork(networkName)
            self.addNetwork(network)
        elif name in self.__networks:
            self.__networks[name] = network
            self.networkChanged()
    
    def addNetwork(self, network):
        """
        Public method to add a network.
        
        @param network network object to add (IrcNetwork)
        """
        name = network.getName()
        if name not in self.__networks:
            self.__networks[name] = network
            self.networkChanged()
    
    def deleteNetwork(self, name):
        """
        Public method to delete the given network.
        
        @param name name of the network to delete (string)
        """
        if name in self.__networks:
            del self.__networks[name]
            self.networkChanged()
    
    def networkChanged(self):
        """
        Public method to indicate a change of a network object.
        """
        self.dataChanged.emit()
        self.networksChanged.emit()
    
    def getNetworkNames(self):
        """
        Public method to get a list of all known network names.
        
        @return list of network names (list of string)
        """
        if not self.__loaded:
            self.__load()
        
        return list(sorted(self.__networks.keys()))
