# This Mod Organizer plugin is released to the pubic under the terms of the GNU GPL version 3, which is accessible from the Free Software Foundation here: https://www.gnu.org/licenses/gpl-3.0-standalone.html

# To use this plugin, place it in the plugins directory of your Mod Organizer install. You will then find a 'Run FNIS' option under the tools menu.

# Intended behaviour:
# * Adds button to tools menu.
# * If FNIS' location isn't known (or isn't valid, e.g. FNIS isn't actually there) when the button is pressed, a file chooser is displayed to find FNIS.
# * `GenerateFNISforUsers.exe RedirectFiles="<some mod path>" InstantExecute=1` is then run within the VFS, with the RedirectFiles option being controlled by other settings (which the user is prompted to fill in if they have not yet been specified).
# * When it exits, if necessary, its return code is used to generate a helpful popup saying whether or not it worked.

# Future behaviour:
# * As in Vortex's FNIS integration, keeps track of mod files which affect FNIS.
# * If they don't match what was there when the last FNIS output was created, uses IPluginDiagnose interface to display a warning
# * This may already be handled by MO's built-in (but disabled) FNIS checker plugin
import os
import pathlib
import sys

from PyQt5.QtCore import QCoreApplication, qCritical, QFileInfo
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QFileDialog, QFileSystemModel, QMessageBox

if "mobase" not in sys.modules:
    import mock_mobase as mobase

class FNISMissingException(Exception):
    """Thrown if GenerateFNISforUsers.exe path can't be found"""
    pass

class FNISInactiveException(Exception):
    """Thrown if GenerateFNISforUsers.exe is installed to an inactive mod"""
    pass

class UnknownOutputPreferenceException(Exception):
    """Thrown if the user hasn't specified whether to output to a separate mod"""
    pass

