# Copyright 2004-2025 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 unicode_literals

import os
import codecs
import re
import math
import textwrap
import collections
import shutil

import renpy


class Define(object):

    def __init__(self, name, value, comment):
        self.name = name
        self.value = value
        self.comment = comment


# A map from language name to a list of defines.
language_defines = collections.defaultdict(list)


def translate_define(language, name, value, comment=None):
    """
    This function should be called to register the value of a define that is
    set when generating code in `language`.

    `name`
        A string giving the name of the define.

    `value`
        A string giving the value of the define. Be sure to quote it properly,
        or use repr().

    `comment`
        If not None, a comment that will be generated before the define. The
        comment will only be generated if the define does not exist in
        gui.rpy. There is no need to use "## ", as the comment will be
        added and wrapped automatically.
    """

    language_defines[language].append(Define(name, value, comment))


# A map from a language name to a list of (src, dst) pairs. Each represents a
# file that is copied into place.
language_copies = collections.defaultdict(list)


def translate_copy(language, src, dst):
    """
    This function should be called to copy a file from `src` to `dst`
    when generating code in `language`.

    `src`
        A path, relative to the launcher game directory.

    `dst`
        A path, relative to the game directory of the new game.
    """

    language_copies[language].append((src, dst))


# A map from language name and filename to code that should be added to the
# end of a newly-generated file.
language_code = collections.defaultdict(list)


def translate_code(language, filename, code):
    """
    This function can be called to include a block of code verbatim
    into `file` when a game is generated in `language`.
    """

    language_code[language, filename].extend([''] + code.split("\n"))


