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

##########################################################################
# OpenLP - Open Source Lyrics Projection                                 #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2024 OpenLP Developers                              #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify   #
# it under the terms of the GNU General Public License as published by   #
# the Free Software Foundation, either version 3 of the License, or      #
# (at your option) any later version.                                    #
#                                                                        #
# This program is distributed in the hope that it will be useful,        #
# but WITHOUT ANY WARRANTY; without even the implied warranty of         #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          #
# GNU General Public License for more details.                           #
#                                                                        #
# You should have received a copy of the GNU General Public License      #
# along with this program.  If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
Provides additional classes for working in the library
"""
import os
from pathlib import Path
from typing import Any, List, Optional, Union

from PyQt5 import QtCore, QtWidgets

from openlp.core.common import sha256_file_hash
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
from openlp.core.lib import check_item_selected
from openlp.core.lib.mediamanageritem import MediaManagerItem
from openlp.core.lib.plugin import StringContent
from openlp.core.lib.ui import create_widget_action, critical_error_message_box
from openlp.core.ui.folders import AddFolderForm, ChooseFolderForm
from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.views import TreeWidgetWithDnD


class FolderLibraryItem(MediaManagerItem):
    """
    This is a custom MediaManagerItem subclass with support for folders
    """
    def __init__(self, parent, plugin, folder_class, item_class):
        super(FolderLibraryItem, self).__init__(parent, plugin)
        self.manager = self.plugin.manager
        self.choose_folder_form = ChooseFolderForm(self, self.manager, folder_class)
        self.add_folder_form = AddFolderForm(self, self.manager, folder_class)
        self.folder_class = folder_class
        self.item_class = item_class

    @property
    def current_folder(self):
        """
        Returns the currently active folder, or None
        """
        selected_items = self.list_view.selectedItems()
        selected_folder = None
        if selected_items:
            selected_item = selected_items[0]
            if isinstance(selected_item.data(0, QtCore.Qt.UserRole), self.item_class):
                selected_item = selected_item.parent()
            if isinstance(selected_item, QtWidgets.QTreeWidgetItem) and \
                    isinstance(selected_item.data(0, QtCore.Qt.UserRole), self.folder_class):
                selected_folder = selected_item.data(0, QtCore.Qt.UserRole)
        return selected_folder

    def retranslate_ui(self):
        """
        This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem``
        to another language.
        """
        self.add_folder_action.setText(UiStrings().AddFolder)
        self.add_folder_action.setToolTip(UiStrings().AddFolderDot)

    def create_item_from_id(self, item_id: Any):
        """
        Create a media item from an item id.

        :param item_id: Id to make live
        """
        Item = self.item_class
        if isinstance(item_id, (str, Path)):
            # Probably a file name
            item_data = self.manager.get_object_filtered(Item, Item.file_path == str(item_id))
        else:
            item_data = item_id
        item = QtWidgets.QTreeWidgetItem()
        item.setData(0, QtCore.Qt.UserRole, item_data)
        return item

    def on_add_folder_click(self):
        """
        Called to add a new folder
        """
        # Syntactic sugar, plus a minor performance optimisation
        Item = self.item_class

        if self.add_folder_form.exec(show_top_level_folder=True, selected_folder=self.current_folder):
            new_folder = self.add_folder_form.new_folder
            if self.manager.save_object(new_folder):
                self.load_list(self.manager.get_all_objects(Item, order_by_ref=Item.file_path))
                self.expand_folder(new_folder.id)
            else:
                critical_error_message_box(
                    message=translate('OpenLP.FolderLibraryItem', 'Could not add the new folder.'))

    def on_delete_click(self):
        """
        Remove an item from the list.
        """
        # Syntactic sugar, plus a minor performance optimisation
        Folder, Item = self.folder_class, self.item_class

        # Turn off auto preview triggers.
        self.list_view.blockSignals(True)
        if check_item_selected(self.list_view,
                               translate('OpenLP.FolderLibraryItem', 'You must select an item or folder to delete.')):
            tree_item_list = self.list_view.selectedItems()
            self.application.set_busy_cursor()
            self.main_window.display_progress_bar(len(tree_item_list))
            for tree_item in tree_item_list:
                if not tree_item:
                    self.main_window.increment_progress_bar()
                    continue
                item = tree_item.data(0, QtCore.Qt.UserRole)
                if isinstance(item, Item):
                    self.delete_item(item)
                    if not item.folder_id or not tree_item.parent():
                        self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(tree_item))
                    else:
                        tree_item.parent().removeChild(tree_item)
                    self.manager.delete_object(Item, item.id)
                elif isinstance(item, Folder):
                    if QtWidgets.QMessageBox.question(
                            self.list_view.parent(),
                            translate('OpenLP.FolderLibraryItem', 'Remove folder'),
                            translate('OpenLP.FolderLibraryItem',
                                      'Are you sure you want to remove "{name}" and everything in it?'
                                      ).format(name=item.name)
                    ) == QtWidgets.QMessageBox.Yes:
                        self.recursively_delete_folder(item)
                        self.manager.delete_object(Folder, item.id)
                        if item.parent_id is None:
                            self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(tree_item))
                        else:
                            tree_item.parent().removeChild(tree_item)
                self.main_window.increment_progress_bar()
            self.main_window.finished_progress_bar()
            self.application.set_normal_cursor()
        self.list_view.blockSignals(False)

    def add_list_view_to_toolbar(self):
        """
        Creates the main widget for listing items.
        """
        # Add the List widget
        self.list_view = TreeWidgetWithDnD(self, self.plugin.name)
        self.list_view.setObjectName('{name}TreeView'.format(name=self.plugin.name))
        # Add to pageLayout
        self.page_layout.addWidget(self.list_view)
        # define and add the context menu
        self.list_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        if self.has_edit_icon:
            create_widget_action(
                self.list_view,
                text=self.plugin.get_string(StringContent.Edit)['title'],
                icon=UiIcons().edit,
                triggers=self.on_edit_click)
            create_widget_action(self.list_view, separator=True)
        if self.can_preview:
            create_widget_action(
                self.list_view,
                'listView{name}{preview}Item'.format(name=self.plugin.name.title(),
                                                     preview=StringContent.Preview.title()),
                text=self.plugin.get_string(StringContent.Preview)['title'],
                icon=UiIcons().preview,
                can_shortcuts=True,
                triggers=self.on_preview_click)
        if self.can_make_live:
            create_widget_action(
                self.list_view,
                'listView{name}{live}Item'.format(name=self.plugin.name.title(), live=StringContent.Live.title()),
                text=self.plugin.get_string(StringContent.Live)['title'],
                icon=UiIcons().live,
                can_shortcuts=True,
                triggers=self.on_live_click)
        if self.can_add_to_service:
            create_widget_action(
                self.list_view,
                'listView{name}{service}Item'.format(name=self.plugin.name.title(),
                                                     service=StringContent.Service.title()),
                can_shortcuts=True,
                text=self.plugin.get_string(StringContent.Service)['title'],
                icon=UiIcons().add,
                triggers=self.on_add_click)
        if self.add_to_service_item:
            create_widget_action(self.list_view, separator=True)
            create_widget_action(
                self.list_view,
                text=translate('OpenLP.MediaManagerItem', '&Add to selected Service Item'),
                icon=UiIcons().add,
                triggers=self.on_add_edit_click)
            create_widget_action(self.list_view, separator=True)
        if self.has_delete_icon:
            create_widget_action(
                self.list_view,
                'listView{name}{delete}Item'.format(name=self.plugin.name.title(), delete=StringContent.Delete.title()),
                text=self.plugin.get_string(StringContent.Delete)['title'],
                icon=UiIcons().delete,
                can_shortcuts=True, triggers=self.on_delete_click)
        self.add_custom_context_actions()
        # Create the context menu and add all actions from the list_view.
        self.menu = QtWidgets.QMenu()
        self.menu.addActions(self.list_view.actions())
        self.list_view.doubleClicked.connect(self.on_double_clicked)
        self.list_view.itemSelectionChanged.connect(self.on_selection_change)
        self.list_view.customContextMenuRequested.connect(self.context_menu)

    def add_custom_context_actions(self):
        """
        Override this method to add custom context actions
        """
        pass

    def add_middle_header_bar(self):
        """
        Add buttons after the main buttons
        """
        self.add_folder_action = self.toolbar.add_toolbar_action(
            'add_folder_action', icon=UiIcons().folder, triggers=self.on_add_folder_click)

    def add_sub_folders(self, folder_list: List[QtWidgets.QTreeWidgetItem], parent_id: Optional[int] = None):
        """
        Recursively add subfolders to the given parent folder in a QTreeWidget.

        :param folder_list: The List object that contains all QTreeWidgetItems.
        :param parent_folder_id: The ID of the folder that will be added recursively.
        """
        # Syntactic sugar, plus a minor performance optimisation
        Folder = self.folder_class

        folders = self.manager.get_all_objects(Folder, Folder.parent_id == parent_id)
        folders.sort(key=lambda folder_object: get_natural_key(folder_object.name))
        folder_icon = UiIcons().folder
        for folder in folders:
            folder_item = QtWidgets.QTreeWidgetItem()
            folder_item.setText(0, folder.name)
            folder_item.setData(0, QtCore.Qt.UserRole, folder)
            folder_item.setIcon(0, folder_icon)
            if parent_id is None:
                self.list_view.addTopLevelItem(folder_item)
            else:
                folder_list[parent_id].addChild(folder_item)
            folder_list[folder.id] = folder_item
            self.add_sub_folders(folder_list, folder.id)

    def expand_folder(self, folder_id: int, root_item: Optional[QtWidgets.QTreeWidgetItem] = None):
        """
        Expand folders in the widget recursively.

        :param folder_id: The ID of the folder that will be expanded.
        :param root_item: This option is only used for recursion purposes.
        """
        return_value = False
        if root_item is None:
            root_item = self.list_view.invisibleRootItem()
        for i in range(root_item.childCount()):
            child = root_item.child(i)
            if self.expand_folder(folder_id, child):
                child.setExpanded(True)
                return_value = True
        if isinstance(root_item.data(0, QtCore.Qt.UserRole), self.folder_class):
            if root_item.data(0, QtCore.Qt.UserRole).id == folder_id:
                return True
        return return_value

    def recursively_delete_folder(self, folder):
        """
        Recursively deletes a folder and all folders and items in it.

        :param folder: The Folder instance of the folder that will be deleted.
        """
        # Syntactic sugar, plus a minor performance optimisation
        Folder, Item = self.folder_class, self.item_class

        items = self.manager.get_all_objects(Item, Item.folder_id == folder.id)
        for item in items:
            self.delete_item(item)
            self.manager.delete_object(Item, item.id)
        folders = self.manager.get_all_objects(Folder, Folder.parent_id == folder.id)
        for child in folders:
            self.recursively_delete_folder(child)
            self.manager.delete_object(Folder, child.id)

    def file_to_item(self, filename: Union[Path, str]):
        """
        This method allows the media item to convert a string filename into an item class

        Override this method to customise your plugin's loading method
        """
        if isinstance(filename, Path):
            name = filename.name
            filename = str(filename)
        else:
            name = os.path.basename(filename)
        item = self.item_class(name=name, file_path=filename)
        self.manager.save_object(item)
        return item

    def load_item(self, item, is_initial_load=False):
        """
        This method allows the media item to set up the QTreeWidgetItem the way it wants
        """
        raise NotImplementedError('load_item needs to be implemented by the descendant class')

    def delete_item(self, item):
        """
        This method allows the media item to delete the Item
        """
        raise NotImplementedError('delete_item needs to be implemented by the descendant class')

    def load_list(self, items, is_initial_load=False, target_folder=None):
        """
        Load the list of items into the tree view

        :param items: The items to load
        :param target_folder: The folder to load the items into
        """
        if not is_initial_load:
            self.application.set_busy_cursor()
            self.main_window.display_progress_bar(len(items))
        self.list_view.clear()
        # Load the list of folders and add them to the tree view
        folder_items = {}
        self.add_sub_folders(folder_items, parent_id=None)
        if target_folder is not None:
            self.expand_folder(target_folder.id)
        # Convert any filenames to items
        for counter, filename in enumerate(items):
            if isinstance(filename, (Path, str)):
                items[counter] = self.file_to_item(filename)
        # Sort the files by the filename
        items.sort(key=lambda item: get_natural_key(item if isinstance(item, str) else item.file_path))
        for item in items:
            self.log_debug('Loading item: {name}'.format(name=item.file_path))
            tree_item = self.load_item(item, is_initial_load)
            if not tree_item:
                continue
            elif not item.folder_id or item.folder_id not in folder_items:
                self.list_view.addTopLevelItem(tree_item)
            else:
                folder_items[item.folder_id].addChild(tree_item)
            if not is_initial_load:
                self.main_window.increment_progress_bar()
        if not is_initial_load:
            self.main_window.finished_progress_bar()
        self.application.set_normal_cursor()

    def format_search_result(self, item):
        """
        Format an item for the search results. The default implementation simply returns
        [item.file_path, item.file_path]

        :param Item item: An Item to be formatted
        :return list[str, str]: A list of two items containing the full path and a pretty name
        """
        return [item.file_path, item.file_path]

    @QtCore.pyqtSlot(str, bool, result=list)
    def search(self, string: str, show_error: bool = True) -> list[list[Any]]:
        """
        Performs a search for items containing ``string``

        :param string: String to be displayed
        :param show_error: Should the error be shown (True)
        :return: The search result.
        """
        string = string.lower()
        items = self.manager.get_all_objects(self.item_class, self.item_class.file_path.ilike('%' + string + '%'))
        return [self.format_search_result(item) for item in items]

    def validate_and_load(self, file_paths: List[Path], target_folder: Optional[QtWidgets.QTreeWidgetItem] = None):
        """
        Process a list for files either from the File Dialog or from Drag and Drop.
        This method is overloaded from MediaManagerItem.

        :param list[Path] file_paths: A List of paths  to be loaded
        :param target_folder: The QTreeWidgetItem of the folder that will be the parent of the added files
        """
        self.application.set_normal_cursor()
        if target_folder:
            target_folder = target_folder.data(0, QtCore.Qt.UserRole)
        elif self.current_folder:
            target_folder = self.current_folder
        if not target_folder and self.choose_folder_form.exec() == QtWidgets.QDialog.Accepted:
            target_folder = self.choose_folder_form.folder
            if self.choose_folder_form.is_new_folder:
                self.manager.save_object(target_folder)
        existing_files = [item.file_path for item in self.manager.get_all_objects(self.item_class)]
        # Convert file paths to items
        for file_path in file_paths:
            if file_paths.count(file_path) > 1 or existing_files.count(file_path) > 0:
                # If a file already exists in the items or has been selected twice, show an error message
                critical_error_message_box(translate('OpenLP.FolderLibraryItem', 'File Exists'),
                                           translate('OpenLP.FolderLibraryItem',
                                                     'An item with that filename already exists.'))
                continue

            self.log_debug('Adding new item: {name}'.format(name=file_path))
            item = self.item_class(name=str(file_path), file_path=str(file_path))
            if isinstance(file_path, Path) and file_path.exists():
                item.file_hash = sha256_file_hash(file_path)
            if target_folder:
                item.folder_id = target_folder.id
            self.manager.save_object(item)
            self.main_window.increment_progress_bar()
        self.load_list(self.manager.get_all_objects(self.item_class, order_by_ref=self.item_class.file_path),
                       target_folder=target_folder)
        last_dir = Path(file_paths[0]).parent
        self.settings.setValue(self.settings_section + '/last directory', last_dir)

    def dnd_move_internal(self, target):
        """
        Handle drag-and-drop moving of items within the media manager

        :param target: This contains the QTreeWidget that is the target of the DnD action
        """
        items_to_move = self.list_view.selectedItems()
        # Determine group to move images to
        target_folder = target
        if target_folder is not None and isinstance(target_folder.data(0, QtCore.Qt.UserRole), self.item_class):
            target_folder = target.parent()
        # Move to toplevel
        if target_folder is None:
            target_folder = self.list_view.invisibleRootItem()
            target_folder.setData(0, QtCore.Qt.UserRole, self.folder_class())
            target_folder.data(0, QtCore.Qt.UserRole).id = 0
        # Move images in the treeview
        items_to_save = []
        for item in items_to_move:
            if isinstance(item.data(0, QtCore.Qt.UserRole), self.item_class):
                if isinstance(item.parent(), QtWidgets.QTreeWidgetItem):
                    item.parent().removeChild(item)
                else:
                    self.list_view.invisibleRootItem().removeChild(item)
                target_folder.addChild(item)
                item.setSelected(True)
                item_data = item.data(0, QtCore.Qt.UserRole)
                item_data.folder_id = target_folder.data(0, QtCore.Qt.UserRole).id
                items_to_save.append(item_data)
        target_folder.setExpanded(True)
        # Update the folder ID's of the items in the database
        self.manager.save_objects(items_to_save)
        # Sort the target folder
        sort_folders = []
        sort_items = []
        for item in target_folder.takeChildren():
            if isinstance(item.data(0, QtCore.Qt.UserRole), self.folder_class):
                sort_folders.append(item)
            if isinstance(item.data(0, QtCore.Qt.UserRole), self.item_class):
                sort_items.append(item)
        sort_folders.sort(key=lambda item: get_natural_key(item.text(0)))
        target_folder.addChildren(sort_folders)
        sort_items.sort(key=lambda item: get_natural_key(item.text(0)))
        target_folder.addChildren(sort_items)
