# 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.

"""SCons interactive mode. """

# TODO:
#
# This has the potential to grow into something with a really big life
# of its own, which might or might not be a good thing.  Nevertheless,
# here are some enhancements that will probably be requested some day
# and are worth keeping in mind (assuming this takes off):
#
# - A command to re-read / re-load the SConscript files.  This may
#   involve allowing people to specify command-line options (e.g. -f,
#   -I, --no-site-dir) that affect how the SConscript files are read.
#
# - Additional command-line options on the "build" command.
#
#   Of the supported options that seemed to make sense (after a quick
#   pass through the list), the ones that seemed likely enough to be
#   used are listed in the man page and have explicit test scripts.
#
#   These had code changed in Script/Main.py to support them, but didn't
#   seem likely to be used regularly, so had no test scripts added:
#
#       build --diskcheck=*
#       build --implicit-cache=*
#       build --implicit-deps-changed=*
#       build --implicit-deps-unchanged=*
#
#   These look like they should "just work" with no changes to the
#   existing code, but like those above, look unlikely to be used and
#   therefore had no test scripts added:
#
#       build --random
#
#   These I'm not sure about.  They might be useful for individual
#   "build" commands, and may even work, but they seem unlikely enough
#   that we'll wait until they're requested before spending any time on
#   writing test scripts for them, or investigating whether they work.
#
#       build -q [???  is there a useful analog to the exit status?]
#       build --duplicate=
#       build --profile=
#       build --max-drift=
#       build --warn=*
#       build --Y
#
# - Most of the SCons command-line options that the "build" command
#   supports should be settable as default options that apply to all
#   subsequent "build" commands.  Maybe a "set {option}" command that
#   maps to "SetOption('{option}')".
#
# - Need something in the 'help' command that prints the -h output.
#
# - A command to run the configure subsystem separately (must see how
#   this interacts with the new automake model).
#
# - Command-line completion of target names; maybe even of SCons options?
#   Completion is something that's supported by the Python cmd module,
#   so this should be doable without too much trouble.
#

import cmd
import copy
import os
import re
import shlex
import sys

try:
    import readline
except ImportError:
    pass

