# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

from __future__ import print_function

import renpy.translation

import re
import os
import time
import io
import codecs
import collections
import shutil

from renpy.translation import quote_unicode
from renpy.parser import elide_filename

################################################################################
# Translation Generation
################################################################################

STRING_RE = r"""(?x)
\b__?\s*\(\s*[uU]?(
\"\"\"(?:\\.|\"{1,2}|[^\\"])*?\"\"\"
|'''(?:\\.|\'{1,2}|[^\\'])*?'''
|"(?:\\.|[^\\"])*"
|'(?:\\.|[^\\'])*'
)\s*\)
"""


def scan_strings(filename):
    """
    Scans `filename`, a file containing Ren'Py script, for translatable
    strings.

    Generates a list of (line, string) tuples.
    """

    rv = [ ]

    for line, s in renpy.game.script.translator.additional_strings[filename]:  # @UndefinedVariable
        rv.append((line, s))

    line = 1

    for _filename, lineno, text in renpy.parser.list_logical_lines(filename):

        for m in re.finditer(STRING_RE, text):

            s = m.group(1)
            if s is not None:
                s = s.strip()
                s = "u" + s
                s = eval(s)
                rv.append((lineno, s))

    return rv


def scan_comments(filename):

    rv = [ ]

    if filename not in renpy.config.translate_comments:
        return rv

    comment = [ ]
    start = 0

    with codecs.open(filename, "r", "utf-8") as f:
        lines = [ i.rstrip() for i in f.read().replace(u"\ufeff", "").split('\n') ]

    for i, l in enumerate(lines):

        if not comment:
            start = i + 1

        m = re.match(r'\s*## (.*)', l)

        if m:
            c = m.group(1)

            if comment:
                c = c.strip()

            comment.append(c)

        elif comment:
            s = "## " + " ".join(comment)

            if s.endswith("#"):
                s = s.rstrip("# ")

            comment = [ ]

            rv.append((start, s))

    return rv


tl_file_cache = { }

# Should we write the TODO marker?
todo = True


def open_tl_file(fn):

    if fn in tl_file_cache:
        return tl_file_cache[fn]

    if not os.path.exists(fn):
        dn = os.path.dirname(fn)

        try:
            os.makedirs(dn)
        except:
            pass

        f = io.open(fn, "a", encoding="utf-8")
        f.write(u"\ufeff")

    else:
        f = io.open(fn, "a", encoding="utf-8")

    if todo:
        f.write(u"# TO" + "DO: Translation updated at {}\n".format(time.strftime("%Y-%m-%d %H:%M")))

    f.write(u"\n")

    tl_file_cache[fn] = f

    return f


def close_tl_files():

    for i in tl_file_cache.values():
        i.close()

    tl_file_cache.clear()


def shorten_filename(filename):
    """
    Shortens a file name. Returns the shortened filename, and a flag that says
    if the filename is in the common directory.
    """

    commondir = os.path.normpath(renpy.config.commondir)
    gamedir = os.path.normpath(renpy.config.gamedir)

    if filename.startswith(commondir):
        fn = os.path.relpath(filename, commondir)
        common = True

    elif filename.startswith(gamedir):
        fn = os.path.relpath(filename, gamedir)
        common = False

    else:
        fn = os.path.basename(filename)
        common = False

    return fn, common


def write_translates(filename, language, filter):  # @ReservedAssignment

    fn, common = shorten_filename(filename)

    # The common directory should not have dialogue in it.
    if common:
        return

    tl_filename = os.path.join(renpy.config.gamedir, renpy.config.tl_directory, language, fn)

    if tl_filename[-1] == "m":
        tl_filename = tl_filename[:-1]

    if language == "None":
        language = None

    translator = renpy.game.script.translator

    for label, t in translator.file_translates[filename]:

        if (t.identifier, language) in translator.language_translates:
            continue

        if hasattr(t, "alternate"):
            if (t.alternate, language) in translator.language_translates:
                continue

        f = open_tl_file(tl_filename)

        if label is None:
            label = ""

        f.write(u"# {}:{}\n".format(t.filename, t.linenumber))
        f.write(u"translate {} {}:\n".format(language, t.identifier.replace('.', '_')))
        f.write(u"\n")

        for n in t.block:
            f.write(u"    # " + n.get_code() + "\n")

        for n in t.block:
            f.write(u"    " + n.get_code(filter) + "\n")

        f.write(u"\n")


