#!/opt/local/Library/Frameworks/Python.framework/Versions/3.13/bin/python3.13
#
# SCons - a Software Constructor
#
# 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.

"""Utility script to dump information from SCons signature database."""

import getopt
import importlib
import os
import sys
from dbm import whichdb

import time
import pickle

import SCons.compat
import SCons.SConsign


def my_whichdb(filename):
    if filename[-7:] == ".dblite":
        return "SCons.dblite"
    try:
        with open(filename + ".dblite", "rb"):
            return "SCons.dblite"
    except OSError:
        pass
    return whichdb(filename)


class Flagger:
    default_value = 1

    def __setitem__(self, item, value) -> None:
        self.__dict__[item] = value
        self.default_value = 0

    def __getitem__(self, item):
        return self.__dict__.get(item, self.default_value)


Do_Call = None
Print_Directories = []
Print_Entries = []
Print_Flags = Flagger()
Verbose = 0
Readable = 0
Warns = 0


def default_mapper(entry, name):
    """
    Stringify an entry that doesn't have an explicit mapping.

    Args:
        entry:  entry
        name: field name

    Returns: str

    """
    try:
        val = eval("entry." + name)
    except AttributeError:
        val = None
    return str(val)


def map_action(entry, _):
    """
    Stringify an action entry and signature.

    Args:
        entry: action entry
        second argument is not used

    Returns: str

    """
    try:
        bact = entry.bact
        bactsig = entry.bactsig
    except AttributeError:
        return None
    return '%s [%s]' % (bactsig, bact)


def map_timestamp(entry, _):
    """
    Stringify a timestamp entry.

    Args:
        entry: timestamp entry
        second argument is not used

    Returns: str

    """
    try:
        timestamp = entry.timestamp
    except AttributeError:
        timestamp = None
    if Readable and timestamp:
        return "'" + time.ctime(timestamp) + "'"
    else:
        return str(timestamp)


def map_bkids(entry, _):
    """
    Stringify an implicit entry.

    Args:
        entry:
        second argument is not used

    Returns: str

    """
    try:
        bkids = entry.bsources + entry.bdepends + entry.bimplicit
        bkidsigs = entry.bsourcesigs + entry.bdependsigs + entry.bimplicitsigs
    except AttributeError:
        return None

    if len(bkids) != len(bkidsigs):
        global Warns
        Warns += 1
        # add warning to result rather than direct print so it will line up
        msg = "Warning: missing information, {} ids but {} sigs"
        result = [msg.format(len(bkids), len(bkidsigs))]
    else:
        result = []
    result += [nodeinfo_string(bkid, bkidsig, "        ")
               for bkid, bkidsig in zip(bkids, bkidsigs)]
    if not result:
        return None
    return "\n        ".join(result)


map_field = {
    'action'    : map_action,
    'timestamp' : map_timestamp,
    'bkids'     : map_bkids,
}

map_name = {
    'implicit'  : 'bkids',
}


def field(name, entry, verbose=Verbose):
    if not Print_Flags[name]:
        return None
    fieldname = map_name.get(name, name)
    mapper = map_field.get(fieldname, default_mapper)
    val = mapper(entry, name)
    if verbose:
        val = name + ": " + val
    return val


def nodeinfo_raw(name, ninfo, prefix: str=""):
    """
    This just formats the dictionary, which we would normally use str()
    to do, except that we want the keys sorted for deterministic output.
    """
    d = ninfo.__getstate__()
    try:
        keys = ninfo.field_list + ['_version_id']
    except AttributeError:
        keys = sorted(d.keys())
    values = []
    for key in keys:
        values.append('%s: %s' % (repr(key), repr(d.get(key))))
    if '\n' in name:
        name = repr(name)
    return name + ': {' + ', '.join(values) + '}'


def nodeinfo_cooked(name, ninfo, prefix: str=""):
    try:
        field_list = ninfo.field_list
    except AttributeError:
        field_list = []
    if '\n' in name:
        name = repr(name)
    outlist = [name + ':'] + [
        f for f in [field(x, ninfo, Verbose) for x in field_list] if f
    ]
    if Verbose:
        sep = '\n    ' + prefix
    else:
        sep = ' '
    return sep.join(outlist)