class FNISTool(mobase.IPluginTool):

    def __init__(self):
        super(FNISTool, self).__init__()
        self.__organizer = None
        self.__parentWidget = None

    def init(self, organizer):
        self.__organizer = organizer
        if sys.version_info < (3, 0):
            qCritical(self.__tr("FNISTool plugin requires a Python 3 interpreter, but is running on a Python 2 interpreter."))
            QMessageBox.critical(self.__parentWidget, self.__tr("Incompatible Python version."), self.__tr("This version of the FNIS Integration plugin requires a Python 3 interpreter, but Mod Organizer has provided a Python 2 interpreter. You should check for an updated version, including in the Mod Organizer 2 Development Discord Server."))
            return False
        return True

    def name(self):
        return "FNIS Integration Tool"

    def localizedName(self):
        return self.__tr("FNIS Integration Tool")

    def author(self):
        return "AnyOldName3"

    def description(self):
        return self.__tr("Runs GenerateFNISforUsers.exe so the game can load custom animations.")

    def version(self):
        return mobase.VersionInfo(1, 2, 0, 0)

    def requirements(self):
        return [
            mobase.PluginRequirementFactory.gameDependency({
                "Skyrim",
                "Skyrim Special Edition",
                "Skyrim VR"
            })
        ]

    def settings(self):
        return [
            mobase.PluginSetting("fnis-path", self.__tr("Path to GenerateFNISforUsers.exe"), ""),
            mobase.PluginSetting("output-to-mod", self.__tr("Whether or not to direct the FNIS output to a mod folder."), True),
            mobase.PluginSetting("output-path", self.__tr("When output-to-mod is enabled, the path to the mod to use."), ""),
            mobase.PluginSetting("initialised", self.__tr("Settings have been initialised.  Set to False to reinitialise them."), False),
            mobase.PluginSetting("output-logs-to-mod", self.__tr("Whether or not to direct any new FNIS logs to a mod folder."), True),
            mobase.PluginSetting("output-logs-path", self.__tr("When output-logs-to-mod is enabled, the path to the mod to use."), ""),
            ]

    def displayName(self):
        return self.__tr("FNIS/Run FNIS")

    def tooltip(self):
        return self.__tr("Runs GenerateFNISforUsers.exe so the game can load custom animations.")

    def icon(self):
        fnisPath = self.__organizer.pluginSetting(self.name(), "fnis-path")
        if os.path.exists(fnisPath):
            # We can't directly grab the icon from an executable, but this seems like the simplest alternative.
            fin = QFileInfo(fnisPath)
            model = QFileSystemModel()
            model.setRootPath(fin.path())
            return model.fileIcon(model.index(fin.filePath()))
        else:
            # Fall back to where the user might have put an icon manually.
            return QIcon("plugins/FNIS.ico")

    def setParentWidget(self, widget):
        self.__parentWidget = widget

    def display(self):
        args = []
        redirectOutput = True
        outputModName = None
        logOutputModName = ""

        if not bool(self.__organizer.pluginSetting(self.name(), "initialised")):
            self.__organizer.setPluginSetting(self.name(), "fnis-path", "")
            self.__organizer.setPluginSetting(self.name(), "output-path", "")
            self.__organizer.setPluginSetting(self.name(), "output-to-mod", True)
            self.__organizer.setPluginSetting(self.name(), "output-logs-path", "")
            self.__organizer.setPluginSetting(self.name(), "output-logs-to-mod", True)

        try:
            redirectOutput = self.__getRedirectOutput()
        except UnknownOutputPreferenceException:
            QMessageBox.critical(self.__parentWidget, self.__tr("Output preference not set"), self.__tr("Whether or not to output to a mod was not specified. The tool will now exit."))
            return
        if redirectOutput:
            try:
                outputPath = self.__getOutputPath()
                args.append('RedirectFiles="' + outputPath + '"')
                outputModName = pathlib.Path(outputPath).name
            except UnknownOutputPreferenceException:
                QMessageBox.critical(self.__parentWidget, self.__tr("Output mod not set"), self.__tr("The mod to output to was not specified. The tool will now exit."))
                return
        args.append('InstantExecute=1')

        if redirectOutput:
            try:
                redirectLogs = self.__getRedirectLogs()
            except UnknownOutputPreferenceException:
                QMessageBox.critical(self.__parentWidget, self.__tr("Output preference not set"), self.__tr("Whether or not to output to a mod was not specified. The tool will now exit."))
                return
            if redirectLogs:
                try:
                    outputPath = self.__getLogOutputPath()
                    logOutputModName = pathlib.Path(outputPath).name
                except UnknownOutputPreferenceException:
                    QMessageBox.critical(self.__parentWidget, self.__tr("Output mod not set"), self.__tr("The mod to output to was not specified. The tool will now exit."))
                    return

        try:
            executable = self.__getFNISPath()
        except FNISMissingException:
            QMessageBox.critical(self.__parentWidget, self.__tr("FNIS path not specified"), self.__tr("The path to GenerateFNISforUsers.exe wasn't specified. The tool will now exit."))
            return
        except FNISInactiveException:
            # Error has already been displayed, just quit
            return

        self.__organizer.setPluginSetting(self.name(), "initialised", True)

        if redirectOutput:
            # Disable the output mod as USVFS isn't designed to cope with its input directories being modified
            self.__organizer.modList().setActive(outputModName, False)

            if redirectLogs:
                # Enable the log output mod
                self.__organizer.modList().setActive(logOutputModName, True)


        handle = self.__organizer.startApplication(executable, args, forcedCustomOverwrite=logOutputModName, ignoreCustomOverwrite=not bool(logOutputModName))
        result, exitCode = self.__organizer.waitForApplication(handle)

        if redirectOutput:
            # Enable the output mod
            self.__organizer.modList().setActive(outputModName, True)
            # Ensure the 'No valid game data' message goes away
            self.__organizer.modDataChanged(self.__organizer.getMod(outputModName))

            if redirectLogs:
                self.__organizer.modDataChanged(self.__organizer.getMod(logOutputModName))

    def __tr(self, str):
        return QCoreApplication.translate("FNISTool", str)

    def __getRedirectOutput(self):
        redirectOutput = bool(self.__organizer.pluginSetting(self.name(), "output-to-mod"))
        initialised = bool(self.__organizer.pluginSetting(self.name(), "initialised"))
        if not initialised:
            result = QMessageBox.question(self.__parentWidget, self.__tr("Output to a mod?"), self.__tr("Fore's New Idles in Skyrim can output either to Mod Organizer's VFS (potentially overwriting files from multiple mods) or to a separate mod. Would you like FNIS to output to a separate mod? This setting can be updated in the Plugins tab of the Mod Organizer Settings menu."), QMessageBox.StandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel))
            if result == QMessageBox.Yes:
                redirectOutput = True
            elif result == QMessageBox.No:
                redirectOutput = False
            else:
                # the user pressed cancel
                raise UnknownOutputPreferenceException

            self.__organizer.setPluginSetting(self.name(), "output-to-mod", redirectOutput)
        return redirectOutput

    def __getRedirectLogs(self):
        redirectLogs = bool(self.__organizer.pluginSetting(self.name(), "output-logs-to-mod"))
        initialised = bool(self.__organizer.pluginSetting(self.name(), "initialised"))
        if not initialised:
            result = QMessageBox.question(self.__parentWidget, self.__tr("Output logs to a mod?"), self.__tr("Any new logs generated when running FNIS will end up in Mod Organizer's overwrite folder. Would you like these logs to be output to a separate mod? This setting can be updated in the Plugins tab of the Mod Organizer Settings menu."), QMessageBox.StandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel))
            if result == QMessageBox.Yes:
                redirectLogs = True
            elif result == QMessageBox.No:
                redirectLogs = False
            else:
                # the user pressed cancel
                raise UnknownOutputPreferenceException

            self.__organizer.setPluginSetting(self.name(), "output-logs-to-mod", redirectLogs)
        return redirectLogs

    def __getOutputPath(self):
        path = self.__organizer.pluginSetting(self.name(), "output-path")
        pathlibPath = pathlib.Path(path)
        modDirectory = self.__getModDirectory()
        isAMod = pathlibPath.parent.samefile(modDirectory)
        if not pathlibPath.is_dir() or not isAMod:
            QMessageBox.information(self.__parentWidget, self.__tr("Choose an output mod"), self.__tr("Please choose an output mod for Fore's New Idles in Skyrim. This must be a directory in Mod Organizer's mods directory, and you can create one if you do not have one already. This mod will not be available to the VFS when FNIS is run, so do not choose a mod you use for anything else. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu."))
            while not pathlibPath.is_dir() or not isAMod:
                path = QFileDialog.getExistingDirectory(self.__parentWidget, self.__tr("Choose an output mod"), str(modDirectory), QFileDialog.ShowDirsOnly)
                if not path:
                    # cancel was pressed
                    raise UnknownOutputPreferenceException
                pathlibPath = pathlib.Path(path)
                isAMod = pathlibPath.parent.samefile(modDirectory)
                if not isAMod:
                    QMessageBox.information(self.__parentWidget, self.__tr("Not a mod..."), self.__tr("The selected directory is not a Mod Organizer managed mod. Please choose a directory within the mods directory."))
                    continue
                empty = True
                for item in pathlibPath.iterdir():
                    if item.name != "meta.ini":
                        empty = False
                        break
                if not empty:
                    if QMessageBox.question(self.__parentWidget, self.__tr("Mod not empty"), self.__tr("The selected mod already contains files. Are you sure want to use it as the output mod?"), QMessageBox.StandardButtons(QMessageBox.Yes | QMessageBox.No)) == QMessageBox.Yes:
                        # Proceed normally - the user is happy
                        pass
                    else:
                        # Restart outer loop - the user wants to pick again
                        isAMod = False
            # The user may have created a new mod in the MO mods directory, so we must trigger a refresh
            self.__organizer.refreshModList()
            self.__organizer.setPluginSetting(self.name(), "output-path", path)
        return path

    def __getLogOutputPath(self):
        path = self.__organizer.pluginSetting(self.name(), "output-logs-path")
        pathlibPath = pathlib.Path(path)
        modDirectory = self.__getModDirectory()
        fnisOutputPath = pathlib.Path(self.__getOutputPath())
        isAMod = pathlibPath.parent.samefile(modDirectory)
        isSameAsFnisOutput = pathlibPath.samefile(fnisOutputPath)
        if not pathlibPath.is_dir() or not isAMod or isSameAsFnisOutput:
            QMessageBox.information(self.__parentWidget, self.__tr("Choose an output mod"), self.__tr("Please choose an output mod for logs for Fore's New Idles in Skyrim. This must be a directory in Mod Organizer's mods directory, must not be the same as the FNIS output mod, and you can create one if you do not have one already. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu."))
            while not pathlibPath.is_dir() or not isAMod or isSameAsFnisOutput:
                path = QFileDialog.getExistingDirectory(self.__parentWidget, self.__tr("Choose a log output mod"), str(modDirectory), QFileDialog.ShowDirsOnly)
                if not path:
                    # cancel was pressed
                    raise UnknownOutputPreferenceException
                pathlibPath = pathlib.Path(path)
                isAMod = pathlibPath.parent.samefile(modDirectory)
                if not isAMod:
                    QMessageBox.information(self.__parentWidget, self.__tr("Not a mod..."), self.__tr("The selected directory is not a Mod Organizer managed mod. Please choose a directory within the mods directory."))
                    continue
                isSameAsFnisOutput = pathlibPath.samefile(fnisOutputPath)
                if isSameAsFnisOutput:
                    QMessageBox.information(self.__parentWidget, self.__tr("Same as FNIS output"), self.__tr("The selected mod is the same as the FNIS output mod.  Please choose a different mod."))
                    continue
                empty = True
                for item in pathlibPath.iterdir():
                    if item.name != "meta.ini":
                        empty = False
                        break
                if not empty:
                    if QMessageBox.question(self.__parentWidget, self.__tr("Mod not empty"), self.__tr("The selected mod already contains files. Are you sure want to use it as the output mod?"), QMessageBox.StandardButtons(QMessageBox.Yes | QMessageBox.No)) == QMessageBox.Yes:
                        # Proceed normally - the user is happy
                        pass
                    else:
                        # Restart outer loop - the user wants to pick again
                        isAMod = False

            # The user may have created a new mod in the MO mods directory, so we must trigger a refresh
            self.__organizer.refreshModList()
            self.__organizer.setPluginSetting(self.name(), "output-logs-path", path)
        return path

    def __getFNISPath(self):
        savedPath = self.__organizer.pluginSetting(self.name(), "fnis-path")
        # FNIS must be installed within the game's data directory, so needs to either be within that or a mod folder
        modDirectory = self.__getModDirectory()
        gameDataDirectory = pathlib.Path(self.__organizer.managedGame().dataDirectory().absolutePath())
        pathlibPath = pathlib.Path(savedPath)
        inGoodLocation = self.__withinDirectory(pathlibPath, modDirectory)
        inGoodLocation |= self.__withinDirectory(pathlibPath, gameDataDirectory)
        if not pathlibPath.is_file() or not inGoodLocation:
            QMessageBox.information(self.__parentWidget, self.__tr("Find FNIS"), self.__tr("Fore's New Idles in Skyrim can't be found using the location saved in Mod Organizer's settings. Please find GenerateFNISforUsers.exe in the file picker. FNIS must be visible within the VFS, so choose an installation either within the game's data directory or within a mod folder. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu."))
            while True:
                path = QFileDialog.getOpenFileName(self.__parentWidget, self.__tr("Locate GenerateFNISforUsers.exe"), str(modDirectory), "FNIS (GenerateFNISforUsers.exe)")[0]
                if path == "":
                    # Cancel was pressed
                    raise FNISMissingException
                pathlibPath = pathlib.Path(path)
                inGoodLocation = self.__withinDirectory(pathlibPath, modDirectory)
                inGoodLocation |= self.__withinDirectory(pathlibPath, gameDataDirectory)
                if pathlibPath.is_file() and inGoodLocation:
                    self.__organizer.setPluginSetting(self.name(), "fnis-path", path)
                    savedPath = path
                    break
                else:
                    QMessageBox.information(self.__parentWidget, self.__tr("Not a compatible location..."), self.__tr("Fore's New Idles in Skyrim only works when within the VFS, so must be installed to the game's data directory or within a mod folder. Please select a different FNIS installation."))
        # Check the mod is actually enabled
        if self.__withinDirectory(pathlibPath, modDirectory):
            fnisModName = None
            for path in pathlibPath.parents:
                if path.parent.samefile(modDirectory):
                    fnisModName = path.name
                    break
            if (self.__organizer.modList().state(fnisModName) & mobase.ModState.active) == 0:
                # FNIS is installed to an inactive mod
                result = QMessageBox.question(self.__parentWidget, self.__tr("FNIS mod deactivated"), self.__tr("Fore's New Idles in Skyrim is installed to an inactive mod. Press OK to activate it or Cancel to quit the tool"), QMessageBox.StandardButtons(QMessageBox.Ok | QMessageBox.Cancel))
                if result == QMessageBox.Ok:
                    self.__organizer.modList().setActive(fnisModName, True)
                else:
                    raise FNISInactiveException
        return savedPath

    def __getModDirectory(self):
        return self.__organizer.modsPath()

    @staticmethod
    def __withinDirectory(innerPath, outerDir):
        for path in innerPath.parents:
            if path.samefile(outerDir):
                return True
        return False

def createPlugin():
    return FNISTool()