def translation_filename(s):

    if renpy.config.translate_launcher:
        return s.launcher_file

    if s.common:
        return "common.rpy"

    filename = s.elided

    if filename[-1] == "m":
        filename = filename[:-1]

    return filename


def write_strings(language, filter, min_priority, max_priority, common_only):  # @ReservedAssignment
    """
    Writes strings to the file.
    """

    if language == "None":
        stl = renpy.game.script.translator.strings[None]  # @UndefinedVariable
    else:
        stl = renpy.game.script.translator.strings[language]  # @UndefinedVariable

    # If this function changes, count_missing may also need to
    # change.

    strings = renpy.translation.scanstrings.scan(min_priority, max_priority, common_only)

    stringfiles = collections.defaultdict(list)

    for s in strings:

        tlfn = translation_filename(s)

        if tlfn is None:
            continue

        # Already seen.
        if s.text in stl.translations:
            continue

        if language == "None" and tlfn == "common.rpy":
            tlfn = "common.rpym"

        stringfiles[tlfn].append(s)

    for tlfn, sl in stringfiles.items():

        # sl.sort(key=lambda s : (s.filename, s.line))

        tlfn = os.path.join(renpy.config.gamedir, renpy.config.tl_directory, language, tlfn)
        f = open_tl_file(tlfn)

        f.write(u"translate {} strings:\n".format(language))
        f.write(u"\n")

        for s in sl:
            text = filter(s.text)

            f.write(u"    # {}:{}\n".format(elide_filename(s.filename), s.line))
            f.write(u"    old \"{}\"\n".format(quote_unicode(s.text)))
            f.write(u"    new \"{}\"\n".format(quote_unicode(text)))
            f.write(u"\n")


def null_filter(s):
    return s


def empty_filter(s):
    return ""


def generic_filter(s, function):
    """
    :doc: text_utility

    Transforms `s`, while leaving text tags and interpolation the same.

    `function`
        A function that is called with strings corresponding to runs of
        text, and should return a second string that replaces that run
        of text.

    ::

        init python:
            def upper(s):
                return s.upper()

        $ upper = renpy.transform_text("{b}Not Upper{/b}")

    """

    def remove_special(s, start, end, process):
        specials = 0
        first = False

        rv = ""
        buf = ""

        for i in s:

            if i == start:
                if first:
                    specials = 0
                else:
                    rv += process(buf)
                    buf = ""

                    if specials == 0:
                        first = True

                    specials += 1

                rv += start

            elif i == end:

                first = False

                specials -= 1
                if specials < 0:
                    specials += 1

                rv += end

            else:
                if specials:
                    rv += i
                else:
                    buf += i

        if buf:
            rv += process(buf)

        return rv

    def remove_braces(s):
        return remove_special(s, "{", "}", function)

    return remove_special(s, "[", "]", remove_braces)


def rot13_transform(s):

    ROT13 = { }

    for i, j in zip("ABCDEFGHIJKLM", "NMOPQRSTUVWYZ"):
        ROT13[i] = j
        ROT13[j] = i

        i = i.lower()
        j = j.lower()

        ROT13[i] = j
        ROT13[j] = i

    return "".join(ROT13.get(i, i) for i in s)


def rot13_filter(s):
    return generic_filter(s, rot13_transform)


def piglatin_transform(s):
    # Based on http://stackoverflow.com/a/23177629/3549890

    lst = ['sh', 'gl', 'ch', 'ph', 'tr', 'br', 'fr', 'bl', 'gr', 'st', 'sl', 'cl', 'pl', 'fl']

    def replace(m):
        i = m.group(0)

        if i[0] in ['a', 'e', 'i', 'o', 'u']:
            rv = i + 'ay'
        elif i[:2] in lst:
            rv = i[2:] + i[:2] + 'ay'
        else:
            rv = i[1:] + i[0] + 'ay'

        if i[0].isupper():
            rv = rv.capitalize()

        return rv

    return re.sub(r'\w+', replace, s)


