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

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

"""
Module implementing a class to manage Python virtual environments.
"""

from __future__ import unicode_literals

import os
import sys
import shutil
import json
import copy

from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtWidgets import QDialog

from E5Gui import E5MessageBox

import Preferences


class VirtualenvManager(QObject):
    """
    Class implementing an object to manage Python virtual environments.
    """
    DefaultKey = "<default>"
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent object
        @type QWidget
        """
        super(VirtualenvManager, self).__init__(parent)
        
        self.__ui = parent
        
        self.__loadSettings()
        
        self.__virtualenvManagerDialog = None
    
    def __loadSettings(self):
        """
        Private slot to load the virtual environments.
        """
        venvString = Preferences.Prefs.settings.value(
            "PyVenv/VirtualEnvironments", "{}")     # __IGNORE_WARNING_M613__
        environments = json.loads(venvString)
        
        self.__virtualEnvironments = {}
        # each environment entry is a dictionary:
        #   path:           the directory of the virtual environment
        #                   (empty for a global environment)
        #   interpreter:    the path of the Python interpreter
        #   variant:        Python variant (2 or 3)
        #   is_global:      a flag indicating a global environment
        #   is_conda:       a flag indicating an Anaconda environment
        #   exec_path:      a string to be prefixed to the PATH environment
        #                   setting
        #
        for venvName in environments:
            interpreter = environments[venvName]["interpreter"]
            if os.access(interpreter, os.X_OK):
                environment = environments[venvName]
                if "is_global" not in environment:
                    environment["is_global"] = environment["path"] == ""
                if "is_conda" not in environment:
                    environment["is_conda"] = False
                if "exec_path" not in environment:
                    environment["exec_path"] = ""
                self.__virtualEnvironments[venvName] = environment
        
        # check, if the interpreter used to run eric is in the environments
        defaultPy = sys.executable.replace("w.exe", ".exe")
        found = False
        for venvName in self.__virtualEnvironments:
            if (defaultPy ==
                    self.__virtualEnvironments[venvName]["interpreter"]):
                found = True
                break
        if not found:
            # add an environment entry for the default interpreter
            self.__virtualEnvironments[VirtualenvManager.DefaultKey] = {
                "path": "",
                "interpreter": defaultPy,
                "variant": sys.version_info[0],
                "is_global": True,
                "is_conda": False,
                "exec_path": "",
            }
        
        self.__saveSettings()
    
    def __saveSettings(self):
        """
        Private slot to save the virtual environments.
        """
        Preferences.Prefs.settings.setValue(
            "PyVenv/VirtualEnvironments",
            json.dumps(self.__virtualEnvironments)
        )
    
    def getDefaultEnvironment(self):
        """
        Public method to get the default virtual environment.
        
        Default is an environment with the key '<default>' or the first one
        having an interpreter matching sys.executable (i.e. the one used to
        execute eric6 with)
        
        @return tuple containing the environment name and a dictionary
            containing a copy of the default virtual environment
        @rtype tuple of (str, dict)
        """
        if VirtualenvManager.DefaultKey in self.__virtualEnvironments:
            return (
                VirtualenvManager.DefaultKey,
                copy.copy(
                    self.__virtualEnvironments[VirtualenvManager.DefaultKey])
            )
        
        else:
            defaultPy = sys.executable.replace("w.exe", ".exe")
            for venvName in self.__virtualEnvironments:
                if (defaultPy ==
                        self.__virtualEnvironments[venvName]["interpreter"]):
                    return (
                        venvName,
                        copy.copy(self.__virtualEnvironments[venvName])
                    )
        
        return ("", {})
    
    @pyqtSlot()
    def createVirtualEnv(self):
        """
        Public slot to create a new virtual environment.
        """
        from .VirtualenvConfigurationDialog import \
            VirtualenvConfigurationDialog
        
        dlg = VirtualenvConfigurationDialog()
        if dlg.exec_() == QDialog.Accepted:
            (pyvenv, args, name, openTarget, createLog, createScript,
             targetDir, interpreter) = dlg.getData()
            
            # now do the call
            from .VirtualenvExecDialog import VirtualenvExecDialog
            dia = VirtualenvExecDialog(pyvenv, targetDir, name, openTarget,
                                       createLog, createScript, interpreter,
                                       self)
            dia.show()
            dia.start(args)
            dia.exec_()
    
    def addVirtualEnv(self, venvName, venvDirectory, venvInterpreter="",
                      venvVariant=3, isGlobal=False, isConda=False,
                      execPath=""):
        """
        Public method to add a virtual environment.
        
        @param venvName logical name for the virtual environment
        @type str
        @param venvDirectory directory of the virtual environment
        @type str
        @param venvInterpreter interpreter of the virtual environment
        @type str
        @param venvVariant Python variant of the virtual environment
        @type int
        @param isGlobal flag indicating a global environment
        @type bool
        @param isConda flag indicating an Anaconda virtual environment
        @type bool
        @param execPath search path string to be prepended to the PATH
            environment variable
        @type str
        """
        if venvName in self.__virtualEnvironments:
            ok = E5MessageBox.yesNo(
                None,
                self.tr("Add Virtual Environment"),
                self.tr("""A virtual environment named <b>{0}</b> exists"""
                        """ already. Shall it be replaced?""")
                .format(venvName),
                icon=E5MessageBox.Warning)
            if not ok:
                return
        
        if not venvInterpreter:
            from .VirtualenvInterpreterSelectionDialog import \
                VirtualenvInterpreterSelectionDialog
            dlg = VirtualenvInterpreterSelectionDialog(venvName, venvDirectory)
            if dlg.exec_() == QDialog.Accepted:
                venvInterpreter, venvVariant = dlg.getData()
                isGlobal = True
        
        if venvInterpreter:
            self.__virtualEnvironments[venvName] = {
                "path": venvDirectory,
                "interpreter": venvInterpreter,
                "variant": venvVariant,
                "is_global": isGlobal,
                "is_conda": isConda,
                "exec_path": execPath,
            }
            
            self.__saveSettings()
            
            if self.__virtualenvManagerDialog:
                self.__virtualenvManagerDialog.refresh()
    
    def setVirtualEnv(self, venvName, venvDirectory, venvInterpreter,
                      venvVariant, isGlobal, isConda, execPath):
        """
        Public method to change a virtual environment.
        
        @param venvName logical name of the virtual environment
        @type str
        @param venvDirectory directory of the virtual environment
        @type str
        @param venvInterpreter interpreter of the virtual environment
        @type str
        @param venvVariant Python variant of the virtual environment
        @type int
        @param isGlobal flag indicating a global environment
        @type bool
        @param isConda flag indicating an Anaconda virtual environment
        @type bool
        @param execPath search path string to be prepended to the PATH
            environment variable
        @type str
        """
        if venvName not in self.__virtualEnvironments:
            E5MessageBox.yesNo(
                None,
                self.tr("Change Virtual Environment"),
                self.tr("""A virtual environment named <b>{0}</b> does not"""
                        """ exist. Aborting!""")
                .format(venvName),
                icon=E5MessageBox.Warning)
            return
        
        self.__virtualEnvironments[venvName] = {
            "path": venvDirectory,
            "interpreter": venvInterpreter,
            "variant": venvVariant,
            "is_global": isGlobal,
            "is_conda": isConda,
            "exec_path": execPath,
        }
        
        self.__saveSettings()
        
        if self.__virtualenvManagerDialog:
            self.__virtualenvManagerDialog.refresh()
    
    def renameVirtualEnv(self, oldVenvName, venvName, venvDirectory,
                         venvInterpreter, venvVariant, isGlobal, isConda,
                         execPath):
        """
        Public method to substitute a virtual environment entry with a new
        name.
        
        @param oldVenvName old name of the virtual environment
        @type str
        @param venvName logical name for the virtual environment
        @type str
        @param venvDirectory directory of the virtual environment
        @type str
        @param venvInterpreter interpreter of the virtual environment
        @type str
        @param venvVariant Python variant of the virtual environment
        @type int
        @param isGlobal flag indicating a global environment
        @type bool
        @param isConda flag indicating an Anaconda virtual environment
        @type bool
        @param execPath search path string to be prepended to the PATH
            environment variable
        @type str
        """
        if oldVenvName not in self.__virtualEnvironments:
            E5MessageBox.yesNo(
                None,
                self.tr("Rename Virtual Environment"),
                self.tr("""A virtual environment named <b>{0}</b> does not"""
                        """ exist. Aborting!""")
                .format(oldVenvName),
                icon=E5MessageBox.Warning)
            return
        
        del self.__virtualEnvironments[oldVenvName]
        self.addVirtualEnv(venvName, venvDirectory, venvInterpreter,
                           venvVariant, isGlobal, isConda, execPath)
    
    def deleteVirtualEnvs(self, venvNames):
        """
        Public method to delete virtual environments from the list and disk.
        
        @param venvNames list of logical names for the virtual environments
        @type list of str
        """
        venvMessages = []
        for venvName in venvNames:
            if venvName in self.__virtualEnvironments and \
                    bool(self.__virtualEnvironments[venvName]["path"]):
                venvMessages.append(self.tr("{0} - {1}").format(
                    venvName, self.__virtualEnvironments[venvName]["path"]))
        if venvMessages:
            from UI.DeleteFilesConfirmationDialog import \
                DeleteFilesConfirmationDialog
            dlg = DeleteFilesConfirmationDialog(
                None,
                self.tr("Delete Virtual Environments"),
                self.tr("""Do you really want to delete these virtual"""
                        """ environments?"""),
                venvMessages
            )
            if dlg.exec_() == QDialog.Accepted:
                for venvName in venvNames:
                    if self.__isEnvironmentDeleteable(venvName):
                        shutil.rmtree(
                            self.__virtualEnvironments[venvName]["path"], True)
                        del self.__virtualEnvironments[venvName]
                
                self.__saveSettings()
                
                if self.__virtualenvManagerDialog:
                    self.__virtualenvManagerDialog.refresh()
    
    def __isEnvironmentDeleteable(self, venvName):
        """
        Private method to check, if a virtual environment can be deleted from
        disk.
        
        @param venvName name of the virtual environment
        @type str
        @return flag indicating it can be deleted
        @rtype bool
        """
        ok = False
        if venvName in self.__virtualEnvironments:
            ok = True
            ok &= bool(self.__virtualEnvironments[venvName]["path"])
            ok &= not self.__virtualEnvironments[venvName]["is_global"]
            ok &= os.access(self.__virtualEnvironments[venvName]["path"],
                            os.W_OK)
        
        return ok
    
    def removeVirtualEnvs(self, venvNames):
        """
        Public method to delete virtual environment from the list.
        
        @param venvNames list of logical names for the virtual environments
        @type list of str
        """
        venvMessages = []
        for venvName in venvNames:
            if venvName in self.__virtualEnvironments:
                venvMessages.append(self.tr("{0} - {1}").format(
                    venvName, self.__virtualEnvironments[venvName]["path"]))
        if venvMessages:
            from UI.DeleteFilesConfirmationDialog import \
                DeleteFilesConfirmationDialog
            dlg = DeleteFilesConfirmationDialog(
                None,
                self.tr("Remove Virtual Environments"),
                self.tr("""Do you really want to remove these virtual"""
                        """ environments?"""),
                venvMessages
            )
            if dlg.exec_() == QDialog.Accepted:
                for venvName in venvNames:
                    if venvName in self.__virtualEnvironments:
                        del self.__virtualEnvironments[venvName]
                
                self.__saveSettings()
                
                if self.__virtualenvManagerDialog:
                    self.__virtualenvManagerDialog.refresh()
    
    def getEnvironmentEntries(self):
        """
        Public method to get a dictionary containing the defined virtual
        environment entries.
        
        @return dictionary containing a copy of the defined virtual
            environments
        @rtype dict
        """
        return copy.deepcopy(self.__virtualEnvironments)
    
    @pyqtSlot()
    def showVirtualenvManagerDialog(self, modal=False):
        """
        Public slot to show the virtual environment manager dialog.
        
        @param modal flag indicating that the dialog should be shown in
            a blocking mode
        """
        if self.__virtualenvManagerDialog is None:
            from .VirtualenvManagerDialog import VirtualenvManagerDialog
            self.__virtualenvManagerDialog = VirtualenvManagerDialog(
                self, self.__ui)
        
        if modal:
            self.__virtualenvManagerDialog.exec_()
        else:
            self.__virtualenvManagerDialog.show()
    
    def shutdown(self):
        """
        Public method to shutdown the manager.
        """
        if self.__virtualenvManagerDialog is not None:
            self.__virtualenvManagerDialog.close()
            self.__virtualenvManagerDialog = None
    
    def isUnique(self, venvName):
        """
        Public method to check, if the give logical name is unique.
        
        @param venvName logical name for the virtual environment
        @type str
        @return flag indicating uniqueness
        @rtype bool
        """
        return venvName not in self.__virtualEnvironments
    
    def getVirtualenvInterpreter(self, venvName):
        """
        Public method to get the interpreter for a virtual environment.
        
        @param venvName logical name for the virtual environment
        @type str
        @return interpreter path
        @rtype str
        """
        if venvName in self.__virtualEnvironments:
            return self.__virtualEnvironments[venvName]["interpreter"]
        else:
            return ""
    
    def getVirtualenvDirectory(self, venvName):
        """
        Public method to get the directory of a virtual environment.
        
        @param venvName logical name for the virtual environment
        @type str
        @return directory path
        @rtype str
        """
        if venvName in self.__virtualEnvironments:
            return self.__virtualEnvironments[venvName]["path"]
        else:
            return ""
    
    def getVirtualenvNames(self):
        """
        Public method to get a list of defined virtual environments.
        
        @return list of defined virtual environments
        @rtype list of str
        """
        return list(self.__virtualEnvironments.keys())
    
    def getVirtualenvNamesForVariant(self, variant):
        """
        Public method to get a list of virtual environments for a given
        Python variant.
        
        @param variant Python variant (2 or 3)
        @type int
        @return list of defined virtual environments
        @rtype list of str
        """
        assert variant in (2, 3)
        
        environments = []
        for venvName in self.__virtualEnvironments:
            if self.__virtualEnvironments[venvName]["variant"] == variant:
                environments.append(venvName)
        
        return environments
    
    def isGlobalEnvironment(self, venvName):
        """
        Public method to test, if a given environment is a global one.
        
        @param venvName logical name of the virtual environment
        @type str
        @return flag indicating a global environment
        @rtype bool
        """
        if venvName in self.__virtualEnvironments:
            return self.__virtualEnvironments[venvName]["is_global"]
        else:
            return False
    
    def isCondaEnvironment(self, venvName):
        """
        Public method to test, if a given environment is an Anaconda
        environment.
        
        @param venvName logical name of the virtual environment
        @type str
        @return flag indicating an Anaconda environment
        @rtype bool
        """
        if venvName in self.__virtualEnvironments:
            return self.__virtualEnvironments[venvName]["is_conda"]
        else:
            return False
    
    def getVirtualenvExecPath(self, venvName):
        """
        Public method to get the search path prefix of a virtual environment.
        
        @param venvName logical name for the virtual environment
        @type str
        @return search path prefix
        @rtype str
        """
        if venvName in self.__virtualEnvironments:
            return self.__virtualEnvironments[venvName]["exec_path"]
        else:
            return ""
