"""Module with the classes and functions
that enter the profiles management.

This file is part of the physbiblio package.
"""
import traceback
import os
import glob
import shutil
from PySide2.QtCore import Qt
from PySide2.QtGui import QIcon
from PySide2.QtWidgets import (
    QButtonGroup,
    QCheckBox,
    QComboBox,
    QDesktopWidget,
    QGridLayout,
    QLineEdit,
    QPushButton,
    QRadioButton,
)

try:
    from physbiblio.config import pbConfig
    from physbiblio.errors import pBLogger
    from physbiblio.database import pBDB
    from physbiblio.gui.errorManager import pBGUIErrorManager, pBGUILogger
    from physbiblio.gui.basicDialogs import askYesNo
    from physbiblio.gui.commonClasses import (
        EditObjectWindow,
        PBComboBox,
        PBDialog,
        PBLabel,
    )
except ImportError:
    print("Could not find physbiblio and its modules!")
    print(traceback.format_exc())


def editProfile(parentObject):
    """Use `EditProfileWindow` and process the form output

    Parameters:
        parentObject: the parent object,
            which has the function `statusBarMessage`
            (a `MainWindow` instance)
    """
    oldOrder = pbConfig.profileOrder
    newProfWin = EditProfileWindow(parentObject)
    newProfWin.exec_()
    data = {}
    if newProfWin.result:
        newProfiles = {}
        deleted = []
        for currEl in newProfWin.elements:
            name = currEl["n"].text()
            try:
                fileName = currEl["f"].text()
            except AttributeError:
                fileName = currEl["f"].currentText()
            if currEl["r"].isChecked() and name != "":
                pbConfig.globalDb.setDefaultProfile(name)
            if name in pbConfig.profiles.keys():
                pbConfig.globalDb.updateProfileField(
                    name, "description", currEl["d"].text()
                )
                if currEl["x"].isChecked() and askYesNo(
                    "Do you really want to cancel the"
                    + " profile '%s'?\n" % name
                    + "The action cannot be undone!\n"
                    + "The corresponding database will not be erased."
                ):
                    pbConfig.globalDb.deleteProfile(name)
                    deleted.append(name)
            elif fileName in [
                pbConfig.profiles[k]["db"].split(os.sep)[-1]
                for k in pbConfig.profiles.keys()
            ]:
                pbConfig.globalDb.updateProfileField(
                    os.path.join(pbConfig.dataPath, fileName),
                    "name",
                    name,
                    identifierField="databasefile",
                )
                pbConfig.globalDb.updateProfileField(
                    name, "description", currEl["d"].text()
                )
                if currEl["x"].isChecked() and askYesNo(
                    "Do you really want to cancel the "
                    + "profile '%s'?\n" % name
                    + "The action cannot be undone!\n"
                    + "The corresponding database will not be erased."
                ):
                    pbConfig.globalDb.deleteProfile(name)
                    deleted.append(name)
            else:
                if name.strip() != "":
                    newFileName = os.path.join(
                        pbConfig.dataPath,
                        (fileName.split(os.sep)[-1] + ".db").replace(".db.db", ".db"),
                    )
                    pbConfig.globalDb.createProfile(
                        name=name,
                        description=currEl["d"].text(),
                        databasefile=newFileName,
                    )
                    if currEl["c"].currentText() != "None":
                        shutil.copy2(
                            os.path.join(pbConfig.dataPath, currEl["c"].currentText()),
                            newFileName,
                        )
                    pBGUIErrorManager.loggerPriority(0).info("New profile created.")
        pbConfig.globalDb.setProfileOrder(
            [
                e["n"].text()
                for e in newProfWin.elements
                if e["n"].text() != "" and e["n"].text() not in deleted
            ]
        )
        pbConfig.loadProfiles()
        message = "Profile management completed"
    else:
        pbConfig.profileOrder = oldOrder
        message = "No modifications"
    try:
        parentObject.statusBarMessage(message)
    except AttributeError:
        pBLogger.debug(
            "parentObject has no attribute 'statusBarMessage'", exc_info=True
        )