def piglatin_filter(s):
    return generic_filter(s, piglatin_transform)


def translate_list_files():
    """
    Returns a list of files that exist and should be scanned for translations.
    """

    filenames = list(renpy.config.translate_files)

    for dirname, filename in renpy.loader.listdirfiles():
        if dirname is None:
            continue

        if filename.startswith("tl/"):
            continue

        filename = os.path.join(dirname, filename)

        if not (filename.endswith(".rpy") or filename.endswith(".rpym")):
            continue

        filename = os.path.normpath(filename)

        if not os.path.exists(filename):
            continue

        filenames.append(filename)

    return filenames


def count_missing(language, min_priority, max_priority, common_only):
    """
    Prints a count of missing translations for `language`.
    """

    translator = renpy.game.script.translator

    missing_translates = 0

    for filename in translate_list_files():
        for _, t in translator.file_translates[filename]:
            if (t.identifier, language) not in translator.language_translates:
                missing_translates += 1

    missing_strings = 0

    stl = renpy.game.script.translator.strings[language]  # @UndefinedVariable

    strings = renpy.translation.scanstrings.scan(min_priority, max_priority, common_only)

    for s in strings:

        tlfn = translation_filename(s)

        if tlfn is None:
            continue

        if s.text in stl.translations:
            continue

        missing_strings += 1

    print("{}: {} missing dialogue translations, {} missing string translations.".format(
        language,
        missing_translates,
        missing_strings
        ))


def translate_command():
    """
    The translate command. When called from the command line, this generates
    the translations.
    """

    ap = renpy.arguments.ArgumentParser(description="Generates or updates translations.")
    ap.add_argument("language", help="The language to generate translations for.")
    ap.add_argument("--rot13", help="Apply rot13 while generating translations.", dest="rot13", action="store_true")
    ap.add_argument("--piglatin", help="Apply pig latin while generating translations.", dest="piglatin", action="store_true")
    ap.add_argument("--empty", help="Produce empty strings while generating translations.", dest="empty", action="store_true")
    ap.add_argument("--count", help="Instead of generating files, print a count of missing translations.", dest="count", action="store_true")
    ap.add_argument("--min-priority", help="Translate strings with more than this priority.", dest="min_priority", default=0, type=int)
    ap.add_argument("--max-priority", help="Translate strings with more than this priority.", dest="max_priority", default=0, type=int)
    ap.add_argument("--strings-only", help="Only translate strings (not dialogue).", dest="strings_only", default=False, action="store_true")
    ap.add_argument("--common-only", help="Only translate string from the common code.", dest="common_only", default=False, action="store_true")
    ap.add_argument("--no-todo", help="Do not include the TODO flag.", dest="todo", default=True, action="store_false")

    args = ap.parse_args()

    global todo
    todo = args.todo

    if renpy.config.translate_launcher:
        max_priority = args.max_priority or 499
    else:
        max_priority = args.max_priority or 299

    if args.count:
        count_missing(args.language, args.min_priority, max_priority, args.common_only)
        return False

    if args.rot13:
        filter = rot13_filter  # @ReservedAssignment
    elif args.piglatin:
        filter = piglatin_filter  # @ReservedAssignment
    elif args.empty:
        filter = empty_filter  # @ReservedAssignment
    else:
        filter = null_filter  # @ReservedAssignment

    if not args.strings_only:
        for filename in translate_list_files():
            write_translates(filename, args.language, filter)

    write_strings(args.language, filter, args.min_priority, max_priority, args.common_only)

    close_tl_files()

    if renpy.config.translate_launcher and (not args.strings_only):
        src = os.path.join(renpy.config.renpy_base, "gui", "game", "script.rpy")
        dst = os.path.join(renpy.config.gamedir, "tl", args.language, "script.rpym")

        if os.path.exists(src) and not os.path.exists(dst):
            shutil.copy(src, dst)

    return False


renpy.arguments.register_command("translate", translate_command)