nodeinfo_string = nodeinfo_cooked


def printfield(name, entry, prefix: str="") -> None:
    outlist = field("implicit", entry, 0)
    if outlist:
        if Verbose:
            print("    implicit:")
        print("        " + outlist)
    outact = field("action", entry, 0)
    if outact:
        if Verbose:
            print("    action: " + outact)
        else:
            print("        " + outact)


def printentries(entries, location) -> None:
    if Print_Entries:
        for name in Print_Entries:
            try:
                entry = entries[name]
            except KeyError:
                err = "sconsign: no entry `%s' in `%s'\n" % (name, location)
                sys.stderr.write(err)
            else:
                try:
                    ninfo = entry.ninfo
                except AttributeError:
                    print(name + ":")
                else:
                    print(nodeinfo_string(name, entry.ninfo))
                printfield(name, entry.binfo)
    else:
        for name in sorted(entries.keys()):
            entry = entries[name]
            try:
                entry.ninfo
            except AttributeError:
                print(name + ":")
            else:
                print(nodeinfo_string(name, entry.ninfo))
            printfield(name, entry.binfo)


class Do_SConsignDB:
    def __init__(self, dbm_name, dbm) -> None:
        self.dbm_name = dbm_name
        self.dbm = dbm

    def __call__(self, fname):
        # The *dbm modules stick their own file suffixes on the names
        # that are passed in.  This causes us to jump through some
        # hoops here.
        try:
            # Try opening the specified file name.  Example:
            #   SPECIFIED                  OPENED BY self.dbm.open()
            #   ---------                  -------------------------
            #   .sconsign               => .sconsign.dblite
            #   .sconsign.dblite        => .sconsign.dblite.dblite
            db = self.dbm.open(fname, "r")
        except OSError as e:
            print_e = e
            try:
                # That didn't work, so try opening the base name,
                # so that if they actually passed in 'sconsign.dblite'
                # (for example), the dbm module will put the suffix back
                # on for us and open it anyway.
                db = self.dbm.open(os.path.splitext(fname)[0], "r")
            except OSError:
                # That didn't work either.  See if the file name
                # they specified even exists (independent of the dbm
                # suffix-mangling).
                try:
                    with open(fname, "rb"):
                        pass  # this is a touch only, we don't use it here.
                except OSError as e:
                    # Nope, that file doesn't even exist, so report that
                    # fact back.
                    print_e = e
                sys.stderr.write("sconsign: %s\n" % print_e)
                return
        except KeyboardInterrupt:
            raise
        except pickle.UnpicklingError:
            sys.stderr.write("sconsign: ignoring invalid `%s' file `%s'\n"
                             % (self.dbm_name, fname))
            return
        except Exception as e:
            sys.stderr.write("sconsign: ignoring invalid `%s' file `%s': %s\n"
                             % (self.dbm_name, fname, e))
            exc_type, _, _ = sys.exc_info()
            if exc_type.__name__ == "ValueError":
                sys.stderr.write("unrecognized pickle protocol.\n")
            return

        if Print_Directories:
            for dir in Print_Directories:
                try:
                    val = db[dir]
                except KeyError:
                    err = "sconsign: no dir `%s' in `%s'\n" % (dir, args[0])
                    sys.stderr.write(err)
                else:
                    self.printentries(dir, val)
        else:
            for dir in sorted(db.keys()):
                self.printentries(dir, db[dir])

    @staticmethod
    def printentries(dir, val) -> None:
        try:
            print('=== ' + dir + ':')
        except TypeError:
            print('=== ' + dir.decode() + ':')
        printentries(pickle.loads(val), dir)


def Do_SConsignDir(name):
    try:
        with open(name, 'rb') as fp:
            try:
                sconsign = SCons.SConsign.Dir(fp)
            except KeyboardInterrupt:
                raise
            except pickle.UnpicklingError:
                err = "sconsign: ignoring invalid .sconsign file `%s'\n" % name
                sys.stderr.write(err)
                return
            except Exception as e:
                err = "sconsign: ignoring invalid .sconsign file `%s': %s\n" % (name, e)
                sys.stderr.write(err)
                return
            printentries(sconsign.entries, args[0])
    except OSError as e:
        sys.stderr.write("sconsign: %s\n" % e)
        return


