#
# Copyright 2008-2014 Zuza Software Foundation
#
# This file is part of the Translate Toolkit.
#
# 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 contextlib import contextmanager

from lxml import etree

from translate.misc.xml_helpers import parse_xml_file
from translate.storage.placeables import StringElem, xliff
from translate.storage.xliff import xliffunit
from translate.storage.xml_extract import misc, xpath_breadcrumb


class Translatable:
    """
    A node corresponds to a translatable element. A node may have children,
    which correspond to placeables.
    """

    def __init__(
        self, placeable_name, xpath, dom_node, source, is_inline=False
    ) -> None:
        self.placeable_name = placeable_name
        self.source = source
        self.xpath = xpath
        self.is_inline = is_inline
        self.dom_node = dom_node

    @property
    def placeables(self):
        return [
            placeable
            for placeable in self.source
            if isinstance(placeable, Translatable)
        ]

    @property
    def has_translatable_text(self):
        """
        Check if it contains any chunk of text with more than whitespace.

        If not, then there's nothing to translate.
        """
        return any(
            (isinstance(chunk, str) and bool(chunk.strip()))
            or (
                isinstance(chunk, Translatable)
                and chunk.is_inline
                and chunk.has_translatable_text
            )
            for chunk in self.source
        )


def reduce_unit_tree(f, unit_node, *state):
    return misc.reduce_tree(
        f, unit_node, unit_node, lambda unit_node: unit_node.placeables, *state
    )


class ParseState:
    """
    Maintain constants and variables used during the walking of a DOM tree
    (via the function apply).
    """

    def __init__(
        self, no_translate_content_elements, inline_elements={}, nsmap={}
    ) -> None:
        self.no_translate_content_elements = no_translate_content_elements
        self.inline_elements = inline_elements
        self.is_inline = False
        self.xpath_breadcrumb = xpath_breadcrumb.XPathBreadcrumb()
        self.placeable_name = "<top-level>"
        self.nsmap = nsmap


def _process_placeable(dom_node, state):
    """
    Process current placeable.

    This returns all nested translatable content for this placeable as a single
    Translatable object, or just returns an empty Translatable object for this
    placeable if there is no nested translatable content.
    """
    placeable = find_translatable_dom_nodes(dom_node, state, process_translatable)

    if len(placeable) == 0:
        # There are no recognized child tags and thus no Translatable object is
        # returned. So create a Translatable with the name "placeable".
        return Translatable("placeable", state.xpath_breadcrumb.xpath, dom_node, [])
    if len(placeable) == 1:
        # The ideal situation: we got exactly one Translatable back when
        # processing this tree.
        return placeable[0]
    raise ValueError(
        "BUG: find_translatable_dom_nodes should never return "
        "more than a single Translatable object"
    )


def process_translatable(dom_node, state):
    """
    Process a translatable DOM node.

    Any translatable content present in a child node is treated as a placeable.
    """
    source = [str(dom_node.text or "")]

    # Append Translatable objects and unicode strings for the translatable
    # content for all the children.
    for child in dom_node:
        source.extend((_process_placeable(child, state), str(child.tail or "")))

    translatable = Translatable(
        state.placeable_name,
        state.xpath_breadcrumb.xpath,
        dom_node,
        source,
        state.is_inline,
    )
    return [translatable]


def _has_idml_translatable_content(dom_node):
    has_translatable_content = True
    if dom_node.tag == "ParagraphStyleRange":
        has_translatable_content = False

        for content_node in dom_node.findall(".//Content"):
            # Iterate over all the Content tags in this ParagraphStyleRange tag.
            if content_node.text is not None and content_node.text.strip():
                return True

            for child in content_node.iterdescendants():
                # The Content node can just have a child before any nested
                # text, and therefore its text is None, so we have to check if
                # it has children, and if any of its children has text.
                if (
                    not isinstance(child, etree._ProcessingInstruction)
                    and child.text is not None
                    and child.text.strip()
                ):
                    return True

                if child.tail is not None and child.tail.strip():
                    return True

    return has_translatable_content


