# MIT License
#
# Copyright The SCons Foundation
#
# 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.

"""Tool specific initialization of `xgettext` tool."""

import os
import re
import sys
from subprocess import PIPE

import SCons.Action
import SCons.Node.FS
import SCons.Tool
import SCons.Util
import SCons.Warnings
from SCons.Builder import BuilderBase
from SCons.Environment import _null
from SCons.Platform.cygwin import CYGWIN_DEFAULT_PATHS
from SCons.Platform.mingw import MINGW_DEFAULT_PATHS
from SCons.Tool.GettextCommon import (
    _detect_xgettext,
    _POTargetFactory,
    RPaths,
    _xgettext_exists,
    # XgettextToolWarning,
)


class _CmdRunner:
    """ Callable object, which runs shell command storing its stdout and stderr to
    variables. It also provides `strfunction()` method, which shall be used by
    scons Action objects to print command string. """

    def __init__(self, command, commandstr=None) -> None:
        self.out = None
        self.err = None
        self.status = None
        self.command = command
        self.commandstr = commandstr

    def __call__(self, target, source, env):
        kw = {
            'stdout': PIPE,
            'stderr': PIPE,
            'universal_newlines': True,
            'shell': True
        }
        command = env.subst(self.command, target=target, source=source)
        cp = SCons.Action.scons_subproc_run(env, command, **kw)
        self.status = cp.returncode
        self.out = cp.stdout
        self.err = cp.stderr
        if self.err:
            sys.stderr.write(str(self.err))
        return self.status

    def strfunction(self, target, source, env):
        comstr = self.commandstr
        if env.subst(comstr, target=target, source=source) == "":
            comstr = self.command
        s = env.subst(comstr, target=target, source=source)
        return s


def _update_pot_file(target, source, env):
    """ Action function for `POTUpdate` builder """
    nop = lambda target, source, env: 0

    # Save scons cwd and os cwd (NOTE: they may be different. After the job, we
    # revert each one to its original state).
    save_cwd = env.fs.getcwd()
    save_os_cwd = os.getcwd()
    chdir = target[0].dir
    chdir_str = repr(chdir.get_abspath())
    # Print chdir message (employ SCons.Action.Action for that. It knows better
    # than me how to to this correctly).
    env.Execute(SCons.Action.Action(nop, "Entering " + chdir_str))
    # Go to target's directory and do our job
    env.fs.chdir(chdir, 1)  # Go into target's directory
    try:
        cmd = _CmdRunner('$XGETTEXTCOM', '$XGETTEXTCOMSTR')
        action = SCons.Action.Action(cmd, strfunction=cmd.strfunction)
        status = action([target[0]], source, env)
    except:
        # Something went wrong.
        env.Execute(SCons.Action.Action(nop, "Leaving " + chdir_str))
        # Revert working dirs to previous state and re-throw exception.
        env.fs.chdir(save_cwd, 0)
        os.chdir(save_os_cwd)
        raise
    # Print chdir message.
    env.Execute(SCons.Action.Action(nop, "Leaving " + chdir_str))
    # Revert working dirs to previous state.
    env.fs.chdir(save_cwd, 0)
    os.chdir(save_os_cwd)
    # If the command was not successfull, return error code.
    if status: return status

    new_content = cmd.out

    if not new_content:
        # When xgettext finds no internationalized messages, no *.pot is created
        # (because we don't want to bother translators with empty POT files).
        needs_update = False
        explain = "no internationalized messages encountered"
    else:
        if target[0].exists():
            # If the file already exists, it's left unaltered unless its messages
            # are outdated (w.r.t. to these recovered by xgettext from sources).
            old_content = target[0].get_text_contents()
            re_cdate = re.compile(r'^"POT-Creation-Date: .*"$[\r\n]?', re.M)
            old_content_nocdate = re.sub(re_cdate, "", old_content)
            new_content_nocdate = re.sub(re_cdate, "", new_content)
            if old_content_nocdate == new_content_nocdate:
                # Messages are up-to-date
                needs_update = False
                explain = "messages in file found to be up-to-date"
            else:
                # Messages are outdated
                needs_update = True
                explain = "messages in file were outdated"
        else:
            # No POT file found, create new one
            needs_update = True
            explain = "new file"
    if needs_update:
        # Print message employing SCons.Action.Action for that.
        msg = "Writing " + repr(str(target[0])) + " (" + explain + ")"
        env.Execute(SCons.Action.Action(nop, msg))
        f = open(str(target[0]), "w")
        f.write(new_content)
        f.close()
        return 0
    else:
        # Print message employing SCons.Action.Action for that.
        msg = "Not writing " + repr(str(target[0])) + " (" + explain + ")"
        env.Execute(SCons.Action.Action(nop, msg))
        return 0