##############################################################################
def main() -> None:
    global Do_Call
    global nodeinfo_string
    global args
    global Verbose
    global Readable

    helpstr = """\
Usage: sconsign [OPTIONS] [FILE ...]

Options:
  -a, --act, --action         Print build action information.
  -c, --csig                  Print content signature information.
  -d DIR, --dir=DIR           Print only info about DIR.
  -e ENTRY, --entry=ENTRY     Print only info about ENTRY.
  -f FORMAT, --format=FORMAT  FILE is in the specified FORMAT.
  -h, --help                  Print this message and exit.
  -i, --implicit              Print implicit dependency information.
  -r, --readable              Print timestamps in human-readable form.
  --raw                       Print raw Python object representations.
  -s, --size                  Print file sizes.
  -t, --timestamp             Print timestamp information.
  -v, --verbose               Verbose, describe each field.
"""

    try:
        opts, args = getopt.getopt(
            sys.argv[1:],
            'acd:e:f:hirstv',
            [
                'act',
                'action',
                'csig',
                'dir=',
                'entry=',
                'format=',
                'help',
                'implicit',
                'raw',
                'readable',
                'size',
                'timestamp',
                'verbose',
            ],
        )
    except getopt.GetoptError as err:
        sys.stderr.write(str(err) + '\n')
        print(helpstr)
        sys.exit(2)

    for o, a in opts:
        if o in ('-a', '--act', '--action'):
            Print_Flags['action'] = 1
        elif o in ('-c', '--csig'):
            Print_Flags['csig'] = 1
        elif o in ('-d', '--dir'):
            Print_Directories.append(a)
        elif o in ('-e', '--entry'):
            Print_Entries.append(a)
        elif o in ('-f', '--format'):
            # Try to map the given DB format to a known module
            # name, that we can then try to import...
            Module_Map = {'dblite': 'SCons.dblite', 'sconsign': None}
            dbm_name = Module_Map.get(a, a)
            if dbm_name:
                try:
                    if dbm_name != "SCons.dblite":
                        dbm = importlib.import_module(dbm_name)
                    else:
                        import SCons.dblite

                        dbm = SCons.dblite
                        # Ensure that we don't ignore corrupt DB files,
                        SCons.dblite.IGNORE_CORRUPT_DBFILES = False
                except ImportError:
                    sys.stderr.write("sconsign: illegal file format `%s'\n" % a)
                    print(helpstr)
                    sys.exit(2)
                Do_Call = Do_SConsignDB(a, dbm)
            else:
                Do_Call = Do_SConsignDir
        elif o in ('-h', '--help'):
            print(helpstr)
            sys.exit(0)
        elif o in ('-i', '--implicit'):
            Print_Flags['implicit'] = 1
        elif o in ('--raw',):
            nodeinfo_string = nodeinfo_raw
        elif o in ('-r', '--readable'):
            Readable = 1
        elif o in ('-s', '--size'):
            Print_Flags['size'] = 1
        elif o in ('-t', '--timestamp'):
            Print_Flags['timestamp'] = 1
        elif o in ('-v', '--verbose'):
            Verbose = 1

    if Do_Call:
        for a in args:
            Do_Call(a)
    else:
        if not args:
            args = [".sconsign.dblite"]
        for a in args:
            dbm_name = my_whichdb(a)
            if dbm_name:
                Map_Module = {'SCons.dblite': 'dblite'}
                if dbm_name != "SCons.dblite":
                    dbm = importlib.import_module(dbm_name)
                else:
                    import SCons.dblite

                    dbm = SCons.dblite
                    # Ensure that we don't ignore corrupt DB files,
                    SCons.dblite.IGNORE_CORRUPT_DBFILES = False
                Do_SConsignDB(Map_Module.get(dbm_name, dbm_name), dbm)(a)
            else:
                Do_SConsignDir(a)

        if Warns:
            print("NOTE: there were %d warnings, please check output" % Warns)


if __name__ == "__main__":
    main()
    sys.exit(0)

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