import os
import re
import select
import shlex
import subprocess
import sys
import threading

import AppKit  # type: ignore
from PyObjCTools.AppHelper import callAfter
from vanilla import *
from vanilla.dialogs import message

from drawBot.ui.codeEditor import OutPutEditor as OutputEditor

welcomeText = """\
Welcome to pip! Pip installs Python Packages from the PyPI database.

Enter one or more package names in the text field, choose an action from \
the popup button on the left, then click “Go!” or hit return or enter.
"""


class PipInstallerController:
    def __init__(self, targetPath):
        self.targetPath = targetPath
        self._isRunning = False

        self.w = Window((640, 300), "Install Python Packages", minSize=(640, 300), autosaveName="PipInstaller")
        self.w.getNSWindow().setTitleVisibility_(True)

        items = [
            # "Search PyPI",  # disable for now, https://status.python.org/incidents/grk0k7sz6zkp
            "Install / Upgrade",
            "Uninstall",
            "Show Package Info",
        ]
        self.pipCommandNames = [f"pip{re.sub(r'[ /]', '', item)}Command" for item in items]
        self.pipCommandsButton = PopUpButton((0, 0, 0, 0), items)
        self.pipCommandsButton.getNSPopUpButton().setBezelStyle_(AppKit.NSBezelStyleTexturedRounded)
        self.pipCommandsButton.getNSPopUpButton().setFrame_((((0, 0), (140, 35))))

        self.textEntry = EditText(
            (0, 0, 0, 0), placeholder="Enter one or more package names", callback=self.textEntryCallback
        )
        self.textEntry.getNSTextField().setBezelStyle_(AppKit.NSTextFieldRoundedBezel)
        self.textEntry.getNSTextField().setFrame_((((0, 0), (200, 35))))

        self.goButton = Button((0, 0, 0, 0), "Go!", callback=self.goButtonCallback)
        self.goButton.enable(False)
        self.goButton.getNSButton().setFrame_((((0, 0), (50, 35))))

        items = [
            dict(title="List Our Installed Packages (pip freeze)", callback=self.pipFreezeCallback),
            dict(title="Show Pip Version (pip --version)", callback=self.pipVersionCallback),
            "----",
            dict(title="Reveal Install Folder in Finder", callback=self.revealInstallFolderCallback),
        ]
        self.extraActionButton = ActionButton((0, 0, 0, 0), items)
        self.extraActionButton.getNSPopUpButton().setFrame_((((0, 0), (40, 35))))

        self.progressSpinner = ProgressSpinner((0, 0, 0, 0))
        self.progressSpinner.getNSProgressIndicator().setFrame_((((0, 0), (20, 20))))

        toolbarItems = [
            dict(
                itemIdentifier="pipCommands",
                label="Pip Commands",
                view=self.pipCommandsButton.getNSPopUpButton(),
            ),
            dict(
                itemIdentifier="pipTextEntry",
                label="Pip",
                view=self.textEntry.getNSTextField(),
            ),
            dict(
                itemIdentifier="pipGo",
                label="Pip",
                view=self.goButton.getNSButton(),
            ),
            dict(
                itemIdentifier="pipSpinner",
                label="Pip",
                view=self.progressSpinner.getNSProgressIndicator(),
            ),
            dict(
                itemIdentifier="pipExtraActions",
                label="Pip Actions",
                view=self.extraActionButton.getNSPopUpButton(),
            ),
        ]

        items = self.w.addToolbar(
            toolbarIdentifier="PipInstallerToolbar", toolbarItems=toolbarItems, addStandardItems=False
        )

        items["pipTextEntry"].setMinSize_((150, 22))
        items["pipTextEntry"].setMaxSize_((2000, 22))

        self.w.getNSWindow().toolbar().setShowsBaselineSeparator_(False)

        self.w.outputField = OutputEditor((0, 0, -0, -20), readOnly=True)
        self.w.resultCodeField = TextBox((10, -18, 200, 0), "", sizeStyle="small")

        self.w.getNSWindow().makeFirstResponder_(self.textEntry.getNSTextField())
        self.w.setDefaultButton(self.goButton)
        self.w.open()

        self.w.bind("should close", self.windowShouldClose)
        self.stdoutWrite(welcomeText)

    def show(self):
        if self.w._window is None:
            # We have been closed, let's start over
            self.__init__(self.targetPath)
        else:
            self.w.show()

    def windowShouldClose(self, window):
        if self.isRunning:
            message("Window can’t be closed", "The ‘pip’ process is still running.", parentWindow=self.w)
            return False
        return True

    @property
    def isRunning(self):
        return self._isRunning

    @isRunning.setter
    def isRunning(self, onoff):
        self._isRunning = onoff
        if onoff:
            self.progressSpinner.start()
        else:
            self.progressSpinner.stop()
        self.goButton.enable(bool(self.textEntry.get()) and not onoff)

    def getUserArguments(self):
        try:
            return shlex.split(self.textEntry.get())
        except ValueError:
            # shlex syntax error: ignore and return the entire string as a single argument
            return [self.textEntry.get()]

    def goButtonCallback(self, sender):
        commandName = self.pipCommandNames[self.pipCommandsButton.get()]
        getattr(self, commandName)(self.getUserArguments())

    def textEntryCallback(self, sender):
        if self.isRunning:
            return
        self.goButton.enable(bool(self.getUserArguments()))

    def pipSearchPyPICommand(self, userArguments):
        self.callPip(["search"] + userArguments)

    def pipInstallUpgradeCommand(self, userArguments):
        if not os.path.exists(self.targetPath):
            os.makedirs(self.targetPath)
            if self.targetPath not in sys.path:
                sys.path.append(self.targetPath)
        self.callPip(["install", "--upgrade", "--target", self.targetPath] + userArguments)

    def pipShowPackageInfoCommand(self, userArguments):
        self.callPip(["show"] + userArguments)

    def pipFreezeCallback(self, sender):
        self.callPip(["freeze", "--path", self.targetPath])

    def pipUninstallCommand(self, userArguments):
        packageNames = [arg.lower() for arg in userArguments if not arg.startswith("-")]
        extraArguments = [arg for arg in userArguments if arg.startswith("-")]
        outputLines = []

        def collectOutput(data):
            outputLines.append(data)

        def doneShowCallback(resultCode):
            if resultCode != 0:
                self.stderrWrite("".join(outputLines))
                self.setResultCode(resultCode)
                self.isRunning = False
                return
            packages = {}
            nameTag = "Name:"
            locationTag = "Location:"
            for line in outputLines:
                if line.startswith(nameTag):
                    name = line[len(nameTag) :].strip()
                elif line.startswith(locationTag):
                    location = line[len(locationTag) :].strip()
                    assert name is not None
                    packages[name.lower()] = location
                    name = None
            packageNamesNotFound = {name for name in packageNames if name not in packages}
            packageNamesBad = [name for name in packageNames if name in packages and packages[name] != self.targetPath]
            packageNamesGood = [name for name in packageNames if name in packages and packages[name] == self.targetPath]
            for name in packageNamesNotFound:
                self.stderrWrite(f"Skipping {name} as it is not installed\n")
            for name in packageNamesBad:
                self.stderrWrite(f"Skipping {name} as it is not installed in {self.targetPath}\n")
            if packageNamesGood:
                self.callPip(["uninstall", "-y"] + extraArguments + packageNamesGood, clearOutput=False)
            else:
                self.isRunning = False

        self.w.outputField.clear()
        self.isRunning = True
        self.setResultCode("--")
        callPip(["show"] + packageNames, collectOutput, collectOutput, doneShowCallback)

    def pipVersionCallback(self, sender):
        self.callPip(["--version"])

    def revealInstallFolderCallback(self, sender):
        if not os.path.exists(self.targetPath):
            message(
                "The install folder does not yet exists.", "Try again after installing a package.", parentWindow=self.w
            )
        else:
            AppKit.NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(None, self.targetPath)

    def callPip(self, arguments, clearOutput=True):
        if clearOutput:
            self.w.outputField.clear()
        self.isRunning = True
        self.setResultCode("--")
        callPip(arguments, self.stdoutWrite, self.stderrWrite, self.resultCallback)

    def stdoutWrite(self, data):
        self.w.outputField.append(data)
        self.w.outputField.scrollToEnd()

    def stderrWrite(self, data):
        self.w.outputField.append(data, isError=True)
        self.w.outputField.scrollToEnd()

    def resultCallback(self, resultCode):
        self.setResultCode(resultCode)
        self.isRunning = False
        if resultCode == 23:  # special pip error code
            self.stdoutWrite("No results.\n")

    def setResultCode(self, resultCode):
        self.w.resultCodeField.set(f"pip result code: {resultCode}")