class _POTBuilder(BuilderBase):
    def _execute(self, env, target, source, *args):
        if not target:
            if 'POTDOMAIN' in env and env['POTDOMAIN']:
                domain = env['POTDOMAIN']
            else:
                domain = 'messages'
            target = [domain]
        return BuilderBase._execute(self, env, target, source, *args)


def _scan_xgettext_from_files(target, source, env, files=None, path=None) -> int:
    """ Parses `POTFILES.in`-like file and returns list of extracted file names.
    """
    if files is None:
        return 0
    if not SCons.Util.is_List(files):
        files = [files]

    if path is None:
        if 'XGETTEXTPATH' in env:
            path = env['XGETTEXTPATH']
        else:
            path = []
    if not SCons.Util.is_List(path):
        path = [path]

    path = SCons.Util.flatten(path)

    dirs = ()
    for p in path:
        if not isinstance(p, SCons.Node.FS.Base):
            if SCons.Util.is_String(p):
                p = env.subst(p, source=source, target=target)
            p = env.arg2nodes(p, env.fs.Dir)
        dirs += tuple(p)
    # cwd is the default search path (when no path is defined by user)
    if not dirs:
        dirs = (env.fs.getcwd(),)

    # Parse 'POTFILE.in' files.
    re_comment = re.compile(r'^#[^\n\r]*$\r?\n?', re.M)
    re_emptyln = re.compile(r'^[ \t\r]*$\r?\n?', re.M)
    re_trailws = re.compile(r'[ \t\r]+$')
    for f in files:
        # Find files in search path $XGETTEXTPATH
        if isinstance(f, SCons.Node.FS.Base) and f.rexists():
            contents = f.get_text_contents()
            contents = re_comment.sub("", contents)
            contents = re_emptyln.sub("", contents)
            contents = re_trailws.sub("", contents)
            depnames = contents.splitlines()
            for depname in depnames:
                depfile = SCons.Node.FS.find_file(depname, dirs)
                if not depfile:
                    depfile = env.arg2nodes(depname, dirs[0].File)
                env.Depends(target, depfile)
    return 0


def _pot_update_emitter(target, source, env):
    """ Emitter function for `POTUpdate` builder """
    if 'XGETTEXTFROM' in env:
        xfrom = env['XGETTEXTFROM']
    else:
        return target, source
    if not SCons.Util.is_List(xfrom):
        xfrom = [xfrom]

    xfrom = SCons.Util.flatten(xfrom)

    # Prepare list of 'POTFILE.in' files.
    files = []
    for xf in xfrom:
        if not isinstance(xf, SCons.Node.FS.Base):
            if SCons.Util.is_String(xf):
                # Interpolate variables in strings
                xf = env.subst(xf, source=source, target=target)
            xf = env.arg2nodes(xf)
        files.extend(xf)
    if files:
        env.Depends(target, files)
        _scan_xgettext_from_files(target, source, env, files)
    return target, source


def _POTUpdateBuilderWrapper(env, target=None, source=_null, **kw):
    return env._POTUpdateBuilder(target, source, **kw)


