# 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 'ENTER BUTTON NAME HERE BEFORE RELEASE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' 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, nag the user to run the main FNIS Tool at least once, and exit.
# * Reads FNIS' patches file for the current game and the MyPatches.txt file to determine which are active.
# * Displays a popup where the user can enable or disable patches, and saves the results to MyPatches.txt when Save is pressed.

# Future behaviour:
# * Maybe allow the user to show hidden patches
# * Maybe move the saved list to something stored per-profile

import os
import pathlib
import sys

from FNISTool import FNISTool
from PyQt6.QtCore import QCoreApplication, qCritical, QFileInfo, Qt
from PyQt6.QtGui import QIcon, QFileSystemModel
from PyQt6.QtWidgets import QDialogButtonBox, QLabel, QListWidget, QListWidgetItem, QMessageBox, QVBoxLayout, QDialog

import mobase

patchListNames = {
    "Skyrim": "PatchList.txt",
    "Skyrim Special Edition": "PatchListSE.txt",
    "Skyrim VR": "PatchListVR.txt"
}

class Patch:

    def __init__(self, fullString):
        fields = fullString.split('#')
        # field names taken from FNIS PatchListSE.txt
        self.patchid = fields[0]
        self.hidden = fields[1] == "1"
        self.num_bones = fields[2]
        self.required_behaviors_pattern = fields[3]
        self.text_for_patch = fields[4]
        if len(fields) > 5:
            self.optional_file_path_for_mod_install_check = fields[5]
        else:
            self.optional_file_path_for_mod_install_check = None

    def asQListWidgetItem(self):
        listItem = QListWidgetItem(self.text_for_patch, None, 0)
        listItem.setFlags(listItem.flags() | Qt.ItemFlag.ItemIsUserCheckable)
        # We can't set the hidden status until the item is actually in a list
        listItem.setData(Qt.ItemDataRole.UserRole, self.patchid)

        return listItem

class ExpandingQListWidget(QListWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

    def sizeHint(self):
        return self.childrenRect().size()

class FNISPatches(mobase.IPluginTool):

    def __init__(self):
        super(FNISPatches, self).__init__()
        self.__organizer = None
        self.__parentWidget = None

    def init(self, organizer):
        self.__organizer = organizer
        if sys.version_info < (3, 0):
            qCritical(self.tr("FNISPatches 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 Patches 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 Patches Tool"

    def localizedName(self):
        return self.tr("FNIS Patches Tool")

    def author(self):
        return "AnyOldName3"

    def description(self):
        return self.tr("Configures the patches which FNIS applies to the game.")

    def version(self):
        return mobase.VersionInfo(1, 0, 1, mobase.ReleaseType.final)

    def master(self):
        return "FNIS Integration Tool"

    def settings(self):
        return []

    def displayName(self):
        return self.tr("FNIS/Configure FNIS Patches")

    def tooltip(self):
        return self.tr("Configures the patches which FNIS applies to the game.")

    def icon(self):
        fnisPath = self.__organizer.pluginSetting(self.__mainToolName(), "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):
        fnisPath = self.__getFNISPath()
        if fnisPath == None:
            return

        enabledPatches = self.__loadEnabledPatches()

        availablePatches = self.__loadAvailablePatches()

        dialog = QDialog(self.__parentWidget)
        dialog.setWindowTitle(self.tr("Select Patches"))

        label = QLabel(self.tr("Note: Some patches may be automatically enabled or disabled by Fore's New Idles in Skyrim, so don't be surprised if its list differs from this one."))
        label.setWordWrap(True)

        listWidget = ExpandingQListWidget()
        for patch in availablePatches.values():
            listItem = patch.asQListWidgetItem()
            if patch.patchid in enabledPatches:
                listItem.setCheckState(Qt.CheckState.Checked)
            else:
                listItem.setCheckState(Qt.CheckState.Unchecked)
            listWidget.addItem(listItem)
            listItem.setHidden(patch.hidden)

        buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
        buttonBox.accepted.connect(dialog.accept)
        buttonBox.rejected.connect(dialog.reject)

        layout = QVBoxLayout()
        layout.addWidget(label)
        layout.addWidget(listWidget)
        layout.addWidget(buttonBox)
        dialog.setLayout(layout)

        result = dialog.exec()
        if result == QDialog.DialogCode.Rejected:
            # Cancel was pressed
            return

        enabledPatches = set()
        for i in range(0, listWidget.count()):
            listItem = listWidget.item(i)
            if listItem.checkState() == Qt.CheckState.Checked:
                enabledPatches.add(listItem.data(Qt.ItemDataRole.UserRole))
        self.__saveEnabledPatches(enabledPatches)

    def tr(self, str):
        return QCoreApplication.translate("FNISPatches", str)

    @staticmethod
    def __mainToolName():
        return FNISTool().name()

    def __getFNISPath(self):
        savedPath = self.__organizer.pluginSetting(self.__mainToolName(), "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("Unable to find FNIS"), self.tr("Fore's New Idles in Skyrim can't be found using the location saved in Mod Organizer's settings. Please run the main FNIS integration tool before using this one."))
            return None
        return savedPath

    def __getModDirectory(self):
        modDirectory = None
        modList = self.__organizer.modList().allModsByProfilePriority()
        # Get the first managed mod so we can access the mods directory.
        for mod in modList:
            if (self.__organizer.modList().state(mod) & 0x2) != 0:
                modDirectory = pathlib.Path(self.__organizer.modList().getMod(mod).absolutePath()).parent
                break
        return modDirectory

    @staticmethod
    def __withinDirectory(innerPath, outerDir):
        for path in innerPath.parents:
            if path.samefile(outerDir):
                return True
        return False

    def __loadEnabledPatches(self, ):
        path = pathlib.Path(self.__getFNISPath()).parent
        path /= "MyPatches.txt"
        if not path.is_file():
            return set()

        enabledPatches = set()
        with path.open() as f:
            for line in f:
                if line != "":
                    enabledPatches.add(line.strip())
        return enabledPatches

    def __saveEnabledPatches(self, enabledPatches):
        path = pathlib.Path(self.__getFNISPath()).parent
        path /= "MyPatches.txt"
        with path.open('w') as f:
            for patchName in enabledPatches:
                f.write(patchName)
                f.write("\n")

    def __loadAvailablePatches(self):
        patchListName = patchListNames[self.__organizer.managedGame().gameName()]
        path = pathlib.Path(self.__getFNISPath()).parent
        path /= patchListName
        availablePatches = {}
        # FNIS uses UTF-8 with a BOM, Python assumes everything's using the platform's default encoding
        with path.open(encoding="utf-8-sig") as f:
            # The first line is a header thing
            firstLine = True
            for line in f:
                if firstLine:
                    firstLine = False
                    continue
                if line[0] == "'":
                    continue
                patch = Patch(line.strip())
                availablePatches[patch.patchid] = patch
        return availablePatches

def createPlugin():
    return FNISPatches()