class SelectProfiles(PBDialog):
    """Widget to change the profile"""

    def __init__(self, parent, message=None):
        """Instantiate the class

        Parameters:
            parent: the parent window (a `MainWindow` instance)
            message: a message used as a description
        """
        if not hasattr(parent, "reloadConfig"):
            raise Exception("Cannot run SelectProfiles: invalid parent")
        PBDialog.__init__(self, parent)
        self.message = message
        self.result = False
        self.combo = None
        self.loadButton = None
        self.cancelButton = None
        self.initUI()

    def onCancel(self):
        """Set result to False and close the window"""
        self.result = False
        self.close()

    def onLoad(self):
        """Get current selection and (eventually) load new profile"""
        prof, desc = self.combo.currentText().split(" -- ")
        newProfile = pbConfig.profiles[prof]
        if prof != pbConfig.currentProfileName:
            pBLogger.info("Changing profile...")
            pbConfig.reInit(prof, newProfile)
            pBDB.reOpenDB(pbConfig.currentDatabase)
            self.parent().reloadConfig()
        self.parent().reloadMainContent()
        self.close()

    def initUI(self):
        """Create a `QGridLayout` with the `PBComboBox` and
        the two selection buttons
        """
        self.setWindowTitle("Select profile")

        grid = QGridLayout()
        grid.setSpacing(10)

        i = 0
        if self.message is not None:
            grid.addWidget(PBLabel("%s" % self.message), 0, 0)
            i += 1

        grid.addWidget(PBLabel("Available profiles: "), i, 0)
        self.combo = PBComboBox(
            self,
            [
                "%s -- %s" % (p, pbConfig.profiles[p]["d"])
                for p in pbConfig.profileOrder
            ],
            current="%s -- %s"
            % (
                pbConfig.currentProfileName,
                pbConfig.profiles[pbConfig.currentProfileName]["d"],
            ),
        )
        grid.addWidget(self.combo, i, 1)
        i += 1

        # Load and Cancel button
        self.loadButton = QPushButton("Load", self)
        self.loadButton.clicked.connect(self.onLoad)
        grid.addWidget(self.loadButton, i, 0)
        self.cancelButton = QPushButton("Cancel", self)
        self.cancelButton.clicked.connect(self.onCancel)
        self.cancelButton.setAutoDefault(True)
        grid.addWidget(self.cancelButton, i, 1)

        self.setLayout(grid)


class PBOrderPushButton(QPushButton):
    """Define a button to switch two form lines"""

    def __init__(self, parent, data, qicon, text, testing=False):
        """Extend `QPushButton.__init__`

        Parameters:
            parent: the parent object (an `EditProfileWindow` instance)
            data: the index of the row that will be switched
            qicon: the `QIcon` that will be used to build the `QPushButton`
            text: the `QPushButton` text
            testing (boolean, default False):
                if True, do not connect the clicked signal
        """
        self.qicon = qicon
        QPushButton.__init__(self, self.qicon, text)
        self.data = data
        self.parentObj = parent
        if not testing:
            self.clicked.connect(self.onClick)

    def onClick(self):
        """Perform the switchLines action"""
        self.parentObj.switchLines(self.data)

    def parent(self):
        """Return the parent"""
        return self.parentObj


