# -*- coding:utf-8 -*-
#
# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
# Copyright (C) 2012-2016, Maximilian Köhl <mail@koehlma.de>
#
# 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 <http://www.gnu.org/licenses/>.

"""
This module extracts the .dic and .aff (Hunspell) dictionaries from any given
.oxt extension.

Extensions could be found at:

    http://extensions.services.openoffice.org/dictionary
"""

import functools
import gettext
import logging
import os
import shutil
import sys
import warnings
import xml.dom.minidom
import xml.parsers.expat
import zipfile

# enable deprecation warnings
warnings.simplefilter("always", DeprecationWarning)

# public objects
__all__ = [
    "extract",
    "batch_extract",
    "BadXml",
    "BadExtensionFile",
    "ExtractPathIsNoDirectory",
    "BATCH_SUCCESS",
    "BATCH_ERROR",
    "BATCH_WARNING",
]

# logger
logger = logging.getLogger(__name__)

# translation
locale_name = "py{}gtkspellcheck".format(sys.version_info.major)
_ = gettext.translation(locale_name, fallback=True).gettext


class BadXml(Exception):
    """
    The XML dictionary registry is not valid XML.
    """


class BadExtensionFile(Exception):
    """
    The extension has a wrong file format, should be a ZIP file.
    """


class ExtractPathIsNoDirectory(Exception):
    """
    The given `extract_path` is no directory.
    """


def find_dictionaries(registry):
    def oor_name(name, element):
        return element.attributes["oor:name"].value.lower() == name

    def get_property(name, properties):
        property = list(filter(functools.partial(oor_name, name), properties))
        if property:
            return property[0].getElementsByTagName("value")[0]

    result = []

    # find all "node" elements which have "dictionaries" as "oor:name" attribute
    for dictionaries in filter(
        functools.partial(oor_name, "dictionaries"),
        registry.getElementsByTagName("node"),
    ):
        # for all "node" elements in this dictionary nodes
        for dictionary in dictionaries.getElementsByTagName("node"):
            # get all "prop" elements
            properties = dictionary.getElementsByTagName("prop")
            # get the format property as text
            format = get_property("format", properties).firstChild.data.strip()
            if format and format == "DICT_SPELL":
                # find the locations property
                locations = get_property("locations", properties)
                # if the location property is text:
                # %origin%/dictionary.aff %origin%/dictionary.dic
                if locations.firstChild.nodeType == xml.dom.Node.TEXT_NODE:
                    locations = locations.firstChild.data
                    locations = locations.replace("%origin%/", "").strip()
                    result.append(locations.split())
                # otherwise:
                # <i>%origin%/dictionary.aff</i> <i>%origin%/dictionary.dic</i>
                else:
                    locations = [
                        item.firshChild.data.replace("%origin%/", "").strip()
                        for item in locations.getElementsByTagName("it")
                    ]
                    result.append(locations)

    return result


def extract(filename, target, override=False):
    """
    Extract Hunspell dictionaries out of LibreOffice ``.oxt`` extensions.

    :param filename: path to the ``.oxt`` extension
    :param target: path to extract Hunspell dictionaries to
    :param override: override existing files in the target directory
    :rtype: list of the extracted dictionaries

    This function extracts the Hunspell dictionaries (``.dic`` and ``.aff``
    files) from the given ``.oxt`` extension found to ``target``.

    Extensions could be found at:

        http://extensions.services.openoffice.org/dictionary
    """
    # TODO 5.0: remove this function
    warnings.warn(
        (
            'call to deprecated function "{}", '
            'moved to separate package "oxt_extract", '
            "will be removed in pygtkspellcheck 5.0"
        ).format(extract.__name__),
        category=DeprecationWarning,
    )
    try:
        with zipfile.ZipFile(filename, "r") as extension:
            files = extension.namelist()

            registry = "dictionaries.xcu"
            if registry not in files:
                for filename in files:
                    if filename.lower().endswith(registry):
                        registry = filename

            if registry in files:
                registry = xml.dom.minidom.parse(extension.open(registry))
                dictionaries = find_dictionaries(registry)
                extracted = []
                for dictionary in dictionaries:
                    for filename in dictionary:
                        dict_file = os.path.join(target, os.path.basename(filename))
                        if not os.path.exists(dict_file) or (
                            override and os.path.isfile(dict_file)
                        ):
                            if filename in files:
                                with open(dict_file, "wb") as _target:
                                    with extension.open(filename, "r") as _source:
                                        extracted.append(os.path.basename(filename))
                                        _target.write(_source.read())
                            else:
                                logger.warning(
                                    "dictionary exists in registry "
                                    "but not in the extension zip"
                                )
                        else:
                            logging.warning(
                                (
                                    'dictionary file "{}" already exists '
                                    "and not overriding it"
                                ).format(dict_file)
                            )
                return extracted
    except zipfile.BadZipfile:
        raise BadExtensionFile("extension is not a valid ZIP file")
    except xml.parsers.expat.ExpatError:
        raise BadXml("dictionary registry is not valid XML")