def _POTUpdateBuilder(env, **kw):
    """ Creates `POTUpdate` builder object """
    kw['action'] = SCons.Action.Action(_update_pot_file, None)
    kw['suffix'] = '$POTSUFFIX'
    kw['target_factory'] = _POTargetFactory(env, alias='$POTUPDATE_ALIAS').File
    kw['emitter'] = _pot_update_emitter
    return _POTBuilder(**kw)


def generate(env, **kw) -> None:
    """ Generate `xgettext` tool """

    if sys.platform == 'win32':
        xgettext = SCons.Tool.find_program_path(env, 'xgettext', default_paths=MINGW_DEFAULT_PATHS + CYGWIN_DEFAULT_PATHS )
        if xgettext:
            xgettext_bin_dir = os.path.dirname(xgettext)
            env.AppendENVPath('PATH', xgettext_bin_dir)
        else:
            SCons.Warnings.warn(
                # XgettextToolWarning,  # using this breaks test, so keep:
                SCons.Warnings.SConsWarning,
                'xgettext tool requested, but binary not found in ENV PATH'
            )
    try:
        env['XGETTEXT'] = _detect_xgettext(env)
    except:
        env['XGETTEXT'] = 'xgettext'
    # NOTE: sources="$SOURCES" would work as well. However, we use following
    # construction to convert absolute paths provided by scons onto paths
    # relative to current working dir. Note, that scons expands $SOURCE(S) to
    # absolute paths for sources $SOURCE(s) outside of current subtree (e.g. in
    # "../"). With source=$SOURCE these absolute paths would be written to the
    # resultant *.pot file (and its derived *.po files) as references to lines in
    # source code (e.g. referring lines in *.c files). Such references would be
    # correct (e.g. in poedit) only on machine on which *.pot was generated and
    # would be of no use on other hosts (having a copy of source code located
    # in different place in filesystem).
    sources = '$( ${_concat( "", SOURCES, "", __env__, XgettextRPaths, TARGET' \
              + ', SOURCES)} $)'

    # NOTE: the output from $XGETTEXTCOM command must go to stdout, not to a file.
    # This is required by the POTUpdate builder's action.
    xgettextcom = '$XGETTEXT $XGETTEXTFLAGS $_XGETTEXTPATHFLAGS' \
                  + ' $_XGETTEXTFROMFLAGS -o - ' + sources

    xgettextpathflags = '$( ${_concat( XGETTEXTPATHPREFIX, XGETTEXTPATH' \
                        + ', XGETTEXTPATHSUFFIX, __env__, RDirs, TARGET, SOURCES)} $)'
    xgettextfromflags = '$( ${_concat( XGETTEXTFROMPREFIX, XGETTEXTFROM' \
                        + ', XGETTEXTFROMSUFFIX, __env__, target=TARGET, source=SOURCES)} $)'

    env.SetDefault(
        _XGETTEXTDOMAIN='${TARGET.filebase}',
        XGETTEXTFLAGS=[],
        XGETTEXTCOM=xgettextcom,
        XGETTEXTCOMSTR='',
        XGETTEXTPATH=[],
        XGETTEXTPATHPREFIX='-D',
        XGETTEXTPATHSUFFIX='',
        XGETTEXTFROM=None,
        XGETTEXTFROMPREFIX='-f',
        XGETTEXTFROMSUFFIX='',
        _XGETTEXTPATHFLAGS=xgettextpathflags,
        _XGETTEXTFROMFLAGS=xgettextfromflags,
        POTSUFFIX=['.pot'],
        POTUPDATE_ALIAS='pot-update',
        XgettextRPaths=RPaths(env)
    )
    env.Append(BUILDERS={
        '_POTUpdateBuilder': _POTUpdateBuilder(env)
    })
    env.AddMethod(_POTUpdateBuilderWrapper, 'POTUpdate')
    env.AlwaysBuild(env.Alias('$POTUPDATE_ALIAS'))


def exists(env):
    """ Check, whether the tool exists """
    try:
        return _xgettext_exists(env)
    except:
        return False


# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:
