#! /usr/bin/env python3
#
# update_po - a gramps tool to update translations
#
# Copyright (C) 2006-2006  Kees Bakker
# Copyright (C) 2006       Brian Matherly
# Copyright (C) 2008       Stephen George
# Copyright (C) 2012
# Copyright (C) 2020       Nick Hall
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

"""
update_po.py for Gramps translations.

Examples:
   python update_po.py -t

      Tests if 'gettext' and 'python' are well configured.

   python update_po.py -h

      Calls help and command line interface.

   python update_po.py -p

      Generates a new template/catalog (gramps.pot).

   python update_po.py -m de.po

      Merges 'de.po' file with 'gramps.pot'.

   python update_po.py -k de.po

      Checks 'de.po' file, tests to compile and generates a textual resume.
"""

from __future__ import print_function

import os
import sys
import shutil
from argparse import ArgumentParser
from tokenize import tokenize, STRING, COMMENT, NL, TokenError

# Windows OS

if sys.platform in ["linux", "linux2", "darwin", "cygwin"] or shutil.which("msgmerge"):
    msgmergeCmd = "msgmerge"
    msgfmtCmd = "msgfmt"
    msgattribCmd = "msgattrib"
    xgettextCmd = "xgettext"
    pythonCmd = os.path.join(sys.prefix, "bin", "python3")
elif sys.platform == "win32":
    # GetText Win 32 obtained from http://gnuwin32.sourceforge.net/packages/gettext.htm
    # ....\gettext\bin\msgmerge.exe needs to be on the path
    msgmergeCmd = os.path.join(
        "C:", "Program Files(x86)", "gettext", "bin", "msgmerge.exe"
    )
    msgfmtCmd = os.path.join("C:", "Program Files(x86)", "gettext", "bin", "msgfmt.exe")
    msgattribCmd = os.path.join(
        "C:", "Program Files(x86)", "gettext", "bin", "msgattrib.exe"
    )
    xgettextCmd = os.path.join(
        "C:", "Program Files(x86)", "gettext", "bin", "xgettext.exe"
    )
    pythonCmd = os.path.join(sys.prefix, "bin", "python.exe")

# Others OS

elif sys.platform in ["linux", "linux2", "darwin", "cygwin"]:
    msgmergeCmd = "msgmerge"
    msgfmtCmd = "msgfmt"
    msgattribCmd = "msgattrib"
    xgettextCmd = "xgettext"
    pythonCmd = os.path.join(sys.prefix, "bin", "python3")
else:
    print("Found platform %s, OS %s" % (sys.platform, os.name))
    print("Update PO ERROR: unknown system, don't know msgmerge, ... commands")
    sys.exit(0)

# List of available languages, useful for grouped actions

# need files with po extension
LANG = [file for file in os.listdir(".") if file.endswith(".po")]
# add a special 'all' argument (for 'check' and 'merge' arguments)
LANG.append("all")
# visual polish on the languages list
LANG.sort()


def tests():
    """
    Testing installed programs.
    We made tests (-t flag) by displaying versions of tools if properly
    installed. Cannot run all commands without 'gettext' and 'python'.
    """
    try:
        print("\n====='msgmerge'=(merge our translation)================\n")
        os.system("""%(program)s -V""" % {"program": msgmergeCmd})
    except:
        print(
            "Please, install %(program)s for updating your translation"
            % {"program": msgmergeCmd}
        )

    try:
        print("\n==='msgfmt'=(format our translation for installation)==\n")
        os.system("""%(program)s -V""" % {"program": msgfmtCmd})
    except:
        print(
            "Please, install %(program)s for checking your translation"
            % {"program": msgfmtCmd}
        )

    try:
        print("\n===='msgattrib'==(list groups of messages)=============\n")
        os.system("""%(program)s -V""" % {"program": msgattribCmd})
    except:
        print(
            "Please, install %(program)s for listing groups of messages"
            % {"program": msgattribCmd}
        )

    try:
        print("\n===='xgettext' =(generate a new template)==============\n")
        os.system("""%(program)s -V""" % {"program": xgettextCmd})
    except:
        print(
            "Please, install %(program)s for generating a new template"
            % {"program": xgettextCmd}
        )

    try:
        print("\n=================='python'=============================\n")
        os.system("""%(program)s -V""" % {"program": pythonCmd})
    except:
        print("Please, install python")


