# -*- 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/>. #
##########################################################################

from zipfile import is_zipfile

from lxml import etree, objectify

from openlp.core.common.i18n import get_language, translate
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.lib.exceptions import ValidationError
from openlp.core.lib.ui import critical_error_message_box
from openlp.plugins.bibles.lib.db import AlternativeBookNamesDB, BibleDB, BiblesResourcesDB


class BibleImport(BibleDB, LogMixin, RegistryProperties):
    """
    Helper class to import bibles from a third party source into OpenLP
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.file_path = kwargs.get('file_path')
        self.wizard = None
        self.stop_import_flag = False
        Registry().register_function('openlp_stop_wizard', self.stop_import)

    @staticmethod
    def is_compressed(file_path):
        """
        Check if the supplied file is compressed

        :param pathlib.Path file_path: A path to the file to check
        """
        if is_zipfile(file_path):
            critical_error_message_box(
                message=translate('BiblesPlugin.BibleImport',
                                  'The file "{file}" you supplied is compressed. You must decompress it before import.'
                                  ).format(file=file_path))
            return True
        return False

    def get_book_ref_id_by_name(self, book, maxbooks=66, language_id=None):
        """
        Find the book id from the name or abbreviation of the book. If it doesn't currently exist, ask the user.

        :param book: The name or abbreviation of the book
        :param maxbooks: The number of books in the bible
        :param language_id: The language_id of the bible
        :return: The id of the bible, or None
        """
        self.log_debug('BibleDB.get_book_ref_id_by_name:("{book}", "{lang}")'.format(book=book, lang=language_id))
        book_temp = BiblesResourcesDB.get_book(book, True)
        if book_temp:
            return book_temp['id']
        book_id = BiblesResourcesDB.get_alternative_book_name(book)
        if book_id:
            return book_id
        book_id = AlternativeBookNamesDB.get_book_reference_id(book)
        if book_id:
            return book_id
        from openlp.plugins.bibles.forms import BookNameForm
        book_name = BookNameForm(self.wizard)
        if book_name.exec(book, self.get_books(), maxbooks) and book_name.book_id:
            AlternativeBookNamesDB.create_alternative_book_name(book, book_name.book_id, language_id)
            return book_name.book_id

    def get_language(self, bible_name=None):
        """
        If no language is given it calls a dialog window where the user could  select the bible language.
        Return the language id of a bible.

        :param bible_name: The language the bible is.
        """
        self.log_debug('BibleImpoer.get_language()')
        from openlp.plugins.bibles.forms import LanguageForm
        language_id = None
        language_form = LanguageForm(self.wizard)
        if language_form.exec(bible_name):
            combo_box = language_form.language_combo_box
            language_id = combo_box.itemData(combo_box.currentIndex())
        else:
            return False
        if not language_id:
            return None
        self.save_meta('language_id', language_id)
        return language_id

    def get_language_id(self, file_language=None, bible_name=None):
        """
        Get the language_id for the language of the bible. Fallback to user input if we cannot do this pragmatically.

        :param file_language: Language of the bible. Possibly retrieved from the file being imported. Str
        :param bible_name: Name of the bible to display on the get_language dialog. Str
        :return: The id of a language Int or None
        """
        language_id = None
        if file_language:
            language = get_language(file_language)
            if language and language.id:
                language_id = language.id
        if not language_id:
            # The language couldn't be detected, ask the user
            language_id = self.get_language(bible_name)
        if not language_id:
            # User cancelled get_language dialog
            self.log_error('Language detection failed when importing from "{name}". User aborted language selection.'
                           .format(name=bible_name))
            return None
        self.save_meta('language_id', language_id)
        return language_id

    def find_and_create_book(self, name, no_of_books, language_id, guess_id=None):
        """
        Find the OpenLP book id and then create the book in this bible db

        :param name: Name of the book. If None, then fall back to the guess_id Str
        :param no_of_books: The total number of books contained in this bible Int
        :param language_id: The OpenLP id of the language of this bible Int
        :param guess_id: The guessed id of the book, used if name is None Int
        :return:
        """
        if name:
            book_ref_id = self.get_book_ref_id_by_name(name, no_of_books, language_id)
        else:
            self.log_debug('No book name supplied. Falling back to guess_id')
            book_ref_id = guess_id
        if not book_ref_id:
            raise ValidationError(msg='Could not resolve book_ref_id in "{}"'.format(self.file_path))
        book_details = BiblesResourcesDB.get_book_by_id(book_ref_id)
        if book_details is None:
            raise ValidationError(msg='book_ref_id: {book_ref} Could not be found in the BibleResourcesDB while '
                                      'importing {file}'.format(book_ref=book_ref_id, file=self.file_path))
        return self.create_book(name, book_ref_id, book_details['testament_id'])

    def parse_xml(self, file_path, use_objectify=False, elements=None, tags=None):
        """
        Parse and clean the supplied file by removing any elements or tags we don't use.
        :param file_path: The filename of the xml file to parse. Str
        :param use_objectify: Use the objectify parser rather than the etree parser. (Bool)
        :param elements: A tuple of element names (Str) to remove along with their content.
        :param tags: A tuple of element names (Str) to remove, preserving their content.
        :return: The root element of the xml document
        """
        try:
            with file_path.open('rb') as import_file:
                # NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own
                # encoding detection, and the two mechanisms together interfere with each other.
                if not use_objectify:
                    tree = etree.parse(import_file, parser=etree.XMLParser(recover=True))
                else:
                    tree = objectify.parse(import_file, parser=objectify.makeparser(recover=True))
                if elements or tags:
                    self.wizard.increment_progress_bar(
                        translate('BiblesPlugin.OsisImport', 'Removing unused tags (this may take a few minutes)...'))
                if elements:
                    # Strip tags we don't use - remove content
                    etree.strip_elements(tree, elements, with_tail=False)
                if tags:
                    # Strip tags we don't use - keep content
                    etree.strip_tags(tree, tags)
                return tree.getroot()
        except OSError as e:
            self.log_exception('Opening {file_name} failed.'.format(file_name=e.filename))
            critical_error_message_box(
                title='An Error Occured When Opening A File',
                message='The following error occurred when trying to open\n{file_name}:\n\n{error}'
                .format(file_name=e.filename, error=e.strerror))
        return None

    def register(self, wizard):
        """
        This method basically just initialises the database. It is called from the Bible Manager when a Bible is
        imported. Descendant classes may want to override this method to supply their own custom
        initialisation as well.

        :param wizard: The actual Qt wizard form.
        """
        self.wizard = wizard
        return self.name

    def set_current_chapter(self, book_name, chapter_name):
        self.wizard.increment_progress_bar(translate('BiblesPlugin.OsisImport', 'Importing {book} {chapter}...')
                                           .format(book=book_name, chapter=chapter_name))

    def stop_import(self):
        """
        Stops the import of the Bible.
        """
        self.log_debug('Stopping import')
        self.stop_import_flag = True

    def validate_xml_file(self, file_path, tag):
        """
        Validate the supplied file

        :param file_path: The supplied file
        :param tag: The expected root tag type
        :return: True if valid. ValidationError is raised otherwise.
        """
        if BibleImport.is_compressed(file_path):
            raise ValidationError(msg='Compressed file')
        bible = self.parse_xml(file_path, use_objectify=True)
        if bible is None:
            raise ValidationError(msg='Error when opening file')
        root_tag = bible.tag.lower()
        bible_type = translate('BiblesPlugin.BibleImport', 'unknown type of',
                               'This looks like an unknown type of XML bible.')
        if root_tag == tag:
            return True
        elif root_tag == 'bible':
            bible_type = "OpenSong"
        elif root_tag == '{http://www.bibletechnologies.net/2003/osis/namespace}osis':
            bible_type = 'OSIS'
        elif root_tag == 'xmlbible':
            bible_type = 'Zefania'
        critical_error_message_box(
            message=translate('BiblesPlugin.BibleImport',
                              'Incorrect Bible file type supplied. This looks like an {bible_type} XML bible.'
                              .format(bible_type=bible_type)))
        raise ValidationError(msg='Invalid xml.')
