#
# Copyright 2006-2010 Zuza Software Foundation
#
# This file is part of translate.
#
# translate 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 2 of the License, or
# (at your option) any later version.
#
# translate 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/>.

"""factory methods to build real storage objects that conform to base.py."""

from __future__ import annotations

import os
from functools import lru_cache
from importlib import import_module
from typing import BinaryIO

from translate.storage.base import TranslationStore

# TODO: Monolingual formats (with template?)

decompressclass = {
    "gz": ("gzip", "GzipFile"),
    "bz2": ("bz2", "BZ2File"),
}


_classes_str = {
    "csv": ("csvl10n", "csvfile"),
    "tab": ("omegat", "OmegaTFileTab"),
    "utf8": ("omegat", "OmegaTFile"),
    "po": ("po", "pofile"),
    "pot": ("po", "pofile"),
    "mo": ("mo", "mofile"),
    "gmo": ("mo", "mofile"),
    "qm": ("qm", "qmfile"),
    "lang": ("mozilla_lang", "LangStore"),
    "utx": ("utx", "UtxFile"),
    "_wftm": ("wordfast", "WordfastTMFile"),
    "_trados_txt_tm": ("trados", "TradosTxtTmFile"),
    "catkeys": ("catkeys", "CatkeysFile"),
    "qph": ("qph", "QphFile"),
    "tbx": ("tbx", "tbxfile"),
    "tmx": ("tmx", "tmxfile"),
    "ts": ("ts2", "tsfile"),
    "xliff": ("xliff", "Xliff1File"),
    "xlf": ("xliff", "Xliff1File"),
    "sdlxliff": ("xliff", "Xliff1File"),
    "ftl": ("fluent", "FluentFile"),
    "toml": ("toml", "TOMLFile"),
    "wxl": ("wxl", "WxlFile"),
}
###  XXX:  if you add anything here, you must also add it to translate.storage.

"""Dictionary of file extensions and the names of their associated class.

Used for dynamic lazy loading of modules.
_ext is a pseudo extension, that is their is no real extension by that name.
"""


def _examine_txt(storefile):
    """Determine the true filetype for a .txt file."""
    if isinstance(storefile, str) and os.path.exists(storefile):
        storefile = open(storefile, "rb")
    try:
        start = storefile.read(600).strip()
    except AttributeError:
        raise ValueError("Need to read object to determine type") from None
    # Some encoding magic for Wordfast
    # pylint: disable-next=import-outside-toplevel
    from translate.storage import wordfast  # noqa: PLC0415

    encoding = "utf-16" if wordfast.TAB_UTF16 in start.split(b"\n")[0] else "iso-8859-1"
    start = start.decode(encoding)
    if "%Wordfast TM" in start:
        pseudo_extension = "_wftm"
    elif "<RTF Preamble>" in start:
        pseudo_extension = "_trados_txt_tm"
    else:
        raise ValueError("Failed to guess file type.")
    storefile.seek(0)
    return pseudo_extension


_hiddenclasses = {"txt": _examine_txt}


def _guess_extension(storefile):
    """
    Guesses the type of a file object by looking at the first few
    characters.  The return value is a file extension.
    """
    start = storefile.read(300).strip()
    if b"<xliff " in start:
        extension = "xlf"
    elif b'msgid "' in start:
        extension = "po"
    elif b"%Wordfast TM" in start:
        extension = "txt"
    elif b"<!DOCTYPE TS>" in start:
        extension = "ts"
    elif b"<tmx " in start:
        extension = "tmx"
    elif b"#UTX" in start:
        extension = "utx"
    else:
        raise ValueError("Failed to guess file type.")
    storefile.seek(0)
    return extension


def _get_dummy_name(storefile) -> str:
    """
    Provides a dummy name for a file object without a name attribute, by
    guessing the file type.
    """
    return f"dummy.{_guess_extension(storefile)}"


def _get_name(storefile):
    """Returns the filename."""
    if storefile is None:
        raise ValueError(
            "This method cannot magically produce a filename when given None as input."
        )
    if not isinstance(storefile, str):
        if not hasattr(storefile, "name"):
            storefilename = _get_dummy_name(storefile)
        else:
            storefilename = storefile.name
    else:
        storefilename = storefile
    return storefilename


@lru_cache(maxsize=128)
def import_class(module_name, class_name, prefix=None):
    if prefix:
        module_name = f"{prefix}.{module_name}"
    module = import_module(module_name)
    return getattr(module, class_name)


