# Copyright (c) 2025 Riverbank Computing Limited <info@riverbankcomputing.com>
# 
# This file is part of PyQt6.
# 
# This file may be used under the terms of the GNU General Public License
# version 3.0 as published by the Free Software Foundation and appearing in
# the file LICENSE included in the packaging of this file.  Please review the
# following information to ensure the GNU General Public License version 3.0
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
# 
# If you do not wish to use this file under the terms of the GPL version 3.0
# then you may purchase a commercial license.  For more information contact
# info@riverbankcomputing.com.
# 
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


import os
from xml.etree import ElementTree

from .user import User, UserException


class TranslationFile(User):
    """ Encapsulate a translation file. """

    def __init__(self, ts_file, no_obsolete, no_summary, **kwargs):
        """ Initialise the translation file. """

        super().__init__(**kwargs)

        if os.path.isfile(ts_file):
            self.progress("Reading {0}...".format(ts_file))

            try:
                self._root = ElementTree.parse(ts_file).getroot()
            except Exception as e:
                raise UserException(
                        "{}: {}: {}".format(ts_file,
                                "invalid translation file", str(e)))
        else:
            self._root = ElementTree.fromstring(_EMPTY_TS)

        self._ts_file = ts_file
        self._no_obsolete = no_obsolete
        self._no_summary = no_summary
        self._updated_contexts = {}

        # Create a dict of contexts keyed by the context name and having the
        # list of message elements as the value.
        self._contexts = {}

        # Also create a dict of existing translations so that they can be
        # re-used.
        self._translations = {}

        context_els = []
        for context_el in self._root:
            if context_el.tag != 'context':
                continue

            context_els.append(context_el)

            name = ''
            message_els = []

            for el in context_el:
                if el.tag == 'name':
                    name = el.text
                elif el.tag == 'message':
                    message_els.append(el)

            if name:
                self._contexts[name] = message_els

                for message_el in message_els:
                    source_el = message_el.find('source')
                    if source_el is None or not source_el.text:
                        continue

                    translation_el = message_el.find('translation')
                    if translation_el is None or not translation_el.text:
                        continue

                    self._translations[source_el.text] = translation_el.text

        # Remove the context elements but keep everything else in the root
        # (probably set by Linguist).
        for context_el in context_els:
            self._root.remove(context_el)

        # Clear the summary statistics.
        self._nr_new = 0
        self._nr_new_duplicates = 0
        self._nr_new_using_existing_translation = 0
        self._nr_existing = 0
        self._nr_kept_obsolete = 0
        self._nr_discarded_obsolete = 0
        self._nr_discarded_untranslated = 0

        # Remember all new messages so we can make the summary less confusing
        # than it otherwise might be.
        self._new_message_els = []

    def update(self, source):
        """ Update the translation file from a SourceFile object. """

        self.progress(
                "Updating {0} from {1}...".format(self._ts_file,
                        source.filename))

        for context in source.contexts:
            # Get the messages that we already know about for this context.
            try:
                message_els = self._contexts[context.name]
            except KeyError:
                message_els = []

            # Get the messages that have already been updated.
            updated_message_els = self._get_updated_message_els(context.name)

            for message in context.messages:
                message_el = self._find_message(message, message_els)

                if message_el is not None:
                    # Move the message to the updated list.
                    message_els.remove(message_el)
                    self._add_message_el(message_el, updated_message_els)
                else:
                    # See if this is a new message.  If not then we just have
                    # another location for an existing message.
                    message_el = self._find_message(message,
                         updated_message_els)

                if message_el is None:
                    message_el = self._make_message_el(message)
                    updated_message_els.append(message_el)

                    self.progress(
                            "Added new message '{0}'".format(
                                    self.pretty(message.source)))
                    self._nr_new += 1
                else:
                    self.progress(
                            "Updated message '{0}'".format(
                                    self.pretty(message.source)))

                    # Go through any translations making sure they are not
                    # 'vanished' which might happen if we have restored a
                    # previously obsolete message.
                    for translation_el in message_el.findall('translation'):
                        if translation_el.get('type') == 'vanished':
                            if translation_el.text:
                                del translation_el.attrib['type']
                            else:
                                translation_el.set('type', 'unfinished')

                    # Don't count another copy of a new message as an existing
                    # one.
                    if message_el in self._new_message_els:
                        self._nr_new_duplicates += 1
                    else:
                        self._nr_existing += 1

                message_el.insert(0, self._make_location_el(message))

    def write(self):
        """ Write the translation file back to the filesystem. """

        # If we are keeping obsolete messages then add them to the updated
        # message elements list.
        for name, message_els in self._contexts.items():
            updated_message_els = None

            for message_el in message_els:
                source = self.pretty(message_el.find('source').text)

                translation_el = message_el.find('translation')
                if translation_el is not None and translation_el.text:
                    if self._no_obsolete:
                        self.progress(
                                "Discarded obsolete message '{0}'".format(
                                        source))
                        self._nr_discarded_obsolete += 1
                    else:
                        translation_el.set('type', 'vanished')

                        if updated_message_els is None:
                            updated_message_els = self._get_updated_message_els(
                                    name)

                        self._add_message_el(message_el, updated_message_els)

                        self.progress(
                                "Kept obsolete message '{0}'".format(source))
                        self._nr_kept_obsolete += 1
                else:
                    self.progress(
                            "Discarded untranslated message '{0}'".format(
                                    source))
                    self._nr_discarded_untranslated += 1

        # Created the sorted context elements.
        for name in sorted(self._updated_contexts.keys()):
            context_el = ElementTree.Element('context')

            name_el = ElementTree.Element('name')
            name_el.text = name
            context_el.append(name_el)

            context_el.extend(self._updated_contexts[name])

            self._root.append(context_el)

        self.progress("Writing {0}...".format(self._ts_file))

        # Replicate the indentation used by Qt Linguist.  Note that there are
        # still differences in the way elements are closed.
        for el in self._root:
            ElementTree.indent(el, space='    ')

        with open(self._ts_file, 'w', encoding='utf-8', newline='\n') as f:
            f.write('<?xml version="1.0" encoding="utf-8"?>\n')
            f.write('<!DOCTYPE TS>\n')
            ElementTree.ElementTree(self._root).write(f, encoding='unicode')
            f.write('\n')

        if not self._no_summary:
            self._summary()

    @staticmethod
    def _add_message_el(message_el, updated_message_els):
        """ Add a message element to a list of updated message elements. """

        # Remove all the location elements.
        for location_el in message_el.findall('location'):
            message_el.remove(location_el)

        # Add the message to the updated list.
        updated_message_els.append(message_el)

    @classmethod
    def _find_message(cls, message, message_els):
        """ Return the message element for a message from a list. """

        for message_el in message_els:
            source = ''
            comment = ''
            extra_comment = ''
            extras = []

            # Extract the data from the element.
            for el in message_el:
                if el.tag == 'source':
                    source = el.text
                elif el.tag == 'comment':
                    comment = el.text
                elif el.tag == 'extracomment':
                    extra_comment = el.text
                elif el.tag.startswith('extra-'):
                    extras.append([el.tag[6:], el.text])

            # Compare with the message.
            if source != message.source:
                continue

            if comment != message.comment:
                continue

            if extra_comment != cls._get_message_extra_comments(message):
                continue

            if extras != message.embedded_comments.extras:
                continue

            return message_el

        return None

    @staticmethod
    def _get_message_extra_comments(message):
        """ Return a message's extra comments as they appear in a .ts file. """

        return ' '.join(message.embedded_comments.extra_comments)

    def _get_updated_message_els(self, name):
        """ Return the list of updated message elements for a context. """

        try:
            updated_message_els = self._updated_contexts[name]
        except KeyError:
            updated_message_els = []
            self._updated_contexts[name] = updated_message_els

        return updated_message_els

    def _make_location_el(self, message):
        """ Return a 'location' element. """

        return ElementTree.Element('location',
                filename=os.path.relpath(message.filename,
                        start=os.path.dirname(os.path.abspath(self._ts_file))),
                line=str(message.line_nr))

    def _make_message_el(self, message):
        """ Return a 'message' element. """

        attrs = {}

        if message.embedded_comments.message_id:
            attrs['id'] = message.embedded_comments.message_id

        if message.numerus:
            attrs['numerus'] = 'yes'

        message_el = ElementTree.Element('message', attrs)

        source_el = ElementTree.Element('source')
        source_el.text = message.source
        message_el.append(source_el)

        if message.comment:
            comment_el = ElementTree.Element('comment')
            comment_el.text = message.comment
            message_el.append(comment_el)

        if message.embedded_comments.extra_comments:
            extracomment_el = ElementTree.Element('extracomment')
            extracomment_el.text = self._get_message_extra_comments(message)
            message_el.append(extracomment_el)

        translation_el = ElementTree.Element('translation',
                type='unfinished')

        # Try and find another message with the same source and use its
        # translation if it has one.
        translation = self._translations.get(message.source)
        if translation:
            translation_el.text = translation

            self.progress(
                    "Reused existing translation for '{0}'".format(
                            self.pretty(message.source)))
            self._nr_new_using_existing_translation += 1

        if message.numerus:
            translation_el.append(ElementTree.Element(
                    'numerusform'))

        message_el.append(translation_el)

        for field, value in message.embedded_comments.extras:
            el = ElementTree.Element('extra-' + field)
            el.text = value
            message_el.append(el)

        self._new_message_els.append(message_el)

        return message_el

    def _summary(self):
        """ Display the summary of changes to the user. """

        summary_lines = []

        # Display a line of the summary and the heading if not already done.
        def summary(line):
            nonlocal summary_lines

            if not summary_lines:
                summary_lines.append(
                        "Summary of changes to {ts}:".format(ts=self._ts_file))

            summary_lines.append("    " + line)

        if self._nr_new:
            if self._nr_new_duplicates:
                summary("{0} new messages were added (and {1} duplicates)".format(
                        self._nr_new, self._nr_new_duplicates))
            else:
                summary("{0} new messages were added".format(self._nr_new))

        if self._nr_new_using_existing_translation:
            summary("{0} messages reused existing translations".format(
                    self._nr_new_using_existing_translation))

        if self._nr_existing:
            summary("{0} existing messages were found".format(
                    self._nr_existing))

        if self._nr_kept_obsolete:
            summary("{0} obsolete messages were kept".format(
                    self._nr_kept_obsolete))

        if self._nr_discarded_obsolete:
            summary("{0} obsolete messages were discarded".format(
                    self._nr_discarded_obsolete))

        if self._nr_discarded_untranslated:
            summary("{0} untranslated messages were discarded".format(
                    self._nr_discarded_untranslated))

        if not summary_lines:
            summary_lines.append("{ts} was unchanged".format(ts=self._ts_file))

        print(os.linesep.join(summary_lines))


# The XML of an empty .ts file.  This is what a current lupdate will create
# with an empty C++ source file.
_EMPTY_TS = '''<TS version="2.1">
</TS>
'''