class SConsInteractiveCmd(cmd.Cmd):
    """\

build [TARGETS]         Build the specified TARGETS and their dependencies. 'b' is a synonym.
clean [TARGETS]         Clean (remove) the specified TARGETS and their dependencies.  'c' is a synonym.
exit                    Exit SCons interactive mode.
help [COMMAND]          Prints help for the specified COMMAND.  'h' and '?' are synonyms.
shell [COMMANDLINE]     Execute COMMANDLINE in a subshell.  'sh' and '!' are synonyms.
version                 Prints SCons version information.
"""

    synonyms = {
        'b'     : 'build',
        'c'     : 'clean',
        'h'     : 'help',
        'scons' : 'build',
        'sh'    : 'shell',
    }

    def __init__(self, **kw) -> None:
        cmd.Cmd.__init__(self)
        for key, val in kw.items():
            setattr(self, key, val)

        if sys.platform == 'win32':
            self.shell_variable = 'COMSPEC'
        else:
            self.shell_variable = 'SHELL'

    def default(self, argv) -> None:
        print("*** Unknown command: %s" % argv[0])

    def onecmd(self, line):
        line = line.strip()
        if not line:
            print(self.lastcmd)
            return self.emptyline()
        self.lastcmd = line
        if line[0] == '!':
            line = 'shell ' + line[1:]
        elif line[0] == '?':
            line = 'help ' + line[1:]
        if os.sep == '\\':
            line = line.replace('\\', '\\\\')
        argv = shlex.split(line)
        argv[0] = self.synonyms.get(argv[0], argv[0])
        if not argv[0]:
            return self.default(line)
        else:
            try:
                func = getattr(self, 'do_' + argv[0])
            except AttributeError:
                return self.default(argv)
            return func(argv)

    def do_build(self, argv) -> None:
        """\
        build [TARGETS]         Build the specified TARGETS and their
                                dependencies.  'b' is a synonym.
        """
        import SCons.Node
        import SCons.SConsign
        import SCons.Script.Main

        options = copy.deepcopy(self.options)

        options, targets = self.parser.parse_args(argv[1:], values=options)

        SCons.Script.COMMAND_LINE_TARGETS = targets

        if targets:
            SCons.Script.BUILD_TARGETS = targets
        else:
            # If the user didn't specify any targets on the command line,
            # use the list of default targets.
            SCons.Script.BUILD_TARGETS = SCons.Script._build_plus_default

        nodes = SCons.Script.Main._build_targets(self.fs,
                                                 options,
                                                 targets,
                                                 self.target_top)

        if not nodes:
            return

        # Call each of the Node's alter_targets() methods, which may
        # provide additional targets that ended up as part of the build
        # (the canonical example being a VariantDir() when we're building
        # from a source directory) and which we therefore need their
        # state cleared, too.
        x = []
        for n in nodes:
            x.extend(n.alter_targets()[0])
        nodes.extend(x)

        # Clean up so that we can perform the next build correctly.
        #
        # We do this by walking over all the children of the targets,
        # and clearing their state.
        #
        # We currently have to re-scan each node to find their
        # children, because built nodes have already been partially
        # cleared and don't remember their children.  (In scons
        # 0.96.1 and earlier, this wasn't the case, and we didn't
        # have to re-scan the nodes.)
        #
        # Because we have to re-scan each node, we can't clear the
        # nodes as we walk over them, because we may end up rescanning
        # a cleared node as we scan a later node.  Therefore, only
        # store the list of nodes that need to be cleared as we walk
        # the tree, and clear them in a separate pass.
        #
        # XXX: Someone more familiar with the inner workings of scons
        # may be able to point out a more efficient way to do this.

        SCons.Script.Main.progress_display("scons: Clearing cached node information ...")

        seen_nodes = {}

        def get_unseen_children(node, parent, seen_nodes=seen_nodes):
            def is_unseen(node, seen_nodes=seen_nodes) -> bool:
                return node not in seen_nodes
            return [child for child in node.children(scan=1) if is_unseen(child)]

        def add_to_seen_nodes(node, parent, seen_nodes=seen_nodes) -> None:
            seen_nodes[node] = 1

            # If this file is in a VariantDir and has a
            # corresponding source file in the source tree, remember the
            # node in the source tree, too.  This is needed in
            # particular to clear cached implicit dependencies on the
            # source file, since the scanner will scan it if the
            # VariantDir was created with duplicate=0.
            try:
                rfile_method = node.rfile
            except AttributeError:
                return
            else:
                rfile = rfile_method()
            if rfile != node:
                seen_nodes[rfile] = 1

        for node in nodes:
            walker = SCons.Node.Walker(node,
                                        kids_func=get_unseen_children,
                                        eval_func=add_to_seen_nodes)
            n = walker.get_next()
            while n:
                n = walker.get_next()

        for node in seen_nodes.keys():
            # Call node.clear() to clear most of the state
            node.clear()
            # node.clear() doesn't reset node.state, so call
            # node.set_state() to reset it manually
            node.set_state(SCons.Node.no_state)
            node.implicit = None

            # Debug:  Uncomment to verify that all Taskmaster reference
            # counts have been reset to zero.
            #if node.ref_count != 0:
            #    from SCons.Debug import Trace
            #    Trace('node %s, ref_count %s !!!\n' % (node, node.ref_count))

        # TODO: REMOVE WPD DEBUG 02/14/2022
        # This call was clearing the list of sconsign files to be written, so it would
        # only write the results of the first build command. All others wouldn't be written
        # to .SConsign.
        # Pretty sure commenting this out is the correct fix.
        # SCons.SConsign.Reset()
        SCons.Script.Main.progress_display("scons: done clearing node information.")

    def do_clean(self, argv):
        """\
        clean [TARGETS]         Clean (remove) the specified TARGETS
                                and their dependencies.  'c' is a synonym.
        """
        return self.do_build(['build', '--clean'] + argv[1:])

    def do_EOF(self, argv) -> None:
        print()
        self.do_exit(argv)

    def _do_one_help(self, arg) -> None:
        try:
            # If help_<arg>() exists, then call it.
            func = getattr(self, 'help_' + arg)
        except AttributeError:
            try:
                func = getattr(self, 'do_' + arg)
            except AttributeError:
                doc = None
            else:
                doc = self._doc_to_help(func)
            if doc:
                sys.stdout.write(doc + '\n')
                sys.stdout.flush()
        else:
            doc = self.strip_initial_spaces(func())
            if doc:
                sys.stdout.write(doc + '\n')
                sys.stdout.flush()

    def _doc_to_help(self, obj):
        doc = obj.__doc__
        if doc is None:
            return ''
        return self._strip_initial_spaces(doc)

    def _strip_initial_spaces(self, s):
        lines = s.split('\n')
        spaces = re.match(' *', lines[0]).group(0)
        def strip_spaces(l, spaces=spaces):
            if l[:len(spaces)] == spaces:
                l = l[len(spaces):]
            return l
        lines = list(map(strip_spaces, lines))
        return '\n'.join(lines)

    def do_exit(self, argv) -> None:
        """\
        exit                    Exit SCons interactive mode.
        """
        sys.exit(0)

    def do_help(self, argv) -> None:
        """\
        help [COMMAND]          Prints help for the specified COMMAND.  'h'
                                and '?' are synonyms.
        """
        if argv[1:]:
            for arg in argv[1:]:
                if self._do_one_help(arg):
                    break
        else:
            # If bare 'help' is called, print this class's doc
            # string (if it has one).
            doc = self._doc_to_help(self.__class__)
            if doc:
                sys.stdout.write(doc + '\n')
                sys.stdout.flush()

    def do_shell(self, argv) -> None:
        """\
        shell [COMMANDLINE]     Execute COMMANDLINE in a subshell.  'sh' and
                                '!' are synonyms.
        """
        import subprocess
        argv = argv[1:]
        if not argv:
            argv = os.environ[self.shell_variable]
        try:
            # Per "[Python-Dev] subprocess insufficiently platform-independent?"
            # https://mail.python.org/pipermail/python-dev/2008-August/081979.html "+
            # Doing the right thing with an argument list currently
            # requires different shell= values on Windows and Linux.
            p = subprocess.Popen(argv, shell=(sys.platform=='win32'))
        except OSError as e:
            sys.stderr.write('scons: %s: %s\n' % (argv[0], e.strerror))
        else:
            p.wait()

    def do_version(self, argv) -> None:
        """\
        version                 Prints SCons version information.
        """
        sys.stdout.write(self.parser.version + '\n')

def interact(fs, parser, options, targets, target_top) -> None:
    c = SConsInteractiveCmd(prompt = 'scons>>> ',
                            fs = fs,
                            parser = parser,
                            options = options,
                            targets = targets,
                            target_top = target_top)
    c.cmdloop()

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