def getclass(
    storefile,
    localfiletype=None,
    ignore=None,
    classes=None,
    classes_str=None,
    hiddenclasses=None,
):
    """
    Factory that returns the applicable class for the type of file
    presented.  Specify ignore to ignore some part at the back of the name
    (like .gz).
    """
    storefilename = _get_name(storefile)
    if classes_str is None:
        classes_str = _classes_str
    if hiddenclasses is None:
        hiddenclasses = _hiddenclasses
    if ignore and storefilename.endswith(ignore):
        storefilename = storefilename[: -len(ignore)]
    ext = localfiletype
    if ext is None:
        root, ext = os.path.splitext(storefilename)
        ext = ext[len(os.path.extsep) :].lower()
        decomp = None
        if ext in decompressclass:
            decomp = ext
            root, ext = os.path.splitext(root)
            ext = ext[len(os.path.extsep) :].lower()
        if ext in hiddenclasses:
            guesserfn = hiddenclasses[ext]
            if decomp:
                file = import_class(*decompressclass[decomp])
                ext = guesserfn(file(storefile))
            else:
                ext = guesserfn(storefile)
    try:
        # we prefer classes (if given) since that is the older API that Pootle uses
        if classes:
            storeclass = classes[ext]
        else:
            storeclass = import_class(*classes_str[ext], "translate.storage")
    except KeyError:
        raise ValueError(f"Unknown filetype ({storefilename})") from None
    return storeclass


def getobject(
    storefile: str | TranslationStore | BinaryIO,
    localfiletype: str | None = None,
    ignore: str | None = None,
    classes: dict | None = None,
    classes_str: dict | None = None,
    hiddenclasses: dict | None = None,
) -> TranslationStore:
    """
    Factory that returns a usable object for the type of file presented.
    :param storefile: File object or file name.

    Specify ignore to ignore some part at the back of the name (like .gz).
    """
    if isinstance(storefile, TranslationStore):
        return storefile
    if classes_str is None:
        classes_str = _classes_str
    if hiddenclasses is None:
        hiddenclasses = _hiddenclasses
    if isinstance(storefile, str) and (
        os.path.isdir(storefile) or storefile.endswith(os.path.sep)
    ):
        raise ValueError(
            f'Cannot process directory "{storefile}" as a translation storage object. '
            "Please specify a path to a supported file instead."
        )
    storefilename = _get_name(storefile)
    storeclass = getclass(
        storefile,
        localfiletype,
        ignore,
        classes=classes,
        classes_str=classes_str,
        hiddenclasses=hiddenclasses,
    )
    if os.path.exists(storefilename) or not getattr(storefile, "closed", True):
        _name, ext = os.path.splitext(storefilename)
        ext = ext[len(os.path.extsep) :].lower()
        if ext in decompressclass:
            file = import_class(*decompressclass[ext])
            storefile = file(storefilename)
        store = storeclass.parsefile(storefile)
    else:
        store = storeclass()
        store.filename = storefilename
    return store


supported = [
    (
        "Gettext PO file",
        ["po", "pot"],
        [
            "text/x-gettext-catalog",
            "text/x-gettext-translation",
            "text/x-po",
            "text/x-pot",
        ],
    ),
    (
        "XLIFF Translation File",
        ["xlf", "xliff", "sdlxliff"],
        ["application/x-xliff", "application/x-xliff+xml"],
    ),
    (
        "Gettext MO file",
        ["mo", "gmo"],
        ["application/x-gettext-catalog", "application/x-mo"],
    ),
    ("Qt .qm file", ["qm"], ["application/x-qm"]),
    ("TBX Glossary", ["tbx"], ["application/x-tbx"]),
    ("TMX Translation Memory", ["tmx"], ["application/x-tmx"]),
    ("Qt Linguist Translation File", ["ts"], ["application/x-linguist"]),
    ("Qt Phrase Book", ["qph"], ["application/x-qph"]),
    ("OmegaT Glossary", ["utf8", "tab"], ["application/x-omegat-glossary"]),
    ("UTX Dictionary", ["utx"], ["text/x-utx"]),
    ("Haiku catkeys file", ["catkeys"], ["application/x-catkeys"]),
    ("Fluent file", ["ftl"], []),
    ("WiX Localization File", ["wxl"], ["text/xml"]),
]


def supported_files() -> list[tuple[str, list[str], list[str]]]:
    """
    Returns data about all supported files.

    :return: list of type that include (name, extensions, mimetypes)
    """
    return supported[:]