class EditProfileWindow(EditObjectWindow):
    """create a window for editing or creating a profile"""

    def __init__(self, parent=None):
        """Prepare instance and create form"""
        EditObjectWindow.__init__(self, parent)
        self.def_group = None
        self.elements = []
        self.arrows = []
        self.acceptButton = None
        self.cancelButton = None
        self.createForm()

    def onOk(self):
        """In case "Ok" is pressed, decide if the result is valid:
            * if the name or filename of the new profile are empty, reject;
            * if the name or filename of the new profile are already in use,
                reject;
            * in all the other cases, accept and close the dialog.
        """
        if (
            self.elements[-1]["f"].currentText().strip() != ""
            and self.elements[-1]["n"].text().strip() == ""
        ) or (
            self.elements[-1]["f"].currentText().strip() == ""
            and self.elements[-1]["n"].text().strip() != ""
        ):
            pBGUILogger.info(
                "Cannot create a new profile if 'name' or 'filename' is empty."
            )
            return
        if self.elements[-1]["n"].text().strip() in [
            a["n"].text() for a in self.elements[:-1]
        ]:
            pBGUILogger.info("Cannot create new profile: 'name' already in use.")
            return
        if (
            self.elements[-1]["f"].currentText().strip().split(os.sep)[-1] + ".db"
        ).replace(".db.db", ".db") in [
            a["f"].text().split(os.sep)[-1] for a in self.elements[:-1]
        ]:
            pBGUILogger.info("Cannot create new profile: 'filename' already in use.")
            return
        self.result = True
        self.close()

    def addButtons(self, profilesData=None, profileOrder=None, defaultProfile=None):
        """Read profiles configuration and add `QLineEdits` and buttons
        for the existing profiles, using previous form content if requested

        Parameters:
            profilesData (optional): a dictionary of dictionaries,
                containing the information of each profile.
                Each element has the profile name as key, and this structure:
                    "n": profile name
                    "d": description
                    "f": name of the database file
                    "r": True if the profile was marked as default in the form
                    "x": True if the profile was selected for deletion
                If `None`, use `pbConfig.profiles`
            profileOrder (optional): the list containing the order
                of the profiles, by their name in the database.
                If `None`, use `pbConfig.profileOrder`
            defaultProfile (optional): the name of the default profile.
                If `None`, use `pbConfig.defaultProfileName`
        """
        if profilesData is None:
            profilesData = pbConfig.profiles
        if profileOrder is None:
            profileOrder = pbConfig.profileOrder
        if defaultProfile is None:
            defaultProfile = pbConfig.defaultProfileName
        self.def_group = QButtonGroup(self.currGrid)
        self.elements = []
        self.arrows = []
        i = 0
        j = 0
        missing = []
        for k in profileOrder:
            try:
                prof = profilesData[k]
            except KeyError:
                pBLogger.warning(
                    "Missing profile: '%s' in %s" % (k, sorted(list(profilesData)))
                )
                missing.append(k)
                continue
            for f in ["db", "d"]:
                if f not in list(prof):
                    pBLogger.warning(
                        "Missing info: '%s' in %s. " % (f, sorted(list(prof)))
                        + "Default to empty."
                    )
                    prof[f] = ""
            i += 1
            tempEl = {}
            tempEl["r"] = QRadioButton("")
            self.def_group.addButton(tempEl["r"])
            tempEl["r"].setChecked(False)
            self.currGrid.addWidget(tempEl["r"], i, 0)

            tempEl["n"] = QLineEdit(k)
            tempEl["f"] = QLineEdit(prof["db"].split(os.sep)[-1])
            tempEl["f"].setReadOnly(True)
            tempEl["d"] = QLineEdit(prof["d"])
            self.currGrid.addWidget(tempEl["n"], i, 1)
            self.currGrid.addWidget(tempEl["f"], i, 2)
            self.currGrid.addWidget(tempEl["d"], i, 3)
            if i > 1:
                self.arrows.append(
                    [
                        PBOrderPushButton(
                            self, j, QIcon(":/images/arrow-down.png"), ""
                        ),
                        PBOrderPushButton(self, j, QIcon(":/images/arrow-up.png"), ""),
                    ]
                )
                self.currGrid.addWidget(self.arrows[j][0], i - 1, 5)
                self.currGrid.addWidget(self.arrows[j][1], i, 4)
                j += 1
            tempEl["x"] = QCheckBox("", self)
            if "x" in prof.keys() and prof["x"]:
                tempEl["x"].setChecked(True)
            self.currGrid.addWidget(tempEl["x"], i, 6)
            self.elements.append(tempEl)

        # setChecked the radio of the default profile or
        # of the previously selected one
        profileOrder = [k for k in profileOrder if k not in missing]
        for i, k in enumerate(profileOrder):
            if defaultProfile == k:
                self.elements[i]["r"].setChecked(True)
        for i, k in enumerate(profileOrder):
            if "r" in profilesData[k].keys() and profilesData[k]["r"] is True:
                self.elements[i]["r"].setChecked(True)

    def createForm(
        self,
        profilesData=None,
        profileOrder=None,
        defaultProfile=None,
        newLine={"r": False, "n": "", "db": "", "d": "", "c": "None"},
    ):
        """Create the form for managing profiles,
        using previous form content if requested.

        Parameters:
            profilesData (optional): a dictionary of dictionaries,
                containing the information of each profile.
                Each element has the profile name as key, and this structure:
                    "n": profile name
                    "d": description
                    "db": name of the database file
                    "r": True if the profile was marked as default in the form
                    "x": True if the profile was selected for deletion
                If `None`, use `pbConfig.profiles`
            profileOrder (optional): the list containing the order
                of the profiles, by their name in the database.
                If `None`, use `pbConfig.profileOrder`
            defaultProfile (optional): the name of the default profile.
                If `None`, use `pbConfig.defaultProfileName`
            newLine (optional): the content of the line corresponding
                to the potentially new profile.
                It is a dictionary with the following fields:
                    "n": profile name
                    "d": description
                    "db": name of the database file
                    "r": True if the profile was marked as default in the form
                    "c": previous content of "Copy from:"
        """
        if profilesData is None:
            profilesData = pbConfig.profiles
        if profileOrder is None:
            profileOrder = pbConfig.profileOrder
        if defaultProfile is None:
            defaultProfile = pbConfig.defaultProfileName

        self.setWindowTitle("Edit profile")

        labels = [
            PBLabel("Default"),
            PBLabel("Short name"),
            PBLabel("Filename"),
            PBLabel("Description"),
        ]
        for i, e in enumerate(labels):
            self.currGrid.addWidget(e, 0, i)
        self.currGrid.addWidget(PBLabel("Order"), 0, 4, 1, 2)
        self.currGrid.addWidget(PBLabel("Delete?"), 0, 6)

        self.addButtons(profilesData, profileOrder)

        for f in ["c", "db", "d", "n", "r"]:
            if f not in newLine.keys():
                pBLogger.warning(
                    "Missing field: '%s' in %s. " % (f, sorted(list(newLine)))
                    + "Default to empty."
                )
                newLine[f] = ""
        i = len(profilesData) + 3
        self.currGrid.addWidget(PBLabel(""), i - 2, 0)
        tempEl = {}
        self.currGrid.addWidget(PBLabel("Add new?"), i - 1, 0)
        tempEl["r"] = QRadioButton("")
        self.def_group.addButton(tempEl["r"])
        if newLine["r"]:
            tempEl["r"].setChecked(True)
        else:
            tempEl["r"].setChecked(False)
        self.currGrid.addWidget(tempEl["r"], i, 0)

        tempEl["n"] = QLineEdit(newLine["n"])
        self.currGrid.addWidget(tempEl["n"], i, 1)

        tempEl["f"] = QComboBox(self)
        tempEl["f"].setEditable(True)
        dbFiles = [
            f.split(os.sep)[-1]
            for f in list(glob.iglob(os.path.join(pbConfig.dataPath, "*.db")))
        ]
        registeredDb = sorted(
            [a["db"].split(os.sep)[-1] for a in profilesData.values()]
        )
        tempEl["f"].addItems(
            [newLine["db"]] + [f for f in dbFiles if f not in registeredDb]
        )
        self.currGrid.addWidget(tempEl["f"], i, 2)

        tempEl["d"] = QLineEdit(newLine["d"])
        self.currGrid.addWidget(tempEl["d"], i, 3)

        self.currGrid.addWidget(PBLabel("Copy from:"), i, 4, 1, 2)
        tempEl["c"] = QComboBox(self)
        copyElements = ["None"] + registeredDb
        tempEl["c"].addItems(copyElements)
        tempEl["c"].setCurrentIndex(
            copyElements.index(newLine["c"]) if newLine["c"] in copyElements else 0
        )
        self.currGrid.addWidget(tempEl["c"], i, 6)

        self.elements.append(tempEl)

        i += 1
        self.currGrid.addWidget(PBLabel(""), i, 0)
        # OK button
        self.acceptButton = QPushButton("OK", self)
        self.acceptButton.clicked.connect(self.onOk)
        self.currGrid.addWidget(self.acceptButton, i + 1, 1)

        # cancel button
        self.cancelButton = QPushButton("Cancel", self)
        self.cancelButton.clicked.connect(self.onCancel)
        self.cancelButton.setAutoDefault(True)
        self.currGrid.addWidget(self.cancelButton, i + 1, 2)

    def switchLines(self, ix):
        """Save the current form content,
        switch the order of the rows as required,
        create a new form with the new order.

        Parameter:
            ix: the index of the first of the two rows involved in the switch
                (e.g., to switch the first and second rows, use ix = 0)
        """
        currentValues = {}
        currentOrder = []
        for el in self.elements[: len(pbConfig.profiles)]:
            tmp = {}
            tmp["db"] = el["f"].text()
            tmp["d"] = el["d"].text()
            tmp["r"] = el["r"].isChecked()
            tmp["x"] = el["x"].isChecked()
            currentValues[el["n"].text()] = tmp
            currentOrder.append(el["n"].text())
        newLine = {
            "r": self.elements[-1]["r"].isChecked(),
            "n": self.elements[-1]["n"].text(),
            "db": self.elements[-1]["f"].currentText(),
            "d": self.elements[-1]["d"].text(),
        }
        tempOrder = list(currentOrder)
        try:
            tempOrder[ix] = currentOrder[ix + 1]
            tempOrder[ix + 1] = currentOrder[ix]
        except IndexError:
            pBLogger.warning("Impossible to switch lines: index out of range")
            return False
        self.cleanLayout()
        self.createForm(currentValues, tempOrder, newLine)
        return True