class CodeGenerator(object):
    """
    This is used to generate and update the GUI code.
    """

    def __init__(self, parameters):
        """
        Generates or updates gui.rpy.
        """

        self.p = parameters

    def load_template(self, filename):

        target = os.path.join(self.p.prefix, filename)

        if os.path.exists(target) and not self.p.replace_code:
            template = target
        else:
            template = os.path.join(self.p.template, filename)

        if not os.path.exists(template):
            return False
        else:
            with codecs.open(template, "r", "utf-8") as f:
                self.lines = [ i.rstrip().replace(u"\ufeff", "") for i in f ]

            return True

    def remove_scale(self):

        def scale(m):
            original = int(m.group(1))
            scaled = int(math.ceil(original * self.p.scale))
            return str(scaled)

        lines = [ ]

        for l in self.lines:
            l = re.sub(r'gui.scale\((.*?)\)', scale, l)
            lines.append(l)

        self.lines = lines

    def update_size(self):

        gui_init = "gui.init({}, {})".format(self.p.width, self.p.height)

        lines = [ ]

        for l in self.lines:
            l = re.sub(r'gui.init\(.*?\)', gui_init, l)
            lines.append(l)

        self.lines = lines

    def update_defines(self, replacements, additions=[]):
        """
        Replaces define statements in gui.rpy.
        """

        replacements = dict(replacements)

        for d in additions:
            replacements[d.name] = d.value

        seen = set()

        lines = [ ]

        for l in self.lines:

            m = re.match(r'^(\s*)define (.*?) =', l)

            if m:
                indent = m.group(1)
                variable = m.group(2)

                if variable in replacements:
                    l = "{}define {} = {}".format(indent, variable, replacements[variable])

                seen.add(variable)

            lines.append(l)

        for d in additions:

            if d.name in seen:
                continue

            seen.add(d.name)

            lines.append("")

            if d.comment:
                for s in textwrap.wrap(d.comment):
                    lines.append("## " + s)

            lines.append("define {} = {}".format(d.name, d.value))

        self.lines = lines

    def update_gui_defines(self):
        """
        Replaces define statements in gui.rpy.
        """

        replacements = {
            'gui.accent_color' : repr(self.p.accent_color.hexcode),
            'gui.selected_color' : repr(self.p.selected_color.hexcode),
            'gui.hover_color' : repr(self.p.hover_color.hexcode),
            'gui.muted_color' : repr(self.p.muted_color.hexcode),
            'gui.hover_muted_color' : repr(self.p.hover_muted_color.hexcode),
            'gui.title_color' : repr(self.p.title_color.hexcode),
            'gui.idle_color' : repr(self.p.idle_color.hexcode),
            'gui.idle_small_color' : repr(self.p.idle_small_color.hexcode),
            'gui.insensitive_color' : repr(self.p.insensitive_color.hexcode),
            'gui.text_color' : repr(self.p.text_color.hexcode),
            'gui.interface_text_color' : repr(self.p.text_color.hexcode),
            'gui.choice_button_text_idle_color' : repr(self.p.idle_color.hexcode),
            'gui.choice_button_text_insensitive_color' : repr(self.p.insensitive_color.hexcode),
            }

        self.update_defines(replacements, language_defines[self.p.language])

    def update_options_defines(self):
        """
        Replaces define statements in options.rpy.
        """

        def quote(s):
            s = s.replace("\\", "\\\\")
            s = s.replace("\"", "\\\"")
            return '"' + s + '"'

        replacements = {
            'config.name' : "_({})".format(quote(self.p.name)),
            'build.name' : quote(self.p.simple_name),
            'config.save_directory' : quote(self.p.savedir),
            }

        self.update_defines(replacements)

    def write_target(self, filename):

        target = os.path.join(self.p.prefix, filename)

        if os.path.exists(target):

            backup = 1

            while True:

                bfn = "{}.{}.bak".format(target, backup)

                if not os.path.exists(bfn):
                    break

                backup += 1

            if not self.p.skip_backup:
                os.rename(target, bfn)

        with codecs.open(target, "w", "utf-8") as f:
            f.write(u"\ufeff")

            for l in self.lines:
                f.write(l + "\r\n")

    def translate_strings(self):

        def replace(m):
            s = eval(m.group(1))
            s = renpy.translation.translate_string(s, language=self.p.language)
            s = renpy.translation.quote_unicode(s)

            quote = m.group(1)[0]

            s = u"_({}{}{})".format(quote, s, quote)

            return s

        lines = [ ]

        for l in self.lines:

            l = re.sub(r'_\((\".*?\")\)', replace, l)
            l = re.sub(r'_\((\'.*?\')\)', replace, l)

            lines.append(l)

        self.lines = lines

    def translate_comments(self):

        lines = [ ]

        comment = [ ]
        indent = ""

        for l in self.lines:

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

            if m:

                indent = m.group(1)
                c = m.group(2)

                if comment:
                    c = c.strip()

                comment.append(c)

            else:

                if comment:
                    s = "## " + ' '.join(comment)

                    if s.endswith("#"):
                        hashpad = True
                        s = s.rstrip('# ')
                    else:
                        hashpad = False

                    s = renpy.translation.translate_string(s, language=self.p.language)

                    m = re.match(r'## ?([ *]*)(.*)', s)

                    if m is None:
                        raise Exception("Comment translation doesn't start with '## ': {}".format(s))

                    prefix = m.group(1)
                    empty = ' ' * len(prefix)
                    rest = m.group(2)

                    len_prefix = len(indent) + len(prefix)
                    len_wrap = 80 - len_prefix

                    import store.gui

                    for i, s in enumerate(renpy.text.extras.textwrap(rest, len_wrap, store.gui.asian)):

                        if i == 0:
                            s = indent + prefix + s
                        else:
                            s = indent + empty + s

                        if hashpad and len(s) < 79:
                            s = s + ' ' + "#" * (79 - len(s))

                        lines.append(s)

                    comment = [ ]

                lines.append(l)

        self.lines = lines

    def copy_files(self):

        for src, dst in language_copies[self.p.language]:
            src = os.path.join(renpy.config.renpy_base, src)
            dst = os.path.join(self.p.prefix, dst)

            if os.path.exists(dst):
                continue

            dstdir = os.path.dirname(dst)

            if not os.path.exists(dstdir):
                os.makedirs(dstdir, 0o777)

            shutil.copy(src, dst)

    def copy_script(self, name):
        #dst = os.path.join(self.p.prefix, name)
        dst = name

        if os.path.exists(dst):
            return

        language = renpy.store._preferences.language # @UndefinedVariable

        if language is None:
            language = "None"

        src = os.path.join(renpy.config.gamedir, "tl", language, name + "m")

        if not os.path.exists(src):
            src = os.path.join(self.p.template, name)

        if not self.load_template(src):
            return

        self.remove_scale()
        self.write_target(dst)

    def add_code(self, fn):

        if not self.p.replace_code:
            return

        self.lines.extend(language_code[self.p.language, fn])

    def generate_gui(self, fn, defines=False):
        if not self.p.update_code:
            return

        if not self.load_template(fn):
            return

        if defines:
            self.update_gui_defines()

        if self.p.replace_code:
            self.remove_scale()
            self.update_size()
            self.translate_strings()
            self.translate_comments()
            self.add_code(fn)

        self.write_target(fn)

    def generate_code(self, fn):

        target = os.path.join(self.p.prefix, fn)

        if os.path.exists(target):
            return

        if not self.load_template(fn):
            return

        self.translate_strings()
        self.translate_comments()
        self.update_options_defines()

        self.add_code(fn)

        self.write_target(fn)