def main():
    """
    The utility for handling translation stuff.
    What is need by Gramps, nothing more.
    """

    parser = ArgumentParser(
        description="This program generates a new template and "
        "also provides some common features.",
    )
    parser.add_argument(
        "-t",
        "--test",
        action="store_true",
        dest="test",
        default=True,
        help="test if 'python' and 'gettext' are properly installed",
    )

    parser.add_argument(
        "-x",
        "--xml",
        action="store_true",
        dest="xml",
        default=False,
        help="extract messages from xml based file formats",
    )
    parser.add_argument(
        "-g",
        "--glade",
        action="store_true",
        dest="glade",
        default=False,
        help="extract messages from glade file format only",
    )
    parser.add_argument(
        "-c",
        "--clean",
        action="store_true",
        dest="clean",
        default=False,
        help="remove created files",
    )
    parser.add_argument(
        "-p",
        "--pot",
        action="store_true",
        dest="catalog",
        default=False,
        help="create a new catalog",
    )

    update = parser.add_argument_group("Update", "Maintenance around translations")

    # need at least one argument (sv.po, de.po, etc ...)

    # lang.po files maintenance
    update.add_argument(
        "-m", dest="merge", choices=LANG, help="merge lang.po files with last catalog"
    )

    update.add_argument("-k", dest="check", choices=LANG, help="check lang.po files")

    # testing stage
    trans = parser.add_argument_group(
        "Translation", "Display content of translations file"
    )

    # need one argument (eg, de.po)

    trans.add_argument(
        "-u",
        dest="untranslated",
        choices=[file for file in os.listdir(".") if file.endswith(".po")],
        help="list untranslated messages",
    )
    trans.add_argument(
        "-f",
        dest="fuzzy",
        choices=[file for file in os.listdir(".") if file.endswith(".po")],
        help="list fuzzy messages",
    )

    args = parser.parse_args()
    namespace, extra = parser.parse_known_args()

    if args.test:
        tests()

    if args.xml:
        extract_xml()

    if args.glade:
        create_filesfile()
        extract_glade()
        if os.path.isfile("tmpfiles"):
            os.unlink("tmpfiles")

    if args.catalog:
        retrieve()

    if args.clean:
        clean()

    if args.merge:
        # retrieve() windows os?
        if sys.argv[2:] == ["all"]:
            sys.argv[2:] = LANG
        merge(sys.argv[2:])

    if args.check:
        # retrieve() windows os?
        if sys.argv[2:] == ["all"]:
            sys.argv[2:] = LANG
        check(sys.argv[2:])

    if args.untranslated:
        untranslated(sys.argv[2:])

    if args.fuzzy:
        fuzzy(sys.argv[2:])


def create_filesfile():
    """
    Create a file with all files that we should translate.
    These are all python files not in POTFILES.skip added with those in
    POTFILES.in
    """
    dir = os.getcwd()
    topdir = os.path.normpath(os.path.join(dir, "..", "gramps"))
    lentopdir = len(topdir)
    with open("POTFILES.in") as f:
        infiles = dict(
            ["../" + file.strip(), None]
            for file in f
            if file.strip() and not file[0] == "#"
        )

    with open("POTFILES.skip") as f:
        notinfiles = dict(
            ["../" + file.strip(), None] for file in f if file and not file[0] == "#"
        )

    for dirpath, dirnames, filenames in os.walk(topdir):
        root, subdir = os.path.split(dirpath)
        if subdir.startswith("."):
            # don't continue in this dir
            dirnames[:] = []
            continue
        for dirname in dirnames:
            # Skip hidden and system directories:
            if dirname.startswith(".") or dirname in ["po", "locale"]:
                dirnames.remove(dirname)
        # add the files which are python or glade files
        # if the directory does not exist or is a link, do nothing
        if not os.path.isdir(dirpath) or os.path.islink(dirpath):
            continue

        for filename in os.listdir(dirpath):
            name = os.path.split(filename)[1]
            if name.endswith((".py", ".glade")):
                full_filename = os.path.join(dirpath, filename)
                # Skip the file if in POTFILES.skip
                if full_filename[lentopdir:] in notinfiles:
                    infiles["../gramps" + full_filename[lentopdir:]] = None
    # now we write out all the files in form ../gramps/filename
    with open("tmpfiles", "w") as f:
        for file in sorted(infiles.keys()):
            f.write(file)
            f.write("\n")