BATCH_SUCCESS = "success"
BATCH_ERROR = "error"
BATCH_WARNING = "warning"


def batch_extract(oxt_path, extract_path, override=False, move_path=None):
    """
    Uncompress, read and install LibreOffice ``.oxt`` dictionaries extensions.

    :param oxt_path: path to a directory containing the ``.oxt`` extensions
    :param extract_path: path to extract Hunspell dictionaries files to
    :param override: override already existing files
    :param move_path: optional path to move the ``.oxt`` files after processing
    :rtype: generator over all extensions, yielding result, extension name,
        error, extracted dictionaries and translated error message - result
        would be :const:`BATCH_SUCCESS` for success, :const:`BATCH_ERROR` if
        some error happened or :const:`BATCH_WARNING` which contain some warning
        messages instead of errors

    This function extracts the Hunspell dictionaries (``.dic`` and ``.aff``
    files) from all the ``.oxt`` extensions found on ``oxt_path`` directory to
    the ``extract_path`` directory.

    Extensions could be found at:

        http://extensions.services.openoffice.org/dictionary

    In detail, this functions does the following:

    1. find all the ``.oxt`` extension files within ``oxt_path``
    2. open (unzip) each extension
    3. find the dictionary definition file within (*dictionaries.xcu*)
    4. parse the dictionary definition file and locate the dictionaries files
    5. uncompress those files to ``extract_path``


    By default file overriding is disabled, set ``override`` parameter to True
    if you want to enable it. As additional option, each processed extension can
    be moved to ``move_path``.

    Example::

        for result, name, error, dictionaries, message in oxt_extract.batch_extract(...):
            if result == oxt_extract.BATCH_SUCCESS:
                print('successfully extracted extension "{}"'.format(name))
            elif result == oxt_extract.BATCH_ERROR:
                print('could not extract extension "{}"'.format(name))
                print(message)
                print('error {}'.format(error))
            elif result == oxt_extract.BATCH_WARNING:
                print('warning during processing extension "{}"'.format(name))
                print(message)
                print(error)

    """

    # TODO 5.0: remove this function
    warnings.warn(
        (
            'call to deprecated function "{}", '
            'moved to separate package "oxt_extract", '
            "will be removed in pygtkspellcheck 5.0"
        ).format(extract.__name__),
        category=DeprecationWarning,
    )

    # get the real, absolute and normalized path
    oxt_path = os.path.normpath(os.path.abspath(os.path.realpath(oxt_path)))

    # check that the input directory exists
    if not os.path.isdir(oxt_path):
        return

    # create extract directory if not exists
    if not os.path.exists(extract_path):
        os.makedirs(extract_path)

    # check that the extract path is a directory
    if not os.path.isdir(extract_path):
        raise ExtractPathIsNoDirectory("extract path is not a valid directory")

    # get all .oxt extension at given path
    oxt_files = [
        extension
        for extension in os.listdir(oxt_path)
        if extension.lower().endswith(".oxt")
    ]

    for extension_name in oxt_files:
        extension_path = os.path.join(oxt_path, extension_name)

        try:
            dictionaries = extract(extension_path, extract_path, override)
            yield BATCH_SUCCESS, extension_name, None, dictionaries, ""
        except BadExtensionFile as error:
            logger.error(
                ('extension "{}" is not a valid ZIP file').format(extension_name)
            )
            yield (
                BATCH_ERROR,
                extension_name,
                error,
                [],
                _('extension "{}" is not a valid ZIP file').format(extension_name),
            )
        except BadXml as error:
            logger.error(
                ('extension "{}" has no valid XML dictionary registry').format(
                    extension_name
                )
            )
            yield (
                BATCH_ERROR,
                extension_name,
                error,
                [],
                _('extension "{}" has no valid XML dictionary registry').format(
                    extension_name
                ),
            )

        # move the extension after processing if user requires it
        if move_path is not None:
            # create move path if it doesn't exists
            if not os.path.exists(move_path):
                os.makedirs(move_path)
            # move to the given path only if it is a directory and target
            # doesn't exists
            if os.path.isdir(move_path):
                if (
                    not os.path.exists(os.path.join(move_path, extension_name))
                    or override
                ):
                    shutil.move(extension_path, move_path)
                else:
                    logger.warning(
                        (
                            "unable to move extension, file with same "
                            "name exists within move_path"
                        )
                    )
                    yield (
                        BATCH_WARNING,
                        extension_name,
                        (
                            "unable to move extension, file with same name "
                            "exists within move_path"
                        ),
                        [],
                        _(
                            "unable to move extension, file with same name "
                            "exists within move_path"
                        ),
                    )
            else:
                logger.warning(
                    ("unable to move extension, move_path is not a " "directory")
                )
                yield (
                    BATCH_WARNING,
                    extension_name,
                    ("unable to move extension, move_path is not a " "directory"),
                    [],
                    _("unable to move extension, move_path is not a " "directory"),
                )