def _retrieve_idml_placeables(dom_node, state):
    source = []
    for child in dom_node:
        if not isinstance(child, etree._Element):
            continue

        if isinstance(child, etree._ProcessingInstruction):
            # TODO this probably won't be using the right xpath.
            source.append(
                Translatable(
                    "placeable", state.xpath_breadcrumb.xpath, child, [], False
                )
            )

            if child.tail is not None and child.tail.strip():
                source.append(str(child.tail))

            continue

        namespace, tag = misc.parse_tag(child.tag)

        with parse_status_set(namespace, tag, state):
            # Ensure we extract all the tags below ParagraphStyleRange as
            # placeables, independently of them being translatable or not.
            # state.is_inline = True

            nested_stuff = []

            if child.text is not None and child.text.strip():
                nested_stuff = [str(child.text)]

            nested_stuff.extend(_retrieve_idml_placeables(child, state))

            source.append(
                Translatable(
                    "placeable",
                    state.xpath_breadcrumb.xpath,
                    child,
                    nested_stuff,
                    state.is_inline,
                )
            )

            if child.tail is not None and child.tail.strip():
                source.append(str(child.tail))

    return source


def process_idml_translatable(dom_node, state):
    if _has_idml_translatable_content(dom_node):
        source = _retrieve_idml_placeables(dom_node, state)

        translatable = Translatable(
            state.placeable_name,
            state.xpath_breadcrumb.xpath,
            dom_node,
            source,
            state.is_inline,
        )
        return [translatable]

    return []


def _process_children(dom_node, state, process_func):
    """
    Process an untranslatable DOM node.

    Since the node is untranslatable it just returns any translatable content
    present in its child nodes.
    """
    children = [
        find_translatable_dom_nodes(child, state, process_func) for child in dom_node
    ]

    # Flatten a list of lists into a list of elements
    children = [child for child_list in children for child in child_list]

    if len(children) > 1:
        _namespace, tag = misc.parse_tag(dom_node.tag)
        intermediate_translatable = Translatable(
            tag, state.xpath_breadcrumb.xpath, dom_node, children
        )
        return [intermediate_translatable]
    return children


def compact_tag(nsmap, namespace, tag) -> str:
    if namespace is None:
        return tag
    if namespace in nsmap:
        prefix = nsmap[namespace]
        return f"{prefix or ''}:{tag}"
    return f"{{{namespace}}}{tag}"


@contextmanager
def parse_status_set(namespace, tag, state):
    # Set XPath breadcrumb item for the current node.
    xpath_item = compact_tag(state.nsmap, namespace, tag)
    state.xpath_breadcrumb.start_tag(xpath_item)

    # Set the placeable name for the current node.
    old_placeable_name = state.placeable_name
    state.placeable_name = tag

    # Set the inline status for the current node.
    old_inline = state.is_inline
    state.is_inline = (namespace, tag) in state.inline_elements

    try:
        yield state
    finally:
        # Reset inline status, placeable name and XPath breadcrumb to the
        # previous values.
        state.is_inline = old_inline
        state.placeable_name = old_placeable_name
        state.xpath_breadcrumb.end_tag()


def find_translatable_dom_nodes(dom_node, state, process_func=process_translatable):
    # For now, we only want to deal with XML elements.
    # And we want to avoid processing instructions, which
    # are XML elements (in the inheritance hierarchy).
    if not isinstance(dom_node, etree._Element) or isinstance(
        dom_node, etree._ProcessingInstruction
    ):
        return []

    namespace, tag = misc.parse_tag(dom_node.tag)

    with parse_status_set(namespace, tag, state):
        if (namespace, tag) not in state.no_translate_content_elements:
            return process_func(dom_node, state)
        return _process_children(dom_node, state, process_func)


class IdMaker:
    def __init__(self) -> None:
        self._max_id = 0
        self._obj_id_map = {}

    def get_id(self, obj):
        if not self.has_id(obj):
            self._obj_id_map[obj] = self._max_id
            self._max_id += 1
        return self._obj_id_map[obj]

    def has_id(self, obj):
        return obj in self._obj_id_map


def _to_placeables(parent_translatable, translatable, id_maker):
    """
    Convert the translatable object to a list of strings and XLIFF
    placeables.
    """
    result = []
    for chunk in translatable.source:
        if isinstance(chunk, str):
            result.append(chunk)
        else:
            id = str(id_maker.get_id(chunk))
            if chunk.is_inline:
                sub = _to_placeables(parent_translatable, chunk, id_maker)
                result.append(xliff.G(id=id, sub=sub))
            else:
                result.append(xliff.X(id=id, xid=chunk.xpath))
    return result