def callPip(arguments, stdoutCallback, stderrCallback, resultCallback):
    arguments = [sys.executable, "-m", "pip", "--disable-pip-version-check", "--isolated"] + arguments
    env = dict(PYTHONPATH=":".join(sys.path), PYTHONHOME=sys.prefix, PATH="/usr/bin")
    callExternalProcess("pip", arguments, env, stdoutCallback, stderrCallback, resultCallback)


def _testTimeout():
    arguments = [sys.executable, "-c", "import time; print('Hiiiiiii'); time.sleep(3)"]
    env = dict(PYTHONPATH=":".join(sys.path), PATH="/usr/bin")
    callExternalProcess("pip", arguments, env, print, print, print, timeout=1)


def callExternalProcess(name, arguments, env, stdoutCallback, stderrCallback, resultCallback, timeout=120):
    def worker():
        process = subprocess.Popen(
            arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, encoding="utf-8", bufsize=1
        )
        readers = [process.stdout, process.stderr]
        while readers:
            readyReaders, _, _ = select.select(readers, [], [], timeout)
            if not readyReaders:
                process.kill()
                callAfter(stderrCallback, f"The {name} process timed out (in select.select())")
                callAfter(resultCallback, -1)
                return
            for reader in readyReaders:
                data = reader.readline()
                if not data:
                    readers.remove(reader)
                    continue
                if reader is process.stderr:
                    callAfter(stderrCallback, data)
                else:
                    callAfter(stdoutCallback, data)
        try:
            process.wait(timeout=timeout)
        except subprocess.TimeoutExpired:
            process.kill()
            callAfter(stderrCallback, f"The {name} process timed out (in process.wait())")
            callAfter(resultCallback, -1)
            return
        callAfter(resultCallback, process.returncode)

    thread = threading.Thread(target=worker)
    thread.start()


if __name__ == "__main__":
    # _testTimeout()
    appSupportPath = AppKit.NSSearchPathForDirectoriesInDomains(
        AppKit.NSApplicationSupportDirectory, AppKit.NSUserDomainMask, True
    )[0]
    version = f"{sys.version_info.major}.{sys.version_info.minor}"
    dbSitePath = os.path.join(appSupportPath, f"DrawBot/Python{version}")
    PipInstallerController(dbSitePath)