def listing(name, extensionlist):
    """
    List files according to extensions.
    Parsing from a textual file (gramps) is faster and easy for maintenance.
    Like POTFILES.in and POTFILES.skip
    """

    with open("tmpfiles") as f:
        files = [file.strip() for file in f if file and not file[0] == "#"]

    with open(name, "w") as temp:
        for entry in files:
            for ext in extensionlist:
                if entry.endswith(ext):
                    temp.write(entry)
                    temp.write("\n")
                    break


def headers():
    """
    Look at existing C file format headers.
    Generated by 'intltool-extract' but want to get rid of this
    dependency (perl, just a set of tools).
    """
    headers = []

    # in.h; extract_xml
    if os.path.isfile("""fragments.pot"""):
        headers.append("""fragments.pot""")

    return headers


def extract_xml():
    """
    Extract translation strings from XML based, mime and desktop files.
    Uses custom ITS rules found in the po/its directory.
    """
    if not os.path.isfile("gramps.pot"):
        create_template()

    for input_file in [
        "../data/holidays.xml",
        "../data/tips.xml",
        "../data/org.gramps_project.Gramps.xml.in",
        "../data/org.gramps_project.Gramps.metainfo.xml.in",
        "../data/org.gramps_project.Gramps.desktop.in",
    ]:
        os.system(
            (
                "GETTEXTDATADIR=. %(xgettext)s -F -j "
                "-o gramps.pot --from-code=UTF-8 %(inputfile)s"
            )
            % {"xgettext": xgettextCmd, "inputfile": input_file}
        )
        print(input_file)


def create_template():
    """
    Create a new file for template, if it does not exist.
    """
    with open("gramps.pot", "w") as template:
        pass


def extract_glade():
    """
    Extract messages from a temp file with all .glade
    """
    if not os.path.isfile("gramps.pot"):
        create_template()

    listing("glade.txt", [".glade"])
    os.system(
        """%(xgettext)s -F --add-comments -j -L Glade """
        """--from-code=UTF-8 -o gramps.pot --files-from=glade.txt"""
        % {"xgettext": xgettextCmd}
    )


def xml_fragments():
    """search through the file for xml fragments that contain the
    'translate="yes">string<' pattern.  These need to be added to the message
    catalog"""
    with open("tmpfiles") as __f:
        files = [
            file.strip()
            for file in __f
            if file and not (file[0] == "#") and file.endswith(".py\n")
        ]
        print("Checking for XML fragments in Python files")
        modop = int(len(files) / 20)
    wfp = open("fragments.pot", "w", encoding="utf-8")
    wfp.write('msgid ""\n')
    wfp.write('msgstr ""\n')
    wfp.write('"Content-Type: text/plain; charset=UTF-8\\n"\n\n')
    for indx, filename in enumerate(files):
        if not indx % modop:
            print(int(indx / len(files) * 100), end="\r")
        fp = open(filename, "rb")
        try:
            tokens = tokenize(fp.readline)
            in_string = False
            for _token, _text, _start, _end, _line in tokens:
                if _text.startswith(('"""', "'''")):
                    _text = _text[3:]
                elif _text.startswith(('"', "'")):
                    _text = _text[1:]
                if _text.endswith(('"""', "'''")):
                    _text = _text[:-3]
                elif _text.endswith(('"', "'")):
                    _text = _text[:-1]
                if _token == STRING and not in_string:
                    in_string = True
                    line_no = _start[0]
                    text = _text
                    continue
                elif _token == STRING and in_string:
                    text += _text
                    continue
                elif _token == COMMENT or _token == NL and in_string:
                    # need to ignore comments and concatinate strings
                    _ml = True
                    continue
                elif in_string:
                    in_string = False
                    end = 0
                    # _find_message_in_xml(text)
                    while True:
                        fnd = text.find('translatable="yes">', end)
                        if fnd == -1:
                            break
                        end = text.find("<", fnd)
                        if end == -1:
                            print(
                                "\nBad xml fragment '%s' at %s line %d"
                                % (text[fnd:], filename, _start[0])
                            )
                            break
                        msg = text[fnd + 19 : end]
                        if "%s" in msg or (msg.startswith("{") and msg.endswith("}")):
                            print(
                                "\n#: %s:%d  Are you sure you want to "
                                'translate the "%%s"???' % (filename, line_no)
                            )
                            break
                        wfp.write(
                            '#: %s:%d\nmsgid "%s"\nmsgstr ""\n'
                            % (filename, line_no, msg)
                        )
        except TokenError as e:
            print(
                "\n%s: %s, line %d, column %d"
                % (e.args[0], filename, e.args[1][0], e.args[1][1]),
                file=sys.stderr,
            )
        finally:
            fp.close()
    wfp.close()