def _make_store_adder(store):
    """
    Return a function which, when called with a Translatable will add
    a unit to 'store'. The placeables will be represented as strings according
    to 'placeable_quoter'.
    """
    id_maker = IdMaker()

    def add_translatable_to_store(parent_translatable, translatable) -> None:
        """
        Construct a new translation unit, set its source and location
        information and add it to 'store'.
        """
        unit = store.UnitClass("")
        unit.rich_source = [
            StringElem(_to_placeables(parent_translatable, translatable, id_maker))
        ]
        if isinstance(unit, xliffunit):
            unit.setid(translatable.xpath)
        else:
            unit.addlocation(translatable.xpath)
        store.addunit(unit)

    return add_translatable_to_store


def make_postore_adder(store, id_maker, filename):
    """
    Return a function which, when called with a Translatable will add
    a unit to 'store'. The placeables will be represented as strings according
    to 'placeable_quoter'.
    """

    def add_translatable_to_store(parent_translatable, translatable) -> None:
        """
        Construct a new translation unit, set its source and location
        information and add it to 'store'.
        """
        xliff_unit = xliffunit("")
        placeables = _to_placeables(parent_translatable, translatable, id_maker)
        xliff_unit.rich_source = [StringElem(placeables)]

        # Get the plain text for the unit source. The output is enclosed within
        # XLIFF source tags we don't want, so strip them.
        unit_source = etree.tostring(xliff_unit.source_dom, encoding="unicode")
        unit_source = unit_source[unit_source.find(">", 1) + 1 :]
        unit_source = unit_source[: unit_source.rfind("<", 1)]

        # Create the PO unit and add it to the PO store.
        po_unit = store.UnitClass(unit_source)
        po_unit.addlocation(translatable.xpath)
        po_unit.addlocation(filename)
        store.addunit(po_unit)

    return add_translatable_to_store


def _walk_idml_translatable_tree(
    translatables, store_adder, parent_translatable
) -> None:
    """
    Traverse all the found IDML translatables and add them to the Store.

    Inline translatables are not added to the Store.
    """
    for translatable in translatables:
        if translatable.dom_node.tag == "ParagraphStyleRange":
            store_adder(parent_translatable, translatable)
            continue

        new_parent_translatable = parent_translatable
        _walk_idml_translatable_tree(
            translatable.placeables, store_adder, new_parent_translatable
        )


def _walk_translatable_tree(
    translatables, store_adder, parent_translatable, stored_by_parent=False
) -> None:
    """
    Traverse all the found translatables and add them to the Store.

    Inline translatables are not added to the Store.
    """
    for translatable in translatables:
        store_here = translatable.has_translatable_text and (
            not translatable.is_inline or not stored_by_parent
        )
        if store_here:
            store_adder(parent_translatable, translatable)
        new_parent_translatable = parent_translatable
        _walk_translatable_tree(
            translatable.placeables,
            store_adder,
            new_parent_translatable,
            store_here or stored_by_parent,
        )


def reverse_map(a_map):
    return {value: key for key, value in a_map.items()}


def build_idml_store(odf_file, store, parse_state, store_adder=None):
    """Build a store for the given IDML file."""
    store_adder = store_adder or _make_store_adder(store)
    tree = parse_xml_file(odf_file)
    root = tree.getroot()
    parse_state.nsmap = reverse_map(root.nsmap)
    translatables = find_translatable_dom_nodes(
        root, parse_state, process_idml_translatable
    )
    _walk_idml_translatable_tree(translatables, store_adder, None)
    return tree


def build_store(
    odf_file,
    store,
    parse_state,
    store_adder=None,
    *,
    collect_ids: bool = True,
):
    """Build a store for the given XML file."""
    store_adder = store_adder or _make_store_adder(store)
    tree = parse_xml_file(odf_file, collect_ids=collect_ids)
    root = tree.getroot()
    parse_state.nsmap = reverse_map(root.nsmap)
    translatables = find_translatable_dom_nodes(root, parse_state)
    _walk_translatable_tree(translatables, store_adder, None)
    return tree
