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

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

"""
Package implementing the conda GUI logic.
"""

from __future__ import unicode_literals
try:
    str = unicode       # __IGNORE_EXCEPTION__
except NameError:
    pass

import json
import os

from PyQt5.QtCore import pyqtSignal, QObject, QProcess, QCoreApplication
from PyQt5.QtWidgets import QDialog

from E5Gui import E5MessageBox

import Globals
import Preferences

from . import rootPrefix, condaVersion
from .CondaExecDialog import CondaExecDialog


class Conda(QObject):
    """
    Class implementing the conda GUI logic.
    
    @signal condaEnvironmentCreated() emitted to indicate the creation of
        a new environment
    @signal condaEnvironmentRemoved() emitted to indicate the removal of
        an environment
    """
    condaEnvironmentCreated = pyqtSignal()
    condaEnvironmentRemoved = pyqtSignal()
    
    RootName = QCoreApplication.translate("Conda", "<root>")
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent parent
        @type QObject
        """
        super(Conda, self).__init__(parent)
        
        self.__ui = parent
    
    #######################################################################
    ## environment related methods below
    #######################################################################
    
    def createCondaEnvironment(self, arguments):
        """
        Public method to create a conda environment.
        
        @param arguments list of command line arguments
        @type list of str
        @return tuple containing a flag indicating success, the directory of
            the created environment (aka. prefix) and the corresponding Python
            interpreter
        @rtype tuple of (bool, str, str)
        """
        args = ["create", "--json", "--yes"] + arguments
        
        dlg = CondaExecDialog("create", self.__ui)
        dlg.start(args)
        dlg.exec_()
        ok, resultDict = dlg.getResult()
        
        if ok:
            if "actions" in resultDict and \
                    "PREFIX" in resultDict["actions"]:
                prefix = resultDict["actions"]["PREFIX"]
            elif "prefix" in resultDict:
                prefix = resultDict["prefix"]
            elif "dst_prefix" in resultDict:
                prefix = resultDict["dst_prefix"]
            else:
                prefix = ""
            
            # determine Python executable
            if prefix:
                pathPrefixes = [
                    prefix,
                    rootPrefix()
                ]
            else:
                pathPrefixes = [
                    rootPrefix()
                ]
            for pathPrefix in pathPrefixes:
                if Globals.isWindowsPlatform():
                    python = os.path.join(pathPrefix, "python.exe")
                else:
                    python = os.path.join(pathPrefix, "bin", "python")
                if os.path.exists(python):
                    break
            else:
                python = ""
            
            self.condaEnvironmentCreated.emit()
            return True, prefix, python
        else:
            return False, "", ""
    
    def removeCondaEnvironment(self, name="", prefix=""):
        """
        Public method to remove a conda environment.
        
        @param name name of the environment
        @type str
        @param prefix prefix of the environment
        @type str
        @return flag indicating success
        @rtype bool
        @exception RuntimeError raised to indicate an error in parameters
        
        Note: only one of name or prefix must be given.
        """
        if name and prefix:
            raise RuntimeError("Only one of 'name' or 'prefix' must be given.")
        
        if not name and not prefix:
            raise RuntimeError("One of 'name' or 'prefix' must be given.")
        
        args = [
            "remove",
            "--json",
            "--quiet",
            "--all",
        ]
        if name:
            args.extend(["--name", name])
        elif prefix:
            args.extend(["--prefix", prefix])
        
        exe = Preferences.getConda("CondaExecutable")
        if not exe:
            exe = "conda"
        
        proc = QProcess()
        proc.start(exe, args)
        if not proc.waitForStarted(15000):
            E5MessageBox.critical(
                self.__ui,
                self.tr("conda remove"),
                self.tr("""The conda executable could not be started."""))
            return False
        else:
            proc.waitForFinished(15000)
            output = str(proc.readAllStandardOutput(),
                         Preferences.getSystem("IOEncoding"),
                         'replace').strip()
            try:
                jsonDict = json.loads(output)
            except Exception:
                E5MessageBox.critical(
                    self.__ui,
                    self.tr("conda remove"),
                    self.tr("""The conda executable returned invalid data."""))
                return False
            
            if "error" in jsonDict:
                E5MessageBox.critical(
                    self.__ui,
                    self.tr("conda remove"),
                    self.tr("<p>The conda executable returned an error.</p>"
                            "<p>{0}</p>").format(jsonDict["message"]))
                return False
            
            if jsonDict["success"]:
                self.condaEnvironmentRemoved.emit()
            
            return jsonDict["success"]
        
        return False
    
    def getCondaEnvironmentsList(self):
        """
        Public method to get a list of all Conda environments.
        
        @return list of tuples containing the environment name and prefix
        @rtype list of tuples of (str, str)
        """
        exe = Preferences.getConda("CondaExecutable")
        if not exe:
            exe = "conda"
        
        environmentsList = []
        
        proc = QProcess()
        proc.start(exe, ["info", "--json"])
        if proc.waitForStarted(15000):
            if proc.waitForFinished(15000):
                output = str(proc.readAllStandardOutput(),
                             Preferences.getSystem("IOEncoding"),
                             'replace').strip()
                try:
                    jsonDict = json.loads(output)
                except Exception:
                    jsonDict = {}
                
                if "envs" in jsonDict:
                    for prefix in jsonDict["envs"][:]:
                        if prefix == jsonDict["root_prefix"]:
                            if not jsonDict["root_writable"]:
                                # root prefix is listed but not writable
                                continue
                            name = self.RootName
                        else:
                            name = os.path.basename(prefix)
                        
                        environmentsList.append((name, prefix))
        
        return environmentsList
    
    #######################################################################
    ## package related methods below
    #######################################################################
    
    def getInstalledPackages(self, name="", prefix=""):
        """
        Public method to get a list of installed packages of a conda
        environment.
        
        @param name name of the environment
        @type str
        @param prefix prefix of the environment
        @type str
        @return list of installed packages. Each entry is a tuple containing
            the package name, version and build.
        @rtype list of tuples of (str, str, str)
        @exception RuntimeError raised to indicate an error in parameters
        
        Note: only one of name or prefix must be given.
        """
        if name and prefix:
            raise RuntimeError("Only one of 'name' or 'prefix' must be given.")
        
        if not name and not prefix:
            raise RuntimeError("One of 'name' or 'prefix' must be given.")
        
        args = [
            "list",
            "--json",
        ]
        if name:
            args.extend(["--name", name])
        elif prefix:
            args.extend(["--prefix", prefix])
        
        exe = Preferences.getConda("CondaExecutable")
        if not exe:
            exe = "conda"
        
        packages = []
        
        proc = QProcess()
        proc.start(exe, args)
        if proc.waitForStarted(15000):
            if proc.waitForFinished(30000):
                output = str(proc.readAllStandardOutput(),
                             Preferences.getSystem("IOEncoding"),
                             'replace').strip()
                try:
                    jsonList = json.loads(output)
                except Exception:
                    jsonList = []
                
                for package in jsonList:
                    if isinstance(package, dict):
                        packages.append((
                            package["name"],
                            package["version"],
                            package["build_string"]
                        ))
                    else:
                        parts = package.rsplit("-", 2)
                        while len(parts) < 3:
                            parts.append("")
                        packages.append(tuple(parts))
        
        return packages
    
    def getUpdateablePackages(self, name="", prefix=""):
        """
        Public method to get a list of updateable packages of a conda
        environment.
        
        @param name name of the environment
        @type str
        @param prefix prefix of the environment
        @type str
        @return list of installed packages. Each entry is a tuple containing
            the package name, version and build.
        @rtype list of tuples of (str, str, str)
        @exception RuntimeError raised to indicate an error in parameters
        
        Note: only one of name or prefix must be given.
        """
        if name and prefix:
            raise RuntimeError("Only one of 'name' or 'prefix' must be given.")
        
        if not name and not prefix:
            raise RuntimeError("One of 'name' or 'prefix' must be given.")
        
        args = [
            "update",
            "--json",
            "--quiet",
            "--all",
            "--dry-run",
        ]
        if name:
            args.extend(["--name", name])
        elif prefix:
            args.extend(["--prefix", prefix])
        
        exe = Preferences.getConda("CondaExecutable")
        if not exe:
            exe = "conda"
        
        packages = []
        
        proc = QProcess()
        proc.start(exe, args)
        if proc.waitForStarted(15000):
            if proc.waitForFinished(30000):
                output = str(proc.readAllStandardOutput(),
                             Preferences.getSystem("IOEncoding"),
                             'replace').strip()
                try:
                    jsonDict = json.loads(output)
                except Exception:
                    jsonDict = {}
                
                if "actions" in jsonDict and "LINK" in jsonDict["actions"]:
                    for linkEntry in jsonDict["actions"]["LINK"]:
                        if isinstance(linkEntry, dict):
                            packages.append((
                                linkEntry["name"],
                                linkEntry["version"],
                                linkEntry["build_string"]
                            ))
                        else:
                            package = linkEntry.split()[0]
                            parts = package.rsplit("-", 2)
                            while len(parts) < 3:
                                parts.append("")
                            packages.append(tuple(parts))
        
        return packages
    
    def updatePackages(self, packages, name="", prefix=""):
        """
        Public method to update packages of a conda environment.
        
        @param packages list of package names to be updated
        @type list of str
        @param name name of the environment
        @type str
        @param prefix prefix of the environment
        @type str
        @return flag indicating success
        @rtype bool
        @exception RuntimeError raised to indicate an error in parameters
        
        Note: only one of name or prefix must be given.
        """
        if name and prefix:
            raise RuntimeError("Only one of 'name' or 'prefix' must be given.")
        
        if not name and not prefix:
            raise RuntimeError("One of 'name' or 'prefix' must be given.")
        
        if packages:
            args = [
                "update",
                "--json",
                "--yes",
            ]
            if name:
                args.extend(["--name", name])
            elif prefix:
                args.extend(["--prefix", prefix])
            args.extend(packages)
            
            dlg = CondaExecDialog("update", self.__ui)
            dlg.start(args)
            dlg.exec_()
            ok, _ = dlg.getResult()
        else:
            ok = False
        
        return ok
    
    def updateAllPackages(self, name="", prefix=""):
        """
        Public method to update all packages of a conda environment.
        
        @param name name of the environment
        @type str
        @param prefix prefix of the environment
        @type str
        @return flag indicating success
        @rtype bool
        @exception RuntimeError raised to indicate an error in parameters
        
        Note: only one of name or prefix must be given.
        """
        if name and prefix:
            raise RuntimeError("Only one of 'name' or 'prefix' must be given.")
        
        if not name and not prefix:
            raise RuntimeError("One of 'name' or 'prefix' must be given.")
        
        args = [
            "update",
            "--json",
            "--yes",
            "--all"
        ]
        if name:
            args.extend(["--name", name])
        elif prefix:
            args.extend(["--prefix", prefix])
        
        dlg = CondaExecDialog("update", self.__ui)
        dlg.start(args)
        dlg.exec_()
        ok, _ = dlg.getResult()
        
        return ok
    
    def installPackages(self, packages, name="", prefix=""):
        """
        Public method to install packages into a conda environment.
        
        @param packages list of package names to be installed
        @type list of str
        @param name name of the environment
        @type str
        @param prefix prefix of the environment
        @type str
        @return flag indicating success
        @rtype bool
        @exception RuntimeError raised to indicate an error in parameters
        
        Note: only one of name or prefix must be given.
        """
        if name and prefix:
            raise RuntimeError("Only one of 'name' or 'prefix' must be given.")
        
        if not name and not prefix:
            raise RuntimeError("One of 'name' or 'prefix' must be given.")
        
        if packages:
            args = [
                "install",
                "--json",
                "--yes",
            ]
            if name:
                args.extend(["--name", name])
            elif prefix:
                args.extend(["--prefix", prefix])
            args.extend(packages)
            
            dlg = CondaExecDialog("install", self.__ui)
            dlg.start(args)
            dlg.exec_()
            ok, _ = dlg.getResult()
        else:
            ok = False
        
        return ok
    
    def uninstallPackages(self, packages, name="", prefix=""):
        """
        Public method to uninstall packages of a conda environment (including
        all no longer needed dependencies).
        
        @param packages list of package names to be uninstalled
        @type list of str
        @param name name of the environment
        @type str
        @param prefix prefix of the environment
        @type str
        @return flag indicating success
        @rtype bool
        @exception RuntimeError raised to indicate an error in parameters
        
        Note: only one of name or prefix must be given.
        """
        if name and prefix:
            raise RuntimeError("Only one of 'name' or 'prefix' must be given.")
        
        if not name and not prefix:
            raise RuntimeError("One of 'name' or 'prefix' must be given.")
        
        if packages:
            from UI.DeleteFilesConfirmationDialog import \
                DeleteFilesConfirmationDialog
            dlg = DeleteFilesConfirmationDialog(
                self.parent(),
                self.tr("Uninstall Packages"),
                self.tr(
                    "Do you really want to uninstall these packages and"
                    " their dependencies?"),
                packages)
            if dlg.exec_() == QDialog.Accepted:
                args = [
                    "remove",
                    "--json",
                    "--yes",
                ]
                if condaVersion() >= (4, 4, 0):
                    args.append("--prune",)
                if name:
                    args.extend(["--name", name])
                elif prefix:
                    args.extend(["--prefix", prefix])
                args.extend(packages)
                
                dlg = CondaExecDialog("remove", self.__ui)
                dlg.start(args)
                dlg.exec_()
                ok, _ = dlg.getResult()
            else:
                ok = False
        else:
            ok = False
        
        return ok
    
    def searchPackages(self, pattern, fullNameOnly=False, packageSpec=False,
                       platform="", name="", prefix=""):
        """
        Public method to search for a package pattern of a conda environment.
        
        @param pattern package search pattern
        @type str
        @param fullNameOnly flag indicating to search for full names only
        @type bool
        @param packageSpec flag indicating to search a package specification
        @type bool
        @param platform type of platform to be searched for
        @type str
        @param name name of the environment
        @type str
        @param prefix prefix of the environment
        @type str
        @return flag indicating success and a dictionary with package name as
            key and list of dictionaries containing detailed data for the found
            packages as values
        @rtype tuple of (bool, dict of list of dict)
        @exception RuntimeError raised to indicate an error in parameters
        
        Note: only one of name or prefix must be given.
        """
        if name and prefix:
            raise RuntimeError("Only one of 'name' or 'prefix' must be given.")
        
        args = [
            "search",
            "--json",
        ]
        if fullNameOnly:
            args.append("--full-name")
        if packageSpec:
            args.append("--spec")
        if platform:
            args.extend(["--platform", platform])
        if name:
            args.extend(["--name", name])
        elif prefix:
            args.extend(["--prefix", prefix])
        args.append(pattern)
        
        exe = Preferences.getConda("CondaExecutable")
        if not exe:
            exe = "conda"
        
        packages = {}
        ok = False
        
        proc = QProcess()
        proc.start(exe, args)
        if proc.waitForStarted(15000):
            if proc.waitForFinished(30000):
                output = str(proc.readAllStandardOutput(),
                             Preferences.getSystem("IOEncoding"),
                             'replace').strip()
                try:
                    packages = json.loads(output)
                    ok = "error" not in packages
                except Exception:
                    # return values for errors is already set
                    pass
        
        return ok, packages
    
    #######################################################################
    ## special methods below
    #######################################################################
    
    def updateConda(self):
        """
        Public method to update conda itself.
        
        @return flag indicating success
        @rtype bool
        """
        args = [
            "update",
            "--json",
            "--yes",
            "conda"
        ]
        
        dlg = CondaExecDialog("update", self.__ui)
        dlg.start(args)
        dlg.exec_()
        ok, _ = dlg.getResult()
        
        return ok
    
    def writeDefaultConfiguration(self):
        """
        Public method to create a conda configuration with default values.
        """
        args = [
            "config",
            "--write-default",
            "--quiet"
        ]
        
        exe = Preferences.getConda("CondaExecutable")
        if not exe:
            exe = "conda"
        
        proc = QProcess()
        proc.start(exe, args)
        proc.waitForStarted(15000)
        proc.waitForFinished(30000)
    
    def getCondaInformation(self):
        """
        Public method to get a dictionary containing information about conda.
        
        @return dictionary containing information about conda
        @rtype dict
        """
        exe = Preferences.getConda("CondaExecutable")
        if not exe:
            exe = "conda"
        
        infoDict = {}
        
        proc = QProcess()
        proc.start(exe, ["info", "--json"])
        if proc.waitForStarted(15000):
            if proc.waitForFinished(30000):
                output = str(proc.readAllStandardOutput(),
                             Preferences.getSystem("IOEncoding"),
                             'replace').strip()
                try:
                    infoDict = json.loads(output)
                except Exception:
                    infoDict = {}
        
        return infoDict
    
    def runProcess(self, args):
        """
        Public method to execute the conda with the given arguments.
        
        The conda executable is called with the given arguments and
        waited for its end.
        
        @param args list of command line arguments
        @type list of str
        @return tuple containing a flag indicating success and the output
            of the process
        @rtype tuple of (bool, str)
        """
        exe = Preferences.getConda("CondaExecutable")
        if not exe:
            exe = "conda"
        
        process = QProcess()
        process.start(exe, args)
        procStarted = process.waitForStarted(15000)
        if procStarted:
            finished = process.waitForFinished(30000)
            if finished:
                if process.exitCode() == 0:
                    output = str(process.readAllStandardOutput(),
                                 Preferences.getSystem("IOEncoding"),
                                 'replace').strip()
                    return True, output
                else:
                    return (False,
                            self.tr("conda exited with an error ({0}).")
                            .format(process.exitCode()))
            else:
                process.terminate()
                process.waitForFinished(2000)
                process.kill()
                process.waitForFinished(3000)
                return False, self.tr("conda did not finish within"
                                      " 30 seconds.")
        
        return False, self.tr("conda could not be started.")
    
    def cleanConda(self, cleanAction):
        """
        Public method to update conda itself.
        
        @param cleanAction cleaning action to be performed (must be one of
            the command line parameters without '--')
        @type str
        """
        args = [
            "clean",
            "--yes",
            "--{0}".format(cleanAction),
        ]
        
        dlg = CondaExecDialog("clean", self.__ui)
        dlg.start(args)
        dlg.exec_()