def retrieve():
    """
    Extract messages from all files used by Gramps (python, glade, xml)
    """
    create_template()

    create_filesfile()
    xml_fragments()

    listing("python.txt", [".py", ".py.in"])

    # additional keywords must always be kept in sync with those in genpot.sh
    os.system(
        """%(xgettext)s -F --add-comments=Translators -j """
        """--directory=./ -d gramps -L Python """
        """-o gramps.pot --files-from=python.txt --debug """
        """--keyword=_ --keyword=_:1,2c """
        """--keyword=_T_ --keyword=_T_:1,2c """
        """--keyword=trans_text --keyword=trans_text:1,2c """
        """--keyword=ngettext --keyword=sgettext """
        """--from-code=UTF-8""" % {"xgettext": xgettextCmd}
    )

    extract_glade()
    extract_xml()

    # C format header (.h extension)
    for h in headers():
        print("xgettext for %s" % h)
        os.system(
            """%(xgettext)s -F --add-comments=Translators -j """
            """-o gramps.pot --keyword=N_ --from-code=UTF-8 %(head)s"""
            % {"xgettext": xgettextCmd, "head": h}
        )
    clean()


def clean():
    """
    Remove created files (C format headers, temp listings)
    """
    for h in headers():
        if os.path.isfile(h):
            os.unlink(h)
            print("Remove %(head)s" % {"head": h})

    if os.path.isfile("python.txt"):
        os.unlink("python.txt")
        print("Remove 'python.txt'")

    if os.path.isfile("glade.txt"):
        os.unlink("glade.txt")
        print("Remove 'glade.txt'")

    if os.path.isfile("tmpfiles"):
        os.unlink("tmpfiles")
        print("Remove 'tmpfiles'")


def merge(args):
    """
    Merge messages with 'gramps.pot'
    """
    for arg in args:
        if arg == "all":
            continue
        print("Merge %(lang)s with current template" % {"lang": arg})
        os.system(
            """%(msgmerge)s -U %(lang)s gramps.pot"""
            % {"msgmerge": msgmergeCmd, "lang": arg}
        )
        print("Updated file: '%(lang)s'." % {"lang": arg})


def check(args):
    """
    Check the translation file
    """
    for arg in args:
        if arg == "all":
            continue
        print(
            "Checked file: '%(lang.po)s'. See '%(txt)s.txt'."
            % {"lang.po": arg, "txt": arg[:-3]}
        )
        os.system(
            """%(python)s ./check_po -s %(lang.po)s > %(lang)s.txt"""
            % {"python": pythonCmd, "lang.po": arg, "lang": arg[:-3]}
        )
        os.system(
            """%(msgfmt)s -c -v %(lang.po)s""" % {"msgfmt": msgfmtCmd, "lang.po": arg}
        )


def untranslated(arg):
    """
    List untranslated messages
    """
    os.system(
        """%(msgattrib)s --untranslated %(lang.po)s"""
        % {"msgattrib": msgattribCmd, "lang.po": arg[0]}
    )


def fuzzy(arg):
    """
    List fuzzy messages
    """
    os.system(
        """%(msgattrib)s --only-fuzzy --no-obsolete %(lang.po)s"""
        % {"msgattrib": msgattribCmd, "lang.po": arg[0]}
    )


if __name__ == "__main__":
    main()